From fec606f9ae524277ef1f6886e614f32d9e15e36e Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 4 Sep 2026 11:49:11 -0700 Subject: [PATCH] perf(web): avoid repeated terminal metadata scans (#9747) Build one ordered terminal metadata index per immutable snapshot. Reuse unchanged session wrappers and thread groups across consumers, with separate environment targets. Keep numeric ordering, picker ties, attach state, and subscription lifetimes unchanged. Let retired snapshots and groups be collected. Created with GPT-6 Astra (preview) in Codex. --- apps/web/src/state/terminalSessions.test.ts | 171 ++++++++++++++++++++ apps/web/src/state/terminalSessions.ts | 148 +++++++++++++---- 2 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 apps/web/src/state/terminalSessions.test.ts diff --git a/apps/web/src/state/terminalSessions.test.ts b/apps/web/src/state/terminalSessions.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee1c25f4439417087177cfc3927f6957858c69f4 --- /dev/null +++ b/apps/web/src/state/terminalSessions.test.ts @@ -0,0 +1,171 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { EnvironmentId, ThreadId, type TerminalSummary } from "@t3tools/contracts"; +import { selectRunningSubprocessTerminalIds } from "@t3tools/client-runtime/state/terminal"; + +import { selectKnownTerminalSessions } from "./terminalSessions"; + +vi.mock("./terminal", () => ({ terminalEnvironment: {} })); + +const environmentA = EnvironmentId.make("environment-a"); +const environmentB = EnvironmentId.make("environment-b"); +const threadA = ThreadId.make("thread-a"); +const threadB = ThreadId.make("thread-b"); + +function summary( + threadId: string, + terminalId: string, + changes: Partial = {}, +): TerminalSummary { + return { + threadId, + terminalId, + cwd: "/repo", + worktreePath: null, + status: "running", + pid: 123, + exitCode: null, + exitSignal: null, + hasRunningSubprocess: true, + label: "Terminal", + updatedAt: "2026-09-04T00:00:00.000Z", + ...changes, + }; +} + +describe("selectKnownTerminalSessions", () => { + it("preserves numeric terminal order and stable picker ties across threads", () => { + const metadata = [ + summary(threadB, "terminal-2"), + summary(threadA, "terminal-10"), + summary(threadB, "terminal-1"), + summary(threadA, "terminal-1"), + summary(threadA, "terminal-01"), + ]; + const all = selectKnownTerminalSessions(metadata, environmentA, null); + const selected = selectKnownTerminalSessions(metadata, environmentA, threadB); + expect(all.map((session) => `${session.target.threadId}/${session.target.terminalId}`)).toEqual( + [ + "thread-b/terminal-1", + "thread-a/terminal-1", + "thread-a/terminal-01", + "thread-b/terminal-2", + "thread-a/terminal-10", + ], + ); + expect(selected.map((session) => session.target.terminalId)).toEqual([ + "terminal-1", + "terminal-2", + ]); + const repeated = [...metadata]; + expect(selectKnownTerminalSessions(repeated, environmentA, null)).toBe(all); + expect(selectKnownTerminalSessions(repeated, environmentA, threadB)).toBe(selected); + + const reordered = metadata.toReversed(); + expect(selectKnownTerminalSessions(reordered, environmentA, threadB)).toBe(selected); + expect( + selectKnownTerminalSessions(reordered, environmentA, null).map( + (session) => `${session.target.threadId}/${session.target.terminalId}`, + ), + ).toEqual([ + "thread-a/terminal-01", + "thread-a/terminal-1", + "thread-b/terminal-1", + "thread-b/terminal-2", + "thread-a/terminal-10", + ]); + }); + + it("changes only the affected group and preserves earlier snapshots", () => { + const first = summary(threadA, "terminal-1"); + const second = summary(threadA, "terminal-2"); + const other = summary(threadB, "terminal-1"); + const metadata = [first, second, other]; + const beforeA = selectKnownTerminalSessions(metadata, environmentA, threadA); + const beforeB = selectKnownTerminalSessions(metadata, environmentA, threadB); + Object.freeze(beforeA); + Object.freeze(beforeB); + const exited = { + ...second, + status: "exited" as const, + pid: null, + exitCode: 0, + hasRunningSubprocess: false, + label: "Finished", + updatedAt: "2026-09-04T00:00:01.000Z", + }; + const next = [first, other, exited]; + const afterA = selectKnownTerminalSessions(next, environmentA, threadA); + expect(selectKnownTerminalSessions(next, environmentA, threadB)).toBe(beforeB); + expect(afterA).not.toBe(beforeA); + expect(afterA[0]).toBe(beforeA[0]); + expect(afterA[1]?.state).toMatchObject({ + status: "exited", + hasRunningSubprocess: false, + summary: { label: "Finished", pid: null, exitCode: 0 }, + }); + expect(selectRunningSubprocessTerminalIds(afterA)).toEqual(["terminal-1"]); + expect(beforeA[1]?.state).toMatchObject({ + status: "running", + hasRunningSubprocess: true, + summary: { label: "Terminal", pid: 123 }, + }); + expect(selectKnownTerminalSessions(metadata, environmentA, threadA)).toBe(beforeA); + + const renamed = [first, exited, { ...other, label: "Renamed" }]; + expect(selectKnownTerminalSessions(renamed, environmentA, threadA)).toBe(afterA); + expect( + selectKnownTerminalSessions(renamed, environmentA, threadB)[0]?.state.summary?.label, + ).toBe("Renamed"); + expect(selectKnownTerminalSessions([other], environmentA, threadA)).toEqual([]); + expect(selectKnownTerminalSessions([other], environmentA, threadB)).toBe(beforeB); + }); + + it("keeps environment targets separate when snapshots share the same objects", () => { + const metadata = [summary(threadA, "terminal-1")]; + const first = selectKnownTerminalSessions(metadata, environmentA, threadA); + const second = selectKnownTerminalSessions(metadata, environmentB, threadA); + expect(first[0]?.target.environmentId).toBe(environmentA); + expect(second[0]?.target.environmentId).toBe(environmentB); + expect(first).not.toBe(second); + expect(first[0]).not.toBe(second[0]); + expect(selectKnownTerminalSessions([...metadata], environmentA, threadA)).toBe(first); + expect(selectKnownTerminalSessions([...metadata], environmentB, threadA)).toBe(second); + }); + + it("returns stable empty results without a connected environment or matching sessions", () => { + const metadata = [summary(threadA, "terminal-1")]; + const empty = selectKnownTerminalSessions(null, environmentA, threadA); + expect(selectKnownTerminalSessions(metadata, null, threadA)).toBe(empty); + expect(selectKnownTerminalSessions([], environmentA, null)).toBe(empty); + expect(selectKnownTerminalSessions(metadata, environmentA, threadB)).toBe(empty); + }); + + it("does not materialize unrelated raw thread identifiers", () => { + const metadata = [summary(" untrimmed-thread ", "terminal-1"), summary(threadA, "terminal-2")]; + expect( + selectKnownTerminalSessions(metadata, environmentA, threadA).map( + (session) => session.target.threadId, + ), + ).toEqual([threadA]); + }); + + it("reads the metadata array once for all thread selectors", () => { + const source = Array.from({ length: 40 }, (_, index) => + summary(`thread-${index}`, "terminal-1"), + ); + let reads = 0; + const metadata = new Proxy(source, { + get(target, property, receiver) { + if (typeof property === "string" && /^(0|[1-9]\d*)$/.test(property)) reads += 1; + return Reflect.get(target, property, receiver); + }, + }); + for (const item of source) { + expect( + selectKnownTerminalSessions(metadata, environmentA, ThreadId.make(item.threadId)), + ).toHaveLength(1); + } + expect(selectKnownTerminalSessions(metadata, environmentA, null)).toHaveLength(source.length); + expect(reads).toBe(source.length); + }); +}); diff --git a/apps/web/src/state/terminalSessions.ts b/apps/web/src/state/terminalSessions.ts index 9e480df08b295124e0dd4974dcca65a8a773da69..5e3a397826de80f92fccd48ed6a87c1023dfbd18 100644 --- a/apps/web/src/state/terminalSessions.ts +++ b/apps/web/src/state/terminalSessions.ts @@ -6,12 +6,121 @@ import { type KnownTerminalSession, type TerminalSessionState, } from "@t3tools/client-runtime/state/terminal"; -import { ThreadId, type EnvironmentId, type TerminalAttachInput } from "@t3tools/contracts"; +import { + ThreadId, + type EnvironmentId, + type TerminalAttachInput, + type TerminalSummary, +} from "@t3tools/contracts"; import { useMemo } from "react"; import { useEnvironmentQuery } from "./query"; import { terminalEnvironment } from "./terminal"; +const EMPTY_KNOWN_TERMINAL_SESSIONS = Object.freeze>([]); + +interface TerminalMetadataIndex { + readonly all: ReadonlyArray; + readonly byThreadId: ReadonlyMap>; + readonly views: Map>>; +} + +const metadataIndexes = new WeakMap, TerminalMetadataIndex>(); +const sessionsBySummary = new WeakMap>(); +// Reuse groups a consumer still holds without keeping old group members alive +// merely because their first summary remains in a newer metadata snapshot. +const groupsByAnchor = new WeakMap< + TerminalSummary, + Map< + EnvironmentId, + { + readonly all?: WeakRef>; + readonly thread?: WeakRef>; + } + > +>(); + +function knownSession( + summary: TerminalSummary, + environmentId: EnvironmentId, +): KnownTerminalSession { + let byEnvironment = sessionsBySummary.get(summary); + const previous = byEnvironment?.get(environmentId); + if (previous) return previous; + const session = { + target: { + environmentId, + threadId: ThreadId.make(summary.threadId), + terminalId: summary.terminalId, + }, + state: combineTerminalSessionState(summary, EMPTY_TERMINAL_BUFFER_STATE), + }; + if (!byEnvironment) { + byEnvironment = new Map(); + sessionsBySummary.set(summary, byEnvironment); + } + byEnvironment.set(environmentId, session); + return session; +} + +function terminalMetadataIndex(metadata: ReadonlyArray): TerminalMetadataIndex { + let index = metadataIndexes.get(metadata); + if (!index) { + const compare = new Intl.Collator(undefined, { numeric: true }).compare; + const all = metadata.toSorted((left, right) => compare(left.terminalId, right.terminalId)); + const byThreadId = new Map(); + for (const summary of all) { + const group = byThreadId.get(summary.threadId); + if (group) group.push(summary); + else byThreadId.set(summary.threadId, [summary]); + } + index = { all, byThreadId, views: new Map() }; + metadataIndexes.set(metadata, index); + } + return index; +} + +/** Share one ordered index per immutable snapshot without changing metadata subscriptions. */ +export function selectKnownTerminalSessions( + metadata: ReadonlyArray | null, + environmentId: EnvironmentId | null, + threadId: ThreadId | null, +): ReadonlyArray { + if (environmentId === null || metadata === null || metadata.length === 0) { + return EMPTY_KNOWN_TERMINAL_SESSIONS; + } + const index = terminalMetadataIndex(metadata); + let views = index.views.get(environmentId); + const cached = views?.get(threadId); + if (cached) return cached; + const summaries = threadId === null ? index.all : index.byThreadId.get(threadId); + if (!summaries || summaries.length === 0) return EMPTY_KNOWN_TERMINAL_SESSIONS; + + const anchor = summaries[0]!; + const kind = threadId === null ? "all" : "thread"; + let groups = groupsByAnchor.get(anchor); + const previousGroups = groups?.get(environmentId); + const previous = previousGroups?.[kind]?.deref(); + const sessions = + previous?.length === summaries.length && + previous.every((session, index) => session.state.summary === summaries[index]) + ? previous + : summaries.map((summary) => knownSession(summary, environmentId)); + if (!groups) { + groups = new Map(); + groupsByAnchor.set(anchor, groups); + } + if (sessions !== previous) { + groups.set(environmentId, { ...previousGroups, [kind]: new WeakRef(sessions) }); + } + if (!views) { + views = new Map(); + index.views.set(environmentId, views); + } + views.set(threadId, sessions); + return sessions; +} + export function useAttachedTerminalSession(input: { readonly environmentId: EnvironmentId | null; readonly terminal: TerminalAttachInput | null; @@ -38,11 +147,11 @@ export function useAttachedTerminalSession(input: { return EMPTY_TERMINAL_SESSION_STATE; } const summary = - metadata.data?.find( - (terminal) => - terminal.threadId === input.terminal?.threadId && - terminal.terminalId === input.terminal?.terminalId, - ) ?? null; + (metadata.data === null + ? null + : terminalMetadataIndex(metadata.data) + .byThreadId.get(input.terminal.threadId) + ?.find((terminal) => terminal.terminalId === input.terminal?.terminalId)) ?? null; const state = combineTerminalSessionState(summary, attach.data ?? EMPTY_TERMINAL_BUFFER_STATE); return attach.error === null ? state : { ...state, error: attach.error, status: "error" }; }, [attach.data, attach.error, input.environmentId, input.terminal, metadata.data]); @@ -60,31 +169,16 @@ export function useKnownTerminalSessions(input: { input: null, }), ); - return useMemo(() => { - if (input.environmentId === null) { - return []; - } - return (metadata.data ?? []) - .filter((summary) => input.threadId === null || summary.threadId === input.threadId) - .map((summary) => ({ - target: { - environmentId: input.environmentId!, - threadId: ThreadId.make(summary.threadId), - terminalId: summary.terminalId, - }, - state: combineTerminalSessionState(summary, EMPTY_TERMINAL_BUFFER_STATE), - })) - .sort((left, right) => - left.target.terminalId.localeCompare(right.target.terminalId, undefined, { - numeric: true, - }), - ); - }, [input.environmentId, input.threadId, metadata.data]); + return useMemo( + () => selectKnownTerminalSessions(metadata.data, input.environmentId, input.threadId), + [input.environmentId, input.threadId, metadata.data], + ); } export function useThreadRunningTerminalIds(input: { readonly environmentId: EnvironmentId | null; readonly threadId: ThreadId | null; }): ReadonlyArray { - return selectRunningSubprocessTerminalIds(useKnownTerminalSessions(input)); + const sessions = useKnownTerminalSessions(input); + return useMemo(() => selectRunningSubprocessTerminalIds(sessions), [sessions]); } -- 2.54.0