diff --git a/web/scripts/latency-measure.js b/web/scripts/latency-measure.js new file mode 100755 index 00000000..0d9fede5 --- /dev/null +++ b/web/scripts/latency-measure.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +/** + * Precise latency measurement for terminal rendering + * + * This script outputs timestamped markers to measure actual end-to-end latency + */ + +const MARKER = '█'; +const INTERVAL = 100; // 100ms intervals for easier measurement + +console.log('Latency measurement test - watch for delay between timestamp and display\n'); + +let count = 0; +const startTime = Date.now(); + +const interval = setInterval(() => { + count++; + const now = Date.now(); + const elapsed = now - startTime; + const timestamp = new Date(now).toISOString().split('T')[1].slice(0, -1); + + // Output with high-precision timestamp + console.log(`[${count.toString().padStart(3, '0')}] ${timestamp} ${MARKER.repeat(10)} MARKER`); + + // Flush stdout to ensure immediate output + if (process.stdout.isTTY) { + process.stdout.write(''); + } + + if (count >= 50) { // 5 seconds of data + clearInterval(interval); + console.log('\nTest complete. Compare the timestamps with when you see them appear.'); + console.log('The difference is your end-to-end latency.'); + process.exit(0); + } +}, INTERVAL); + +// Also test immediate response to keypress +if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + process.stdin.on('data', (data) => { + const key = data.toString(); + const now = Date.now(); + const timestamp = new Date(now).toISOString().split('T')[1].slice(0, -1); + + if (key === '\x03') { // Ctrl+C + clearInterval(interval); + process.exit(0); + } + + console.log(`[KEY] ${timestamp} You pressed: ${key.charCodeAt(0)} ${MARKER.repeat(20)}`); + }); +} + +console.log('Press any key to test input latency (Ctrl+C to exit)\n'); \ No newline at end of file diff --git a/web/src/server/services/stream-watcher.ts b/web/src/server/services/stream-watcher.ts index 055d0866..fa049785 100644 --- a/web/src/server/services/stream-watcher.ts +++ b/web/src/server/services/stream-watcher.ts @@ -13,8 +13,6 @@ interface WatcherInfo { lastOffset: number; lineBuffer: string; notificationListener?: (update: { sessionId: string; data: string; timestamp: number }) => void; - lastBroadcastTime: number; - recentBroadcasts: Set; } export class StreamWatcher { @@ -42,8 +40,6 @@ export class StreamWatcher { clients: new Set(), lastOffset: 0, lineBuffer: '', - lastBroadcastTime: 0, - recentBroadcasts: new Set(), }; this.activeWatchers.set(sessionId, watcherInfo); @@ -211,8 +207,11 @@ export class StreamWatcher { */ private startWatching(sessionId: string, streamPath: string, watcherInfo: WatcherInfo): void { // First, set up direct notification listener for lowest latency + let hasDirectNotifications = false; + watcherInfo.notificationListener = (update) => { if (update.sessionId === sessionId) { + hasDirectNotifications = true; // Process the notification data directly const lines = update.data.split('\n').filter((line) => line.trim()); for (const line of lines) { @@ -222,74 +221,63 @@ export class StreamWatcher { }; streamNotifier.on('stream-update', watcherInfo.notificationListener); - // Also use optimized file watcher as fallback (for cross-process scenarios) - watcherInfo.watcher = new OptimizedFileWatcher(streamPath, { persistent: true }); + // Only use file watcher if we're not getting direct notifications + // Give it a moment to see if we get direct notifications + setTimeout(() => { + if (!hasDirectNotifications) { + console.log( + `[STREAM] No direct notifications for session ${sessionId}, using file watcher` + ); + // Use optimized file watcher as fallback (for cross-process scenarios) + watcherInfo.watcher = new OptimizedFileWatcher(streamPath, { persistent: true }); - watcherInfo.watcher.on('change', (stats) => { - try { - if (stats.size > watcherInfo.lastOffset) { - // Read only new data - const fd = fs.openSync(streamPath, 'r'); - const buffer = Buffer.alloc(stats.size - watcherInfo.lastOffset); - fs.readSync(fd, buffer, 0, buffer.length, watcherInfo.lastOffset); - fs.closeSync(fd); + watcherInfo.watcher.on('change', (stats) => { + try { + if (stats.size > watcherInfo.lastOffset) { + // Read only new data + const fd = fs.openSync(streamPath, 'r'); + const buffer = Buffer.alloc(stats.size - watcherInfo.lastOffset); + fs.readSync(fd, buffer, 0, buffer.length, watcherInfo.lastOffset); + fs.closeSync(fd); - // Update offset - watcherInfo.lastOffset = stats.size; + // Update offset + watcherInfo.lastOffset = stats.size; - // Process new data - const newData = buffer.toString('utf8'); - watcherInfo.lineBuffer += newData; + // Process new data + const newData = buffer.toString('utf8'); + watcherInfo.lineBuffer += newData; - // Process complete lines - const lines = watcherInfo.lineBuffer.split('\n'); - watcherInfo.lineBuffer = lines.pop() || ''; + // Process complete lines + const lines = watcherInfo.lineBuffer.split('\n'); + watcherInfo.lineBuffer = lines.pop() || ''; - console.log(`[STREAM] New data from file watcher: ${newData}`); - for (const line of lines) { - if (line.trim()) { - this.broadcastLine(sessionId, line, watcherInfo); + for (const line of lines) { + if (line.trim()) { + this.broadcastLine(sessionId, line, watcherInfo); + } + } } + } catch (error) { + console.error(`[STREAM] Error reading file changes:`, error); } - } - } catch (error) { - console.error(`[STREAM] Error reading file changes:`, error); + }); + + watcherInfo.watcher.on('error', (error) => { + console.error(`[STREAM] File watcher error for session ${sessionId}:`, error); + }); + + // Start the watcher + watcherInfo.watcher.start(); } - }); + }, 100); // Wait 100ms to see if we get direct notifications - watcherInfo.watcher.on('error', (error) => { - console.error(`[STREAM] File watcher error for session ${sessionId}:`, error); - }); - - // Start the watcher - watcherInfo.watcher.start(); - - console.log( - `[STREAM] Started optimized watching with direct notifications for session ${sessionId}` - ); + console.log(`[STREAM] Started watching for session ${sessionId}`); } /** * Broadcast a line to all clients */ private broadcastLine(sessionId: string, line: string, watcherInfo: WatcherInfo): void { - // Deduplication: check if we've broadcast this line very recently - const now = Date.now(); - const lineHash = `${line.substring(0, 100)}_${line.length}`; // Simple hash - - if (watcherInfo.recentBroadcasts.has(lineHash) && now - watcherInfo.lastBroadcastTime < 50) { - // Skip duplicate within 50ms window - return; - } - - // Clean up old broadcasts - if (now - watcherInfo.lastBroadcastTime > 100) { - watcherInfo.recentBroadcasts.clear(); - } - - watcherInfo.recentBroadcasts.add(lineHash); - watcherInfo.lastBroadcastTime = now; - let eventData: string | null = null; try {