Reputation Trend Chart Canvas Freeze During Concurrent Slashing and Reward Events
Problem Statement
The reputation trend chart in src/components/reputation/ReputationChart.tsx renders a time-series line chart of a node's reputation score over time using a <canvas> element with Chart.js. The updateChartData() function at line 80 receives new data points via WebSocket (one per reputation change event). When a node experiences a slashing (-500) followed immediately by multiple rewards (+10 each) as part of an automated recovery protocol, the chart receives 5-10 data points within 100ms. Each data point triggers chart.update('none') (no animation) which re-renders the canvas synchronously. The chart.update() call takes 3-5ms per invocation. With 10 updates within 100ms, the chart rendering consumes 30-50ms of main-thread time. If the browser's frame budget is 16ms (60fps), the chart causes 2-3 consecutive dropped frames. During active node recovery (50 nodes simultaneously), the chart component re-renders 50 × 10 = 500 times within 1 second, consuming 1.5-2.5s of main-thread time — freezing the entire dashboard for 2+ seconds.
State Invariants & Parameters
- Data points per event: 1 (per reputation change)
- Batch size: 10 points per node per recovery (5-10 events)
- Concurrent nodes: up to 50 in recovery
chart.update() time: 3-5ms per call
- Frame budget: 16ms (60fps)
- Target: no single freeze > 50ms
- Invariant:
∑(chart_update_time) < 100ms over any 500ms window
Affected Code Paths
src/components/reputation/ReputationChart.tsx:75-120 — updateChartData() calls chart.update() per point
src/components/reputation/chartConfig.ts:40-60 — Chart.js configuration with no batch update
src/hooks/useReputationStream.ts:50-80 — WebSocket events pushing to chart
src/components/reputation/tests/reputationChart.test.ts — No high-throughput test
Resolution Blueprint
- Implement batched chart updates: buffer incoming data points for 100ms using a
useRef<DataPoint[]> buffer. Every 100ms (via setInterval), call chart.data.datasets[0].data.push(...buffer) (single push with spread) and chart.update('none') once. This reduces 50 updates to 10 per second — well within the frame budget.
- Use canvas offscreen rendering (
OffscreenCanvas via Web Worker): the chart data is forwarded to a worker via postMessage(). The worker updates its local Chart.js instance (in a mock-compatible environment) and returns the rendered bitmap via transferControlToOffscreen(). The main thread only blits the final bitmap in < 1ms.
- Implement decimation at high event rates: when events exceed 10/s for a chart, aggregate consecutive points into a single point showing
(timestamp: max, score: avg) at 1-second granularity. Display a visual indicator that the chart is in "high-frequency decimation mode."
- Use
requestAnimationFrame-based rendering: instead of calling chart.update() on every event, set a dirty flag. The rAF loop checks the flag and calls chart.update() once per frame, ensuring at most 60 updates/s regardless of event rate.
- Add a performance benchmark that simulates 500 reputation events across 50 nodes within 1 second and measures the maximum single-frame freeze (target: < 50ms).
Labels
Complexity: Hardcore
Layer: Core-Engine
Type: Race-Condition
Reputation Trend Chart Canvas Freeze During Concurrent Slashing and Reward Events
Problem Statement
The reputation trend chart in
src/components/reputation/ReputationChart.tsxrenders a time-series line chart of a node's reputation score over time using a<canvas>element withChart.js. TheupdateChartData()function at line 80 receives new data points via WebSocket (one per reputation change event). When a node experiences a slashing (-500) followed immediately by multiple rewards (+10 each) as part of an automated recovery protocol, the chart receives 5-10 data points within 100ms. Each data point triggerschart.update('none')(no animation) which re-renders the canvas synchronously. Thechart.update()call takes 3-5ms per invocation. With 10 updates within 100ms, the chart rendering consumes 30-50ms of main-thread time. If the browser's frame budget is 16ms (60fps), the chart causes 2-3 consecutive dropped frames. During active node recovery (50 nodes simultaneously), the chart component re-renders 50 × 10 = 500 times within 1 second, consuming 1.5-2.5s of main-thread time — freezing the entire dashboard for 2+ seconds.State Invariants & Parameters
chart.update()time: 3-5ms per call∑(chart_update_time) < 100msover any 500ms windowAffected Code Paths
src/components/reputation/ReputationChart.tsx:75-120—updateChartData()callschart.update()per pointsrc/components/reputation/chartConfig.ts:40-60— Chart.js configuration with no batch updatesrc/hooks/useReputationStream.ts:50-80— WebSocket events pushing to chartsrc/components/reputation/tests/reputationChart.test.ts— No high-throughput testResolution Blueprint
useRef<DataPoint[]>buffer. Every 100ms (viasetInterval), callchart.data.datasets[0].data.push(...buffer)(single push with spread) andchart.update('none')once. This reduces 50 updates to 10 per second — well within the frame budget.OffscreenCanvasvia Web Worker): the chart data is forwarded to a worker viapostMessage(). The worker updates its local Chart.js instance (in a mock-compatible environment) and returns the rendered bitmap viatransferControlToOffscreen(). The main thread only blits the final bitmap in < 1ms.(timestamp: max, score: avg)at 1-second granularity. Display a visual indicator that the chart is in "high-frequency decimation mode."requestAnimationFrame-based rendering: instead of callingchart.update()on every event, set adirtyflag. TherAFloop checks the flag and callschart.update()once per frame, ensuring at most 60 updates/s regardless of event rate.Labels
Complexity: HardcoreLayer: Core-EngineType: Race-Condition