import { TraceData } from './types'; export type PlaybackMode = 'live' | 'paused' | 'slow' | 'fast' | 'catchup'; export interface BufferState { mode: PlaybackMode; speed: number; // Multiplier: 0.25, 0.5, 1, 2, 4 bufferSize: number; currentSize: number; writePosition: number; readPosition: number; droppedCount: number; isPaused: boolean; } export interface TraceBufferConfig { maxSize?: number; updateInterval?: number; batchSize?: number; } type BufferListener = (traces: TraceData[], state: BufferState) => void; export class TraceBuffer { private buffer: (TraceData | null)[]; private maxSize: number; private writePos: number = 0; private readPos: number = 0; private currentSize: number = 0; private droppedCount: number = 0; private mode: PlaybackMode = 'live'; private speed: number = 1; private isPaused: boolean = false; private listeners: Set = new Set(); private updateTimer: NodeJS.Timeout | null = null; private updateInterval: number; private batchSize: number; private pendingTraces: TraceData[] = []; private lastUpdateTime: number = 0; private targetFPS: number = 60; constructor(config: TraceBufferConfig = {}) { this.maxSize = config.maxSize || 10000; this.updateInterval = config.updateInterval || 16; // ~60fps this.batchSize = config.batchSize || 10; this.buffer = new Array(this.maxSize).fill(null); this.startUpdateLoop(); } private startUpdateLoop(): void { const update = () => { const now = Date.now(); const deltaTime = now - this.lastUpdateTime; if (!this.isPaused && this.pendingTraces.length > 0) { const effectiveInterval = this.updateInterval / this.speed; if (deltaTime >= effectiveInterval) { this.processPendingTraces(); this.lastUpdateTime = now; } } this.updateTimer = setTimeout(update, Math.max(1, this.updateInterval)); }; update(); } private processPendingTraces(): void { if (this.pendingTraces.length === 0) return; let tracesToProcess: TraceData[]; switch (this.mode) { case 'live': // Process in real-time batches tracesToProcess = this.pendingTraces.splice(0, this.batchSize); break; case 'slow': // Process one at a time for slow motion tracesToProcess = this.pendingTraces.splice(0, 1); break; case 'fast': // Process larger batches for fast forward tracesToProcess = this.pendingTraces.splice(0, this.batchSize * 4); break; case 'catchup': // Process all pending to catch up tracesToProcess = this.pendingTraces.splice(0, this.pendingTraces.length); this.mode = 'live'; // Return to live after catching up break; default: tracesToProcess = []; } if (tracesToProcess.length > 0) { this.notifyListeners(tracesToProcess); } } public push(trace: TraceData): void { // Add to ring buffer if (this.currentSize >= this.maxSize) { // Buffer is full, overwrite oldest this.droppedCount++; } else { this.currentSize++; } this.buffer[this.writePos] = trace; this.writePos = (this.writePos + 1) % this.maxSize; // Add to pending traces for processing if (!this.isPaused) { this.pendingTraces.push(trace); } } public setMode(mode: PlaybackMode): void { this.mode = mode; switch (mode) { case 'paused': this.isPaused = true; break; case 'catchup': this.isPaused = false; // Will process all pending traces break; default: this.isPaused = false; } } public setSpeed(speed: number): void { this.speed = Math.max(0.25, Math.min(4, speed)); } public pause(): void { this.isPaused = true; this.mode = 'paused'; } public resume(): void { this.isPaused = false; this.mode = 'live'; } public clear(): void { this.buffer.fill(null); this.writePos = 0; this.readPos = 0; this.currentSize = 0; this.droppedCount = 0; this.pendingTraces = []; this.notifyListeners([]); } public getState(): BufferState { return { mode: this.mode, speed: this.speed, bufferSize: this.maxSize, currentSize: this.currentSize, writePosition: this.writePos, readPosition: this.readPos, droppedCount: this.droppedCount, isPaused: this.isPaused, }; } public getRecentTraces(count: number = 100): TraceData[] { const traces: TraceData[] = []; let pos = (this.writePos - 1 + this.maxSize) % this.maxSize; let collected = 0; while (collected < count && collected < this.currentSize) { const trace = this.buffer[pos]; if (trace) { traces.unshift(trace); collected++; } pos = (pos - 1 + this.maxSize) % this.maxSize; } return traces; } public subscribe(listener: BufferListener): () => void { this.listeners.add(listener); // Send current state immediately listener(this.getRecentTraces(), this.getState()); // Return unsubscribe function return () => { this.listeners.delete(listener); }; } private notifyListeners(newTraces: TraceData[]): void { const state = this.getState(); this.listeners.forEach(listener => { try { listener(newTraces, state); } catch (error) { console.error('Error in buffer listener:', error); } }); } public destroy(): void { if (this.updateTimer) { clearTimeout(this.updateTimer); this.updateTimer = null; } this.listeners.clear(); this.buffer = []; this.pendingTraces = []; } } // Singleton instance let bufferInstance: TraceBuffer | null = null; export function getTraceBuffer(config?: TraceBufferConfig): TraceBuffer { if (!bufferInstance) { bufferInstance = new TraceBuffer(config); } return bufferInstance; } export function destroyTraceBuffer(): void { if (bufferInstance) { bufferInstance.destroy(); bufferInstance = null; } }