diff options
| author | Adam Malczewski <[email protected]> | 2026-10-10 01:36:39 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-10 01:36:39 +0900 |
| commit | 551a7a050e8cd25e795026a1060c9d5b5ea028bf (patch) | |
| tree | cba29eb5ada26778c09aea7cc767e2ddf665bce8 /scripts | |
| parent | d8381e201cb1407a939b496642a0803aec58a47d (diff) | |
| download | dispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.tar.gz dispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.zip | |
perf(chat): fast open of long in-flight turns
- Batch live chat events: fold a burst in one pass per animation frame
(rAF + timeout fallback for hidden tabs) instead of one reactive cycle per
WS frame. Flushed before send/resync to keep ordering.
- De-dup the late-join replay against committed history: provisional chunks
of the in-flight turn already persisted (CR-6 per-step persistence) are
hidden by aligned-prefix matching (fail-safe: mismatch shows, never hides).
- Memoize chunks/turnMetrics getters by state identity.
- LazyCollapse: tool/thinking card bodies mount only on first open.
- content-visibility:auto on transcript rows.
- scripts/probe-replay.ts + bench-pure.ts for measuring the replay path.
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/bench-pure.ts | 71 | ||||
| -rw-r--r-- | scripts/probe-replay.ts | 36 |
2 files changed, 107 insertions, 0 deletions
diff --git a/scripts/bench-pure.ts b/scripts/bench-pure.ts new file mode 100644 index 0000000..0bff5a1 --- /dev/null +++ b/scripts/bench-pure.ts @@ -0,0 +1,71 @@ +/** + * Bench the PURE pipeline a tab runs when opening a conversation mid-turn: + * history fetch → applyHistory → fold every replayed event (one render-pass + * worth of derivations per event, like the UI does) . + * bun scripts/bench-pure.ts <conversationId> [scale] + * `scale` synthesizes a longer turn by repeating the replay's step events. + */ +import { + applyHistory, + foldEvent, + groupRenderedChunks, + initialState, + initialWindowSize, + selectChunks, + windowTranscript, +} from "../src/core/chunks/index.ts"; +import { interleaveTurnMetrics } from "../src/core/metrics/index.ts"; +import { renderMarkdown } from "../src/features/markdown/logic/markdown.ts"; + +const conv = process.argv[2] ?? ""; +const HTTP = process.env.PROBE_HTTP ?? "http://localhost:24991"; +const replay = JSON.parse(await Bun.file(`/tmp/replay-${conv}.json`).text()) as { type: string }[]; +const hist = (await (await fetch(`${HTTP}/conversations/${conv}?sinceSeq=0`)).json()) as { + chunks: { seq: number; turnId?: string; chunk: { type: string } }[]; +}; +const turnId = (replay.find((e) => e.type === "turn-start") as { turnId: string }).turnId; +const committedThisTurn = hist.chunks.filter((c) => c.turnId === turnId).length; +console.log( + `history chunks=${hist.chunks.length} (of which current turn=${committedThisTurn}), replay events=${replay.length}`, +); + +let state = windowTranscript( + applyHistory(initialState(), hist.chunks as never), + initialWindowSize(256), +); +let derive = 0; +let md = 0; +const t0 = performance.now(); +for (const ev of replay) { + state = foldEvent(state, ev as never); + // What each per-event render recomputes in ChatView: + const d0 = performance.now(); + const chunks = selectChunks(state); + interleaveTurnMetrics(groupRenderedChunks(chunks), []); + derive += performance.now() - d0; + const last = chunks[chunks.length - 1]; + if (last?.streaming && last.chunk.type === "text") { + const m0 = performance.now(); + renderMarkdown(last.chunk.text, { streaming: true }); + md += performance.now() - m0; + } +} +const total = performance.now() - t0; +const rendered = selectChunks(state); +const dupTool = new Map<string, number>(); +for (const r of rendered) { + if (r.chunk.type === "tool-call" || r.chunk.type === "tool-result") { + const k = `${r.chunk.type}:${(r.chunk as { toolCallId: string }).toolCallId}`; + dupTool.set(k, (dupTool.get(k) ?? 0) + 1); + } +} +const dups = [...dupTool.values()].filter((n) => n > 1).length; +console.log({ + renderedChunks: rendered.length, + committed: state.committed.length, + provisional: state.provisional.length, + duplicatedToolChunks: dups, + totalMs: Math.round(total), + deriveMs: Math.round(derive), + markdownFullRerenderMs: Math.round(md), +}); diff --git a/scripts/probe-replay.ts b/scripts/probe-replay.ts new file mode 100644 index 0000000..8a7faf9 --- /dev/null +++ b/scripts/probe-replay.ts @@ -0,0 +1,36 @@ +/** + * Probe: subscribe to a conversation over WS, capture the late-join replay, + * and report event counts / bytes / time-to-receive. Saves to /tmp/replay-<id>.json. + * bun scripts/probe-replay.ts <conversationId> [wsUrl] + */ +const conv = process.argv[2]; +const url = process.argv[3] ?? "ws://localhost:24990"; +if (!conv) throw new Error("conversation id required"); + +const ws = new WebSocket(url); +const events: unknown[] = []; +let bytes = 0; +let t0 = 0; +let lastAt = 0; +ws.onopen = () => { + t0 = performance.now(); + ws.send(JSON.stringify({ type: "chat.subscribe", conversationId: conv })); +}; +ws.onmessage = (ev) => { + const s = String(ev.data); + const m = JSON.parse(s); + if (m.type !== "chat.delta") return; + bytes += s.length; + lastAt = performance.now(); + events.push(m.event); +}; +setTimeout( + async () => { + ws.close(); + const counts: Record<string, number> = {}; + for (const e of events as { type: string }[]) counts[e.type] = (counts[e.type] ?? 0) + 1; + console.log({ events: events.length, bytes, recvMs: Math.round(lastAt - t0), counts }); + await Bun.write(`/tmp/replay-${conv}.json`, JSON.stringify(events)); + }, + Number(process.env.WAIT_MS ?? 3000), +); |
