From 3d654e3ff4243ed61c830931dee8eb924a725718 Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 30 Aug 2026 21:25:28 -0500 Subject: [PATCH] Add terminal frame replay checkpoints Emit a complete monitor frame after one million characters of incremental terminal output so bounded terminal replay buffers retain a self-contained checkpoint. Idle frames remain silent and normal updates stay differential.\n\nValidated with dart analyze and the full Dart test suite. --- .../lib/services/monitor/monitor_screen.dart | 35 +++++++++++++----- .../lib/utils/terminal/frame_patch.dart | 17 +++++++++ MultiplexorApp/test/frame_patch_test.dart | 36 +++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/MultiplexorApp/lib/services/monitor/monitor_screen.dart b/MultiplexorApp/lib/services/monitor/monitor_screen.dart index 291611e..3d2c154 100644 --- a/MultiplexorApp/lib/services/monitor/monitor_screen.dart +++ b/MultiplexorApp/lib/services/monitor/monitor_screen.dart @@ -277,6 +277,7 @@ class MonitorScreen { int _selectedIndex = 0; Duration _range = monitorRanges.first; bool _forceFull = true; + int _incrementalCharactersSinceFullFrame = 0; int _lastColumns = -1; int _lastLines = -1; final MonitorGeometryStabilizer _geometry = MonitorGeometryStabilizer(); @@ -344,6 +345,7 @@ class MonitorScreen { void _resetViewState() { _last = null; _forceFull = true; + _incrementalCharactersSinceFullFrame = 0; _lastColumns = -1; _lastLines = -1; _geometry.reset(); @@ -513,24 +515,39 @@ class MonitorScreen { _pressedId = _liveId(_pressedId); final String text = frame.rows.join('\n'); - final String patch = renderTerminalPatch( + bool fullFrame = _forceFull; + String patch = renderTerminalPatch( previous: _last, next: text, - forceFull: _forceFull, + forceFull: fullFrame, ); - _last = text; - _forceFull = false; - if (patch.isEmpty) { - return; - } // Raw mode turns OPOST off, so a bare '\n' drops a line without // returning the carriage and stair-steps the frame. Only the full-frame // path contains newlines at all — a patch addresses each line by cursor // position — so that is the only one that needs them expanded. - final String normalized = patch.startsWith(_fullFramePrefix) + String normalized = patch.startsWith(_fullFramePrefix) ? patch.replaceAll('\n', '\r\n') : patch; - stdout.write(synchronizeTerminalPatch(normalized)); + String output = synchronizeTerminalPatch(normalized); + if (!fullFrame && + terminalFullFrameCheckpointDue( + charactersSinceFullFrame: _incrementalCharactersSinceFullFrame, + nextPatchCharacters: output.length, + )) { + fullFrame = true; + patch = renderTerminalPatch(previous: _last, next: text, forceFull: true); + normalized = patch.replaceAll('\n', '\r\n'); + output = synchronizeTerminalPatch(normalized); + } + _last = text; + _forceFull = false; + if (output.isEmpty) { + return; + } + stdout.write(output); + _incrementalCharactersSinceFullFrame = fullFrame + ? 0 + : _incrementalCharactersSinceFullFrame + output.length; } /// Composes [modal]'s card over [base], carrying the instance's latest diff --git a/MultiplexorApp/lib/utils/terminal/frame_patch.dart b/MultiplexorApp/lib/utils/terminal/frame_patch.dart index 749e48f..dfce1e3 100644 --- a/MultiplexorApp/lib/utils/terminal/frame_patch.dart +++ b/MultiplexorApp/lib/utils/terminal/frame_patch.dart @@ -1,3 +1,20 @@ +/// Maximum incremental terminal output emitted between complete frames. +/// +/// Periodic complete frames act as replay checkpoints for terminal hosts that +/// detach and later reconstruct their display from a bounded output history. +/// The budget advances only when a visible patch is emitted, so an idle +/// dashboard still writes nothing. +const int terminalFullFrameCheckpointCharacters = 1_000_000; + +/// Whether [nextPatchCharacters] would exhaust the incremental output budget. +bool terminalFullFrameCheckpointDue({ + required int charactersSinceFullFrame, + required int nextPatchCharacters, + int threshold = terminalFullFrameCheckpointCharacters, +}) => + nextPatchCharacters > 0 && + charactersSinceFullFrame + nextPatchCharacters >= threshold; + /// Computes a differential ANSI patch that turns a previously rendered /// terminal frame into [next], writing only the lines that actually /// changed. This keeps full-screen dashboard repaints flicker-free by diff --git a/MultiplexorApp/test/frame_patch_test.dart b/MultiplexorApp/test/frame_patch_test.dart index bb0c97d..e64a931 100644 --- a/MultiplexorApp/test/frame_patch_test.dart +++ b/MultiplexorApp/test/frame_patch_test.dart @@ -2,6 +2,42 @@ import 'package:multiplexor/utils/terminal/frame_patch.dart'; import 'package:test/test.dart'; void main() { + group('terminalFullFrameCheckpointDue', () { + test('uses a one-million-character default budget', () { + expect(terminalFullFrameCheckpointCharacters, 1_000_000); + }); + + test('keeps incremental output below the checkpoint budget', () { + expect( + terminalFullFrameCheckpointDue( + charactersSinceFullFrame: 900_000, + nextPatchCharacters: 99_999, + ), + isFalse, + ); + }); + + test('requests a checkpoint when the next patch reaches the budget', () { + expect( + terminalFullFrameCheckpointDue( + charactersSinceFullFrame: 900_000, + nextPatchCharacters: 100_000, + ), + isTrue, + ); + }); + + test('does not checkpoint an empty idle update', () { + expect( + terminalFullFrameCheckpointDue( + charactersSinceFullFrame: terminalFullFrameCheckpointCharacters, + nextPatchCharacters: 0, + ), + isFalse, + ); + }); + }); + group('renderTerminalPatch', () { test('renders a full clear and the frame content on the first render', () { const String next = 'line one\nline two';