Skip to content

Commit

Permalink
Add animation abstraction & some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nineonine committed Oct 11, 2023
1 parent ca8e8c1 commit b3d3b68
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 49 deletions.
24 changes: 24 additions & 0 deletions frontend/src/HeapGrid.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,27 @@
left: 50%;
transform: translate(-50%, -50%);
}

.cell.flashing {
animation: flashAnimation 0.5s ease-in-out infinite;
}

.cell.flickering {
animation: flickerAnimation 0.5s linear infinite;
}

.cell.highlighted-margins {
box-shadow: 0 0 5px 2px rgba(255, 255, 255, 0.8);
}

@keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

@keyframes flicker {
0%, 100% { opacity: 1; }
25% { opacity: 0.75; }
50% { opacity: 0.5; }
75% { opacity: 0.75; }
}
45 changes: 42 additions & 3 deletions frontend/src/HeapGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';
import { CellStatus, MemoryCell } from './types';
import './HeapGrid.css';
import { AnimatedCell } from './useHeapAnimation';

interface HeapGridProps {
memory: MemoryCell[];
highlightedCells: number[];
animatedCells: AnimatedCell[];
}

const cellStyleMap: Record<CellStatus, string> = {
Expand All @@ -15,23 +17,60 @@ const cellStyleMap: Record<CellStatus, string> = {
[CellStatus.Used]: '#228B22',
};

const HeapGrid: React.FC<HeapGridProps> = ({ memory, highlightedCells }) => {
const HeapGrid: React.FC<HeapGridProps> = ({ memory, highlightedCells, animatedCells }) => {
const memoryLen = memory.length;
const numCols = Math.ceil(Math.sqrt(memoryLen));

const isFirstHighlighted = (index: number) => highlightedCells[0] === index;

const getAnimationStyle = (index: number): React.CSSProperties => {
const cellAnimation: AnimatedCell | undefined = animatedCells.find(anim => anim.cellIndex === index);
if (!cellAnimation) return {};

let baseStyle: React.CSSProperties = {
};

switch (cellAnimation.animation.type) {
case 'flashing':
return {
...baseStyle,
animation: `flash ${cellAnimation.animation.duration}ms`,
opacity: cellAnimation.animation.opacity.toString()
};
case 'flickering':
return {
...baseStyle,
animation: `flicker ${cellAnimation.animation.duration}ms`,
opacity: cellAnimation.animation.opacity.toString()
};
case 'highlighted-margins':
return {
...baseStyle,
border: '2px solid black',
opacity: cellAnimation.animation.opacity.toString()
};
default:
return baseStyle;
}
}

return (
<div
className="heap-grid"
style={{ gridTemplateColumns: `repeat(${numCols}, 1fr)` }}
>
{memory.map((cell, index) => {
const isHighlighted = highlightedCells.includes(index);
const animationStyle = getAnimationStyle(index);

return (
<div
key={index}
className={`cell ${highlightedCells.includes(index) ? 'highlighted' : ''}`}
style={{ backgroundColor: cellStyleMap[cell.status] }}
className={`cell ${isHighlighted ? 'highlighted' : ''}`}
style={{
...animationStyle,
backgroundColor: cellStyleMap[cell.status]
}}
data-address={isFirstHighlighted(index) ? `0x${index.toString(16).toUpperCase()}` : undefined}
/>
);
Expand Down
86 changes: 61 additions & 25 deletions frontend/src/Visualization.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

import './Visualization.css';
import InfoBlock from './InfoBlock';
import EventStream from './EventStream';
import HeapGrid from './HeapGrid';
import ControlPanel from './ControlPanel';
import { CellStatus, MemoryCell, RESET_MSG, STEP_MSG, TICK_MSG, InfoBlockData, INFOBLOCK_DEFAULT, GCEvent, LogEntry } from './types';
import { CellStatus, MemoryCell, RESET_MSG, STEP_MSG, TICK_MSG, InfoBlockData, INFOBLOCK_DEFAULT, GCEvent, LogEntry, WSMsgRequest } from './types';
import Slider from './Slider';
import Toast from './Toast';

import { SUGGEST_INIT_LOG_ENTRY, mkLogEntry } from './logUtils';
import useHighlightState from './useHightlightState';
import { EventOps, gcEventOps, logEntryOps } from './eventlog';
import useHeapAnimation, { TimedAnimation, createTimedAnimation } from './useHeapAnimation';

function isLogEntry(event: LogEntry | GCEvent): event is LogEntry {
return (event as LogEntry).source !== undefined;
Expand All @@ -34,7 +34,10 @@ const Visualization: React.FC = () => {
highlightedCells,
highlightCells,
clearHighlightedCells,
} = useHighlightState();
animatedCells,
enqueueAnimation,
clearAnimations
} = useHeapAnimation();
const { program_name } = useParams<{ program_name?: string }>();
const [toastMessage, setToastMessage] = useState<string>('');
const [infoBlock, setInfoBlock] = useState<InfoBlockData>(INFOBLOCK_DEFAULT);
Expand All @@ -50,8 +53,10 @@ const Visualization: React.FC = () => {
setInfoBlock(resetInfoBlock(infoBlock, memory.length))
setMemory(new Array(0).fill({ status: CellStatus.Free }));
setEventLogs([SUGGEST_INIT_LOG_ENTRY]);
setPendingGCEvents([]);
setGCEventLogs([]);
clearHighlightedCells();
clearAnimations();
}

const handleRestart = () => {
Expand All @@ -66,7 +71,6 @@ const Visualization: React.FC = () => {
};

useEffect(() => {
// Initialize WebSocket connection only once when component mounts
const wsConnection = new WebSocket(BACKEND);
setWs(wsConnection);
console.log('Established ws conn');
Expand Down Expand Up @@ -129,38 +133,38 @@ const Visualization: React.FC = () => {
};
}, [program_name]);

const nextStep = useCallback((msg: WSMsgRequest)=> {
// Process a single GC event if any
if (pendingGCEvents.length > 0) {
const currentGCEvent: GCEvent = pendingGCEvents[0];
setGCEventLogs(prevLogs => [...prevLogs, currentGCEvent]);
if (eventHasAnimation(currentGCEvent)) {
const cellIndex: number = cellIndexFromEvent(currentGCEvent)!;
enqueueAnimation(cellIndex, animationFromGCEvent(currentGCEvent));
}
setPendingGCEvents(prevGCEvents => prevGCEvents.slice(1));
} else if (ws?.readyState === WebSocket.OPEN) {
setGCEventLogs([]);
ws.send(JSON.stringify(msg));
}
}, [setGCEventLogs, enqueueAnimation, pendingGCEvents, ws]);

useEffect(() => {
let intervalId: any = null;
if (isRunning) {
intervalId = setInterval(() => {
// Process a single GC event if any
if (pendingGCEvents.length > 0) {
const currentGCEvent = pendingGCEvents[0];
setGCEventLogs(prevLogs => [...prevLogs, currentGCEvent]);
setPendingGCEvents(prevGCEvents => prevGCEvents.slice(1));
} else if (ws?.readyState === WebSocket.OPEN) {
setGCEventLogs([]);
ws.send(JSON.stringify(TICK_MSG));
}
nextStep(TICK_MSG)
}, intervalRate);
}

return () => {
intervalId && clearInterval(intervalId);
};
}, [isRunning, ws, intervalRate, gcEventLogs, pendingGCEvents]);
}, [isRunning, ws, intervalRate, gcEventLogs, pendingGCEvents, nextStep]);

const stepTick = () => {
if (!isRunning) {
// If there's a pending GC event, process it
if (pendingGCEvents.length > 0) {
const currentGCEvent = pendingGCEvents[0];
setGCEventLogs(prevLogs => [...prevLogs, currentGCEvent]);
setPendingGCEvents(prevGCEvents => prevGCEvents.slice(1));
} else if (ws?.readyState === WebSocket.OPEN) {
setGCEventLogs([]);
ws.send(JSON.stringify(STEP_MSG));
}
nextStep(STEP_MSG)
}
}

Expand Down Expand Up @@ -203,7 +207,7 @@ const Visualization: React.FC = () => {

<div className='extra-details'></div>
</div>
<HeapGrid memory={memory} highlightedCells={highlightedCells} />
<HeapGrid memory={memory} highlightedCells={highlightedCells} animatedCells={animatedCells}/>
</div>
<ControlPanel isRunning={isRunning}
toggleExecution={toggleExecution}
Expand All @@ -224,3 +228,35 @@ const resetInfoBlock = (infoBlock: InfoBlockData, heapSize: number): InfoBlockDa
free_memory: heapSize,
}
}

const eventHasAnimation = (gcevent: GCEvent): boolean => {
return ['MarkObject', 'FreeObject'].includes(gcevent.type)
}

const animationFromGCEvent = (event: GCEvent): TimedAnimation => {
let animation: TimedAnimation;

switch (event.type) {
case "MarkObject":
animation = createTimedAnimation(500, 'flashing', 0.5);
break;
case "FreeObject":
animation = createTimedAnimation(500, 'flickering', 0.5);
break;
default:
throw new Error(`Unsupported GCEvent: ${event}`);
}

return animation;
}

export const cellIndexFromEvent = (event: GCEvent): number | null => {
switch (event.type) {
case "MarkObject":
case "FreeObject":
return event.addr;
case "GCPhase":
default:
return null;
}
};
74 changes: 74 additions & 0 deletions frontend/src/useHeapAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState } from 'react';

export interface TimedAnimation {
duration: number;
type: AnimationType;
opacity: number;
newColor?: string;
}

type AnimationType = 'flashing' | 'flickering' | 'highlighted-margins';

export interface AnimatedCell {
cellIndex: number;
animation: TimedAnimation;
}

export const createTimedAnimation = (
duration: number,
type: AnimationType,
opacity: number,
newColor?:string
): TimedAnimation => ({
duration,
type,
opacity,
newColor,
});

const useHeapAnimation = () => {
const [highlightedCells, setHighlightedCells] = useState<number[]>([]);
const [animatedCells, setAnimatedCells] = useState<AnimatedCell[]>([]);
const [animationTimeouts, setAnimationTimeouts] = useState<number[]>([]);

const highlightCells = (cellIndices: number[]) => {
setHighlightedCells(cellIndices);
};

const clearHighlightedCells = () => {
setHighlightedCells([]);
};

const enqueueAnimation = (cellIndex: number, animation: TimedAnimation) => {
// Remove any existing animation for this cell
setAnimatedCells(prevState => prevState.filter(cell => cell.cellIndex !== cellIndex));
setAnimatedCells(prevState => [...prevState, { cellIndex, animation }]);

// Set timeout to clear the animation after its duration
const timeout = window.setTimeout(() => {
setAnimatedCells(prevState => prevState.filter(cell => cell.cellIndex !== cellIndex));
}, animation.duration);

setAnimationTimeouts(prevState => [...prevState, timeout]);
};

const clearAnimations = () => {
// Clear all animated cells
setAnimatedCells([]);

// Clear any pending animation timeouts
animationTimeouts.forEach(timeout => clearTimeout(timeout));
setAnimationTimeouts([]);
};

return {
highlightedCells,
highlightCells,
clearHighlightedCells,
animatedCells,
enqueueAnimation,
clearAnimations
};
};

export default useHeapAnimation;
21 changes: 0 additions & 21 deletions frontend/src/useHightlightState.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions frontend/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const darkenColor = (hex: string, percent: number): string => {
// Convert the hex to RGB values
let r: number = parseInt(hex.substring(1, 3), 16);
let g: number = parseInt(hex.substring(3, 5), 16);
let b: number = parseInt(hex.substring(5, 7), 16);

// Calculate the adjustment value
let adjust = (amount: number, color: number) => {
return Math.round(color * (1 - amount));
};

r = adjust(percent, r);
g = adjust(percent, g);
b = adjust(percent, b);

// Convert the RGB values back to hex
return "#" + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
}
5 changes: 5 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,8 @@ fn test_mark_sweep_2() {
fn test_mark_sweep_3() {
assert!(__test("mark_sweep_3").is_ok());
}

#[test]
fn test_mark_sweep_4() {
assert!(__test("mark_sweep_4").is_ok());
}
Loading

0 comments on commit b3d3b68

Please sign in to comment.