History

Track large-thread render timings in desktop perf trace

- Seed each perf project with a large synthetic thread for stress coverage - Measure thread-switch render latency/message counts via ChatView data attributes - Include a Large Thread Render table in `desktop-perf-trace` markdown output

Julius Marminge committed 8 months ago

Commit 41caab91 parent 25cc1393 files · +140 −1Browse files

Unified · Split

3 files
  1. apps/desktop/src/desktopPerfAutomation.ts +125 −1
  2. apps/web/src/components/ChatView.tsx +2 −0
  3. scripts/desktop-perf-trace.mjs +13 −0
apps/desktop/src/desktopPerfAutomation.ts+125−1
@@ -125,6 +125,74 @@ interface PerfPersistedState { · 124 unmodified lines
125 activeThreadId: string | null;125 activeThreadId: string | null;
126}126}
127 127
128const LARGE_THREAD_EXCHANGE_COUNT = 120;
129
130function buildLargeThreadMessage(
131 projectName: string,
132 exchangeIndex: number,
133 role: "user" | "assistant",
134): string {
135 if (role === "user") {
136 return [
137 `Large-thread perf scenario ${exchangeIndex + 1} for ${projectName}.`,
138 "Please audit render cost while scrolling and switching among heavy histories.",
139 "Include hotspots in markdown rendering, timeline grouping, and message list virtualization candidates.",
140 "Track regressions across interaction loops and report any long-task spikes.",
141 ].join("\n");
142 }
143
144 return [
145 `### Perf analysis batch ${exchangeIndex + 1}`,
146 "",
147 "- Checked keypress/input dispatch cost and scheduler churn.",
148 "- Compared component update cadence before/after thread switch.",
149 "- Captured timeline and branch/tool state snapshots.",
150 "",
151 "```txt",
152 `project=${projectName}`,
153 `batch=${exchangeIndex + 1}`,
154 "status=stable",
155 "focus=render-throughput",
156 "```",
157 "",
158 "Observed stable behavior under bursty UI events; continue profiling for long-thread rendering regressions.",
159 ].join("\n");
160}
161
162function buildLargePerfThread(project: PerfPersistedProject, now: number): PerfPersistedThread {
163 const threadId = `${project.id}-thread-large`;
164 const createdAt = new Date(now - 1_500_000).toISOString();
165 const messages: PerfPersistedMessage[] = [];
166 for (let exchangeIndex = 0; exchangeIndex < LARGE_THREAD_EXCHANGE_COUNT; exchangeIndex += 1) {
167 const offsetMs = (LARGE_THREAD_EXCHANGE_COUNT - exchangeIndex) * 11_000;
168 messages.push({
169 id: `${threadId}-user-${exchangeIndex + 1}`,
170 role: "user",
171 text: buildLargeThreadMessage(project.name, exchangeIndex, "user"),
172 createdAt: new Date(now - offsetMs - 2_000).toISOString(),
173 streaming: false,
174 });
175 messages.push({
176 id: `${threadId}-assistant-${exchangeIndex + 1}`,
177 role: "assistant",
178 text: buildLargeThreadMessage(project.name, exchangeIndex, "assistant"),
179 createdAt: new Date(now - offsetMs).toISOString(),
180 streaming: false,
181 });
182 }
183
184 return {
185 id: threadId,
186 projectId: project.id,
187 title: `${project.name} perf large thread`,
188 model: "gpt-5-codex",
189 terminalOpen: false,
190 messages,
191 createdAt,
192 lastVisitedAt: new Date(now - 1_000_000).toISOString(),
193 };
194}
195
128function buildPerfSeedState(): PerfPersistedState {196function buildPerfSeedState(): PerfPersistedState {
129 const now = Date.now();197 const now = Date.now();
130 const projects: PerfPersistedProject[] = [198 const projects: PerfPersistedProject[] = [
@@ -193,6 +261,10 @@ function buildPerfSeedState(): PerfPersistedState { · 62 unmodified lines
193 }261 }
194 }262 }
195 263
264 for (const project of projects) {
265 threads.push(buildLargePerfThread(project, now));
266 }
267
196 return {268 return {
197 version: 7,269 version: 7,
198 runtimeMode: "approval-required",270 runtimeMode: "approval-required",
@@ -216,13 +288,27 @@ async function seedRendererState(window: BrowserWindow): Promise<void> { · 17 unmodified lines
216 288
217async function runRendererPerfInteractions(289async function runRendererPerfInteractions(
218 window: BrowserWindow,290 window: BrowserWindow,
219): Promise<{ threadClicks: number; typedChars: number; selectedModel: string | null }> {291): Promise<{
292 threadClicks: number;
293 typedChars: number;
294 selectedModel: string | null;
295 largeThreadRenderStats: { threadId: string; messageCount: number; renderMs: number }[];
296}> {
220 const script = `297 const script = `
221 (async () => {298 (async () => {
222 const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));299 const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
223 const must = (condition, message) => {300 const must = (condition, message) => {
224 if (!condition) throw new Error(message);301 if (!condition) throw new Error(message);
225 };302 };
303 const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
304 const waitFor = async (predicate, timeoutMs, message) => {
305 const deadline = performance.now() + timeoutMs;
306 while (performance.now() < deadline) {
307 if (predicate()) return;
308 await nextFrame();
309 }
310 throw new Error(message);
311 };
226 312
227 const clickElement = (node) => {313 const clickElement = (node) => {
228 if (!(node instanceof HTMLElement)) return false;314 if (!(node instanceof HTMLElement)) return false;
@@ -242,6 +328,43 @@ async function runRendererPerfInteractions( · 13 unmodified lines
242 await sleep(80);328 await sleep(80);
243 }329 }
244 330
331 const measureThreadRender = async (threadId) => {
332 const selector = '[data-perf-thread-id="' + threadId + '"]';
333 const targetButton = document.querySelector(selector);
334 must(targetButton instanceof HTMLElement, "Thread button missing for " + threadId);
335 const start = performance.now();
336 clickElement(targetButton);
337 await waitFor(
338 () => {
339 const scroller = document.querySelector("[data-perf-messages-scroll]");
340 return (
341 scroller instanceof HTMLElement &&
342 scroller.getAttribute("data-perf-active-thread-id") === threadId
343 );
344 },
345 15_000,
346 "Timed out waiting for thread activation " + threadId,
347 );
348 await nextFrame();
349 await nextFrame();
350 const scroller = document.querySelector("[data-perf-messages-scroll]");
351 const messageCountRaw =
352 scroller instanceof HTMLElement ? scroller.getAttribute("data-perf-message-count") : null;
353 const messageCount =
354 messageCountRaw && /^\\d+$/.test(messageCountRaw) ? Number(messageCountRaw) : 0;
355 return {
356 threadId,
357 messageCount,
358 renderMs: Number((performance.now() - start).toFixed(2)),
359 };
360 };
361
362 const largeThreadRenderStats = [];
363 const largeThreadIds = ["perf-project-1-thread-large", "perf-project-2-thread-large"];
364 for (const threadId of largeThreadIds) {
365 largeThreadRenderStats.push(await measureThreadRender(threadId));
366 }
367
245 const scroller = document.querySelector("[data-perf-messages-scroll]");368 const scroller = document.querySelector("[data-perf-messages-scroll]");
246 if (scroller instanceof HTMLElement) {369 if (scroller instanceof HTMLElement) {
247 scroller.scrollTo({ top: scroller.scrollHeight, behavior: "instant" });370 scroller.scrollTo({ top: scroller.scrollHeight, behavior: "instant" });
@@ -319,6 +442,7 @@ async function runRendererPerfInteractions( · 71 unmodified lines
319 threadClicks: clickCount,442 threadClicks: clickCount,
320 typedChars: inputText.length,443 typedChars: inputText.length,
321 selectedModel,444 selectedModel,
445 largeThreadRenderStats,
322 };446 };
323 })();447 })();
324 `;448 `;
apps/web/src/components/ChatView.tsx+2−0
@@ -1345,6 +1345,8 @@ export default function ChatView() { · 1344 unmodified lines
1345 <div1345 <div
1346 ref={messagesScrollRef}1346 ref={messagesScrollRef}
1347 data-perf-messages-scroll1347 data-perf-messages-scroll
1348 data-perf-active-thread-id={activeThread.id}
1349 data-perf-message-count={activeThread.messages.length}
1348 className="min-h-0 flex-1 overflow-y-auto px-5 py-4"1350 className="min-h-0 flex-1 overflow-y-auto px-5 py-4"
1349 onScroll={onMessagesScroll}1351 onScroll={onMessagesScroll}
1350 >1352 >
scripts/desktop-perf-trace.mjs+13−0
@@ -162,6 +162,9 @@ function summarizeTrace(tracePath) { · 161 unmodified lines
162}162}
163 163
164function createMarkdownSummary({ tracePath, donePayload, summary, thresholds }) {164function createMarkdownSummary({ tracePath, donePayload, summary, thresholds }) {
165 const largeThreadRenderStats = Array.isArray(donePayload.interactions?.largeThreadRenderStats)
166 ? donePayload.interactions.largeThreadRenderStats
167 : [];
165 const lines = [168 const lines = [
166 marker,169 marker,
167 "## Desktop Dev Perf Trace",170 "## Desktop Dev Perf Trace",
@@ -178,6 +181,16 @@ function createMarkdownSummary({ tracePath, donePayload, summary, thresholds }) · 10 unmodified lines
178 `- Typed chars: ${donePayload.interactions?.typedChars ?? "n/a"}`,181 `- Typed chars: ${donePayload.interactions?.typedChars ?? "n/a"}`,
179 `- Model selected: ${donePayload.interactions?.selectedModel ?? "n/a"}`,182 `- Model selected: ${donePayload.interactions?.selectedModel ?? "n/a"}`,
180 "",183 "",
184 "### Large Thread Render",
185 "",
186 "| Thread | Messages | Render (ms) |",
187 "| --- | ---: | ---: |",
188 ...(largeThreadRenderStats.length > 0
189 ? largeThreadRenderStats.map(
190 (stat) => `| ${stat.threadId} | ${stat.messageCount} | ${stat.renderMs} |`,
191 )
192 : ["| n/a | n/a | n/a |"]),
193 "",
181 "### Input Event Metrics",194 "### Input Event Metrics",
182 "",195 "",
183 "| Event | Count | Avg (ms) | Max (ms) | Total (ms) |",196 "| Event | Count | Avg (ms) | Max (ms) | Total (ms) |",