perf(tiles): no snapshot of the session the grid parks when it becomes a tile

Opening the grid runs _cleanupPreviousSession() once to park the main
terminal, and for a non-shell session that serialized the terminal
(1000 lines of scrollback) into the snapshot cache and up to 256 KB of
localStorage. Closing the grid drops the main terminal's snapshot of
every tiled id (stale by then), so when the parked session is itself a
tile, which it is unless the grid opens on a set without it, that copy
was always thrown away. openTileGrid now passes skipSnapshot in exactly
that case; a parked session that stays out of the grid (Open group as
tiles from another session) keeps its snapshot as before.

Measured: the same serialize on the main terminal's buffer costs 32 to
42 ms per grid open (n=6, 35 KB) plus the localStorage write; shells
never took one, so the A/B runs (shells) show no difference. Snapshot
serializes per grid open with a non-shell session active and tiled:
1 -> 0.

Tests: the grid passes skipSnapshot only when the parked session is
tiled; the real _cleanupPreviousSession skips the serialize only when
asked; mutation-checked both ways.

Scope: PR 2 (tile-grid.js, the app.js seam).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 08:57:42 +02:00
parent 7990249e2d
commit ec5e4aa3e1
3 changed files with 38 additions and 2 deletions
+4 -1
View File
@@ -7587,7 +7587,9 @@ class CodemanApp {
} }
} }
_cleanupPreviousSession(newSessionId) { // `skipSnapshot`: the tile grid opening on the session it parks
// (tile-grid.js openTileGrid), whose snapshot closing the grid discards.
_cleanupPreviousSession(newSessionId, { skipSnapshot = false } = {}) {
// Snapshot the OUTGOING session's xterm rendered state (viewport + scrollback + // Snapshot the OUTGOING session's xterm rendered state (viewport + scrollback +
// colors/attrs) before the terminal gets cleared/reset. Lets us restore the // colors/attrs) before the terminal gets cleared/reset. Lets us restore the
// exact view on switch-back rather than replaying codex's byte stream, which // exact view on switch-back rather than replaying codex's byte stream, which
@@ -7598,6 +7600,7 @@ class CodemanApp {
// quota for them. Unknown/undefined mode still snapshots, matching restore. // quota for them. Unknown/undefined mode still snapshots, matching restore.
const outgoingSession = this.activeSessionId ? this.sessions?.get?.(this.activeSessionId) : null; const outgoingSession = this.activeSessionId ? this.sessions?.get?.(this.activeSessionId) : null;
if ( if (
!skipSnapshot &&
this.activeSessionId && this.activeSessionId &&
outgoingSession?.mode !== 'shell' && outgoingSession?.mode !== 'shell' &&
this._serializeAddon && this._serializeAddon &&
+5 -1
View File
@@ -202,7 +202,11 @@ Object.assign(CodemanApp.prototype, {
return true; return true;
} }
this._cleanupPreviousSession(focus); // No snapshot of a session that becomes a tile: closing the grid drops the
// main terminal's snapshot of every tiled id (stale by then), so taking one
// here (a 1000-line serialize and up to 256 KB of localStorage for a
// non-shell session) only ever produced a copy that was thrown away.
this._cleanupPreviousSession(focus, { skipSnapshot: wanted.includes(this.activeSessionId) });
grid.open = true; grid.open = true;
grid.ids = []; grid.ids = [];
grid.focusedId = focus; grid.focusedId = focus;
+29
View File
@@ -82,6 +82,35 @@ beforeEach(() => {
}); });
describe('parking the main terminal', () => { describe('parking the main terminal', () => {
it('opening takes no snapshot of a session that becomes a tile, and keeps it for one that does not', () => {
// Closing the grid drops the snapshot of every tiled id, so one taken of a
// tiled session at opening was always thrown away.
const tiled = makeApp();
tiled.openTileGrid(IDS);
expect(tiled._cleanupPreviousSession).toHaveBeenCalledWith('s-a', { skipSnapshot: true });
resetGridHarness();
const elsewhere = makeApp(); // s-a is active, the grid opens on the others
elsewhere.openTileGrid(['s-b', 's-c']);
expect(elsewhere._cleanupPreviousSession).toHaveBeenCalledWith('s-b', { skipSnapshot: false });
});
it('_cleanupPreviousSession skips the snapshot only when asked', () => {
const app = makeApp();
delete app._cleanupPreviousSession; // the real one
app._disconnectWs = vi.fn();
const serialize = vi.fn(() => 'snapshot of s-a\r\n'.repeat(4));
app._serializeAddon = { serialize };
app._isUsableXtermSnapshot = () => true;
app._persistXtermSnapshot = vi.fn();
app._cleanupPreviousSession('s-b', { skipSnapshot: true });
expect(serialize).not.toHaveBeenCalled();
expect(app._xtermSnapshots.has('s-a')).toBe(false);
app._cleanupPreviousSession('s-b');
expect(serialize).toHaveBeenCalledTimes(1);
expect(app._xtermSnapshots.has('s-a')).toBe(true);
});
it('opening parks it ONCE and puts a tile per session in its place, focused tile active', () => { it('opening parks it ONCE and puts a tile per session in its place, focused tile active', () => {
const app = makeApp(); const app = makeApp();
expect(app.openTileGrid(IDS, { focusedId: 's-b' })).toBe(true); expect(app.openTileGrid(IDS, { focusedId: 's-b' })).toBe(true);