Files
obelisk/tests/session-live-reload.test.mjs
T
tommy0103 b58c34d3af perf(app): stabilize live session timeline updates
Virtualize SessionDetail rows behind stable presentation boundaries and apply typed incremental patches only after visible commits. Preserve reader state across live updates, handle coalesced and reordered patches, and verify the 120Hz append path with Electron tracing.
2026-07-14 18:11:51 +08:00

190 lines
6.3 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createSessionLiveReloadCoordinator } from '../app/src/renderer/src/session-live-reload.mjs';
import { state } from '../app/src/renderer/src/store.js';
import {
getCachedSessionDetail,
loadSessionDetail,
loadSessionDetailPatch,
} from '../app/src/renderer/src/data.js';
import { assembleSessionMessages } from '../app/src/shared/session-detail-assembly.mjs';
import { createSessionPatch } from '../app/src/shared/session-patch.mjs';
test('live snapshots coalesce while scrolling and commit once after scroll end', async () => {
let scrolling = true;
let loads = 0;
const commits = [];
const coordinator = createSessionLiveReloadCoordinator({
isScrolling: () => scrolling,
load: async () => ++loads,
commit: async snapshot => { commits.push(snapshot); },
});
await coordinator.request();
await coordinator.request();
await coordinator.request();
assert.equal(loads, 0);
assert.deepEqual(commits, []);
scrolling = false;
await coordinator.flush();
assert.equal(loads, 1);
assert.deepEqual(commits, [1]);
await coordinator.flush();
assert.equal(loads, 1, 'an idle flush without another update is a no-op');
});
test('an update arriving during an in-flight load skips the stale snapshot without overlap', async () => {
let releaseFirstLoad;
let activeLoads = 0;
let maxActiveLoads = 0;
let loads = 0;
const commits = [];
const firstLoadGate = new Promise(resolve => { releaseFirstLoad = resolve; });
const coordinator = createSessionLiveReloadCoordinator({
isScrolling: () => false,
load: async () => {
loads++;
activeLoads++;
maxActiveLoads = Math.max(maxActiveLoads, activeLoads);
if (loads === 1) await firstLoadGate;
activeLoads--;
return loads;
},
commit: async snapshot => { commits.push(snapshot); },
});
const first = coordinator.request();
const second = coordinator.request();
releaseFirstLoad();
await Promise.all([first, second]);
assert.equal(loads, 2);
assert.equal(maxActiveLoads, 1, 'snapshot loads remain serialized');
assert.deepEqual(commits, [2], 'only the freshest loaded snapshot is committed');
});
test('scrolling that starts during IPC defers the loaded snapshot commit', async () => {
let scrolling = false;
let releaseLoad;
let loads = 0;
const commits = [];
const loadGate = new Promise(resolve => { releaseLoad = resolve; });
const coordinator = createSessionLiveReloadCoordinator({
isScrolling: () => scrolling,
load: async () => {
loads++;
await loadGate;
return 'loaded-before-scroll-ended';
},
commit: async snapshot => { commits.push(snapshot); },
});
const request = coordinator.request();
scrolling = true;
releaseLoad();
await request;
assert.equal(loads, 1);
assert.deepEqual(commits, []);
scrolling = false;
await coordinator.flush();
assert.equal(loads, 1, 'the already-loaded snapshot is reused');
assert.deepEqual(commits, ['loaded-before-scroll-ended']);
});
test('a skipped live patch does not advance the visible patch baseline', async t => {
const sessionId = 'coalesced-patch-session';
const previousSessions = state.sessions;
t.after(() => {
state.sessions = previousSessions;
delete globalThis.window;
});
let rows = [
{ uuid: 'message-1', type: 'user', timestamp: '2026-07-14T00:00:01Z', text: 'one' },
];
let patchCalls = 0;
let releaseFirstPatch;
let firstPatchStarted;
const firstPatchGate = new Promise(resolve => { releaseFirstPatch = resolve; });
const firstPatchReady = new Promise(resolve => { firstPatchStarted = resolve; });
globalThis.window = {
obelisk: {
getSessionMessages: async () => rows,
getSessionToolCalls: async () => [],
getSessionToolResults: async () => [],
getSessionSubagents: async () => [],
getSessionWorkflows: async () => [],
getSessionSummaries: async () => [],
getSessionPatch: async (_id, cursor) => {
const snapshotAtCall = { messages: assembleSessionMessages({
messages: rows,
toolCalls: [],
toolResults: [],
subagents: [],
workflows: [],
}), workflows: [] };
patchCalls++;
if (patchCalls === 1) {
firstPatchStarted();
await firstPatchGate;
}
return createSessionPatch(snapshotAtCall, cursor);
},
},
};
state.sessions = [{ id: sessionId, messages: [] }];
await loadSessionDetail(sessionId);
const commits = [];
const coordinator = createSessionLiveReloadCoordinator({
isScrolling: () => false,
load: () => loadSessionDetailPatch(sessionId),
commit: async latest => {
commits.push({
messages: latest.messages.map(message => message.uuid),
changedIds: latest.messagePatch.changedIds,
});
latest.acceptMessagePatch?.();
},
});
rows = [...rows, { uuid: 'message-2', type: 'assistant', timestamp: '2026-07-14T00:00:02Z', text: 'two' }];
const first = coordinator.request();
await firstPatchReady;
rows = [...rows, { uuid: 'message-3', type: 'assistant', timestamp: '2026-07-14T00:00:03Z', text: 'three' }];
const second = coordinator.request();
releaseFirstPatch();
await Promise.all([first, second]);
assert.deepEqual(commits, [{
messages: ['message-1', 'message-2', 'message-3'],
changedIds: ['message-2', 'message-3'],
}]);
assert.deepEqual(
getCachedSessionDetail(sessionId).messages.map(message => message.uuid),
['message-1', 'message-2', 'message-3'],
'accepted patches become the reusable session-detail snapshot',
);
assert.deepEqual(
state.sessions.find(session => session.id === sessionId).messages,
[],
'the stale full-snapshot copy is invalidated after patch acceptance',
);
const evictionSessionIds = ['eviction-session-1', 'eviction-session-2', 'eviction-session-3'];
state.sessions.push(...evictionSessionIds.map(id => ({ id, messages: [] })));
for (const id of evictionSessionIds) await loadSessionDetail(id);
assert.equal(getCachedSessionDetail(sessionId), null, 'the oldest accepted snapshot is evicted by the bounded cache');
assert.deepEqual(
state.sessions.find(session => session.id === sessionId).messages,
[],
'an evicted session cannot fall back to stale initial messages and must reload',
);
});