From a2b1d18ec2bd40949eec916e60ec0cc29cf41ada Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Wed, 26 Aug 2026 12:08:44 -0600 Subject: [PATCH] test(web): compare work log update cost against a from-scratch derive The 20,000-activity benchmark asserted a 100 ms wall-clock bound. On the 4-vCPU GitHub-hosted runners that replaced Blacksmith it measured 100.5 ms and 107.5 ms on two of the first three runs, for work that takes about 4 ms on a quiet laptop: the bound was measuring runner contention, not the code. Assert the property the optimization actually provides instead: an update over cached rows must be far cheaper than deriving the same rows from scratch, measured back to back in the same process and taking the best of three rounds so a single GC pause or scheduling stall cannot fail it. Locally the ratio is 0.13-0.27; without the per-activity cache both calls walk the same path and the ratio is about 1, so the bound of 0.5 has margin on both sides. --- apps/web/src/session-logic.test.ts | 93 ++++++++++++++++++------------ 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index 2c1bfcfa5..7ad2a3052 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -2353,43 +2353,64 @@ describe("session activity performance", () => { expect(appendedEntries[1]).toBe(initialEntries[1]); }); - it("updates 20,000 ordered tool activities within 100 ms", () => { - const activities = Array.from({ length: 20_000 }, (_, index) => - makeActivity({ - id: `benchmark-tool-${index}`, - createdAt: new Date(1_700_000_000_000 + index).toISOString(), - kind: "tool.completed", - summary: "Ran command", - sequence: index, - payload: { - itemType: "command_execution", - title: "Ran command", - data: { - toolCallId: `benchmark-tool-${index}`, - item: { command: ["git", "status"] }, + it("updates 20,000 ordered tool activities far faster than deriving them from scratch", () => { + const makeActivities = (count: number, prefix: string) => + Array.from({ length: count }, (_, index) => + makeActivity({ + id: `${prefix}-${index}`, + createdAt: new Date(1_700_000_000_000 + index).toISOString(), + kind: "tool.completed", + summary: "Ran command", + sequence: index, + payload: { + itemType: "command_execution", + title: "Ran command", + data: { toolCallId: `${prefix}-${index}`, item: { command: ["git", "status"] } }, }, - }, - }), - ); - deriveWorkLogEntries(activities); - const updatedActivities = [ - ...activities, - makeActivity({ - id: "benchmark-tool-appended", - createdAt: new Date(1_700_000_000_000 + activities.length).toISOString(), - kind: "tool.completed", - summary: "Ran command", - sequence: activities.length, - payload: { - itemType: "command_execution", - title: "Ran command", - data: { toolCallId: "benchmark-tool-appended", item: { command: ["git", "diff"] } }, - }, - }), - ]; + }), + ); + + // Wall-clock on a shared CI runner swings several-fold between runs (the + // old 100 ms bound measured 100-108 ms on a 4-vCPU hosted runner for work + // that takes 4 ms on a quiet laptop), so compare the update against a + // from-scratch derivation measured in the same process instead. Best of + // three rounds shrugs off a GC pause or scheduling stall in any one + // sample. With the per-activity cache an update is roughly 6-8x cheaper; + // without it both calls walk the same code path and the ratio is ~1. + let fromScratchMs = Number.POSITIVE_INFINITY; + let updateMs = Number.POSITIVE_INFINITY; + for (let round = 0; round < 3; round++) { + const activities = makeActivities(20_000, `benchmark-tool-${round}`); + deriveWorkLogEntries(activities); + const updatedActivities = [ + ...activities, + makeActivity({ + id: `benchmark-tool-appended-${round}`, + createdAt: new Date(1_700_000_000_000 + activities.length).toISOString(), + kind: "tool.completed", + summary: "Ran command", + sequence: activities.length, + payload: { + itemType: "command_execution", + title: "Ran command", + data: { + toolCallId: `benchmark-tool-appended-${round}`, + item: { command: ["git", "diff"] }, + }, + }, + }), + ]; + + const updateStartedAt = performance.now(); + expect(deriveWorkLogEntries(updatedActivities)).toHaveLength(20_001); + updateMs = Math.min(updateMs, performance.now() - updateStartedAt); + + const fresh = makeActivities(20_001, `benchmark-fresh-${round}`); + const fromScratchStartedAt = performance.now(); + expect(deriveWorkLogEntries(fresh)).toHaveLength(20_001); + fromScratchMs = Math.min(fromScratchMs, performance.now() - fromScratchStartedAt); + } - const startedAt = performance.now(); - expect(deriveWorkLogEntries(updatedActivities)).toHaveLength(20_001); - expect(performance.now() - startedAt).toBeLessThan(100); + expect(updateMs).toBeLessThan(fromScratchMs / 2); }); });