()
| 183 | let intervalId: number | null = null; |
| 184 | |
| 185 | const streamFrame = (): void => { |
| 186 | if (!streaming || totalPoints >= MAX_POINTS) { |
| 187 | if (intervalId !== null) { |
| 188 | clearInterval(intervalId); |
| 189 | intervalId = null; |
| 190 | } |
| 191 | setText('fps', 'Complete'); |
| 192 | return; |
| 193 | } |
| 194 | |
| 195 | const now = performance.now(); |
| 196 | const delta = now - lastFrameTime; |
| 197 | |
| 198 | // Throttle to target FPS |
| 199 | if (delta < FRAME_INTERVAL_MS) { |
| 200 | return; |
| 201 | } |
| 202 | |
| 203 | lastFrameTime = now; |
| 204 | |
| 205 | // Generate batch |
| 206 | const batch = generateBatch(nextX, POINTS_PER_FRAME); |
| 207 | nextX += POINTS_PER_FRAME * 0.01; |
| 208 | |
| 209 | // Pack to Float32Array (Story 8h: zero-copy transfer) |
| 210 | const packed = packDataPoints(batch); |
| 211 | const byteSize = packed.byteLength; |
| 212 | |
| 213 | // Append with zero-copy transfer |
| 214 | // The 'xy' format specifies interleaved [x0,y0,x1,y1,...] layout |
| 215 | // |
| 216 | // IMPORTANT: After this call, packed.buffer is transferred to the worker and becomes |
| 217 | // detached (packed.length === 0). Do NOT reuse packed after this point. |
| 218 | // Each call to packDataPoints() creates a new ArrayBuffer that can be transferred once. |
| 219 | chart.appendData(0, packed, 'xy'); |
| 220 | |
| 221 | // Update metrics |
| 222 | totalPoints += POINTS_PER_FRAME; |
| 223 | fpsTracker.update(); |
| 224 | dataRateTracker.update(byteSize); |
| 225 | |
| 226 | // Update UI |
| 227 | setText('fps', fpsTracker.getFPS().toFixed(0)); |
| 228 | setText('points', formatInt(totalPoints)); |
| 229 | setText('dataRate', formatDataRate(dataRateTracker.getBytesPerSecond())); |
| 230 | }; |
| 231 | |
| 232 | // Start streaming |
| 233 | intervalId = window.setInterval(streamFrame, 0); |
nothing calls this directly
no test coverage detected