返回 DeepSeek-Reasonix
history-performance-benchmark.tsx
根目录 / desktop / frontend / src / __tests__ / history-performance-benchmark.tsx
1 // Run:
2 // corepack pnpm --dir desktop/frontend exec tsx src/__tests__/history-performance-benchmark.tsx
3 //
4 // Synthetic, privacy-safe benchmark for long restored histories. It logs counts,
5 // byte lengths, and elapsed times only; it never uses real conversation content.
6
7 import { historyMessagesToItems, initialState, reducer, type Item } from "../lib/useController";
8 import { buildTurnGroups, compactQuestionText, scrollVersion, type TurnGroup } from "../lib/transcriptGrouping";
9 import type { HistoryMessage } from "../lib/types";
10
11 type BenchCase = {
12 name: string;
13 turns: number;
14 toolsPerTurn: number;
15 outputSize: number;
16 archived: boolean;
17 };
18
19 type BenchResult = {
20 name: string;
21 messages: number;
22 items: number;
23 jsonBytes: number;
24 itemStringBytes: number;
25 convertMs: number;
26 reducerMs: number;
27 transcriptComputeMs: number;
28 turnGroups: number;
29 };
30
31 const cases: BenchCase[] = [
32 { name: "200-turns-full-10KB", turns: 200, toolsPerTurn: 1, outputSize: 10 * 1024, archived: false },
33 { name: "200-turns-archived-10KB", turns: 200, toolsPerTurn: 1, outputSize: 10 * 1024, archived: true },
34 { name: "1000-turns-full-1KB", turns: 1000, toolsPerTurn: 1, outputSize: 1024, archived: false },
35 { name: "1000-turns-archived-1KB", turns: 1000, toolsPerTurn: 1, outputSize: 1024, archived: true },
36 { name: "1000-turns-archived-3-tools", turns: 1000, toolsPerTurn: 3, outputSize: 1024, archived: true },
37 { name: "5000-turns-archived-1-tool", turns: 5000, toolsPerTurn: 1, outputSize: 1024, archived: true },
38 { name: "10000-turns-archived-1-tool", turns: 10000, toolsPerTurn: 1, outputSize: 1024, archived: true },
39 ];
40
41 function syntheticHistory(c: BenchCase): HistoryMessage[] {
42 const messages: any[] = [];
43 const output = "x".repeat(c.outputSize);
44 for (let turn = 0; turn < c.turns; turn += 1) {
45 messages.push({ role: "user", content: `prompt ${turn}` });
46 const toolCalls: any[] = [];
47 for (let tool = 0; tool < c.toolsPerTurn; tool += 1) {
48 const id = `call_${turn}_${tool}`;
49 toolCalls.push({
50 id,
51 name: "bash",
52 arguments: c.archived ? "" : `{"command":"synthetic ${turn} ${tool}"}`,
53 argumentsArchived: c.archived || undefined,
54 subject: c.archived ? `synthetic ${turn} ${tool}` : undefined,
55 summary: c.archived ? "1 line" : undefined,
56 });
57 }
58 messages.push({ role: "assistant", content: `answer ${turn}`, toolCalls });
59 for (let tool = 0; tool < c.toolsPerTurn; tool += 1) {
60 const id = `call_${turn}_${tool}`;
61 messages.push({
62 role: "tool",
63 toolCallId: id,
64 toolName: "bash",
65 content: c.archived ? "" : output,
66 toolResultArchived: c.archived || undefined,
67 });
68 }
69 }
70 return messages as HistoryMessage[];
71 }
72
73 function itemStringBytes(items: Item[]): number {
74 let total = 0;
75 for (const item of items) {
76 if (item.kind === "user") total += item.text.length;
77 if (item.kind === "assistant") total += item.text.length + item.reasoning.length;
78 if (item.kind === "tool") total += item.args.length + (item.output?.length ?? 0) + (item.error?.length ?? 0);
79 }
80 return total;
81 }
82
83 function time<T>(fn: () => T): { value: T; ms: number } {
84 const start = performance.now();
85 const value = fn();
86 return { value, ms: performance.now() - start };
87 }
88
89 function buildQuestions(items: Item[]): number {
90 let anchors = 0;
91 for (const item of items) {
92 if (item.kind !== "user") continue;
93 compactQuestionText(item.text);
94 anchors += 1;
95 }
96 return anchors;
97 }
98
99 function buildSubcallsByParent(items: Item[]): Map<string, Extract<Item, { kind: "tool" }>[]> {
100 const map = new Map<string, Extract<Item, { kind: "tool" }>[]>();
101 for (const item of items) {
102 if (item.kind === "tool" && item.parentId) {
103 const arr = map.get(item.parentId) ?? [];
104 arr.push(item);
105 map.set(item.parentId, arr);
106 }
107 }
108 return map;
109 }
110
111 function computeTranscriptInputs(items: Item[]): TurnGroup[] {
112 buildQuestions(items);
113 scrollVersion(items);
114 buildSubcallsByParent(items);
115 let needed = 30;
116 for (let i = items.length - 1; i >= 0; i -= 1) {
117 if (items[i].kind === "user") {
118 needed -= 1;
119 if (needed <= 0) break;
120 }
121 }
122 return buildTurnGroups(items);
123 }
124
125 function runCase(c: BenchCase): BenchResult {
126 const messages = syntheticHistory(c);
127 const jsonBytes = JSON.stringify(messages).length;
128 const converted = time(() => historyMessagesToItems(messages, "perf"));
129 const reduced = time(() => reducer(initialState, { type: "history", messages }));
130 const items = converted.value.items;
131 const transcript = time(() => computeTranscriptInputs(items));
132 return {
133 name: c.name,
134 messages: messages.length,
135 items: items.length,
136 jsonBytes,
137 itemStringBytes: itemStringBytes(reduced.value.items),
138 convertMs: converted.ms,
139 reducerMs: reduced.ms,
140 transcriptComputeMs: transcript.ms,
141 turnGroups: transcript.value.length,
142 };
143 }
144
145 function printResult(r: BenchResult): void {
146 process.stdout.write([
147 r.name,
148 `messages=${r.messages}`,
149 `items=${r.items}`,
150 `jsonBytes=${r.jsonBytes}`,
151 `itemStringBytes=${r.itemStringBytes}`,
152 `convertMs=${r.convertMs.toFixed(2)}`,
153 `reducerMs=${r.reducerMs.toFixed(2)}`,
154 `transcriptComputeMs=${r.transcriptComputeMs.toFixed(2)}`,
155 `turnGroups=${r.turnGroups}`,
156 ].join(" ") + "\n");
157 }
158
159 console.log("\nhistory performance benchmark");
160 const results = cases.map(runCase);
161 for (const result of results) {
162 printResult(result);
163 }
164
165 const failures: string[] = [];
166 for (let index = 0; index < results.length; index += 1) {
167 const result = results[index];
168 const input = cases[index];
169 const expectedMessages = input.turns * (2 + input.toolsPerTurn);
170 if (result.messages !== expectedMessages) failures.push(`${result.name}: unexpected message count`);
171 if (result.turnGroups !== input.turns) failures.push(`${result.name}: unexpected turn-group count`);
172 if (result.convertMs > 1_000 || result.reducerMs > 1_000 || result.transcriptComputeMs > 1_000) {
173 failures.push(`${result.name}: exceeded 1s responsiveness ceiling`);
174 }
175 }
176
177 const full10KB = results.find((result) => result.name === "200-turns-full-10KB");
178 const archived10KB = results.find((result) => result.name === "200-turns-archived-10KB");
179 if (!full10KB || full10KB.itemStringBytes * 10 >= full10KB.jsonBytes) {
180 failures.push("restored full tool results retained too much source text");
181 }
182 if (!archived10KB || archived10KB.itemStringBytes * 5 >= archived10KB.jsonBytes) {
183 failures.push("restored archived tool results retained too much source text");
184 }
185
186 if (failures.length > 0) {
187 for (const failure of failures) process.stderr.write(`FAIL ${failure}\n`);
188 process.exit(1);
189 }
190 process.stdout.write("PASS long-history performance contracts\n");
191
191 lines Plain Text