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
125125 activeThreadId: string | null;
126126 }
127127
128+const LARGE_THREAD_EXCHANGE_COUNT = 120;
129+
130+function 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+
162+function 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+
128196 function buildPerfSeedState(): PerfPersistedState {
129197 const now = Date.now();
130198 const projects: PerfPersistedProject[] = [
@@ -193,6 +261,10 @@ function buildPerfSeedState(): PerfPersistedState { · 62 unmodified lines
193261 }
194262 }
195263
264+ for (const project of projects) {
265+ threads.push(buildLargePerfThread(project, now));
266+ }
267+
196268 return {
197269 version: 7,
198270 runtimeMode: "approval-required",
@@ -216,13 +288,27 @@ async function seedRendererState(window: BrowserWindow): Promise<void> { · 17 unmodified lines
216288
217289 async function runRendererPerfInteractions(
218290 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+}> {
220297 const script = `
221298 (async () => {
222299 const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
223300 const must = (condition, message) => {
224301 if (!condition) throw new Error(message);
225302 };
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+ };
226312
227313 const clickElement = (node) => {
228314 if (!(node instanceof HTMLElement)) return false;
@@ -242,6 +328,43 @@ async function runRendererPerfInteractions( · 13 unmodified lines
242328 await sleep(80);
243329 }
244330
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+
245368 const scroller = document.querySelector("[data-perf-messages-scroll]");
246369 if (scroller instanceof HTMLElement) {
247370 scroller.scrollTo({ top: scroller.scrollHeight, behavior: "instant" });
@@ -319,6 +442,7 @@ async function runRendererPerfInteractions( · 71 unmodified lines
319442 threadClicks: clickCount,
320443 typedChars: inputText.length,
321444 selectedModel,
445+ largeThreadRenderStats,
322446 };
323447 })();
324448 `;
apps/web/src/components/ChatView.tsx+2−0
@@ -1345,6 +1345,8 @@ export default function ChatView() { · 1344 unmodified lines
13451345 <div
13461346 ref={messagesScrollRef}
13471347 data-perf-messages-scroll
1348+ data-perf-active-thread-id={activeThread.id}
1349+ data-perf-message-count={activeThread.messages.length}
13481350 className="min-h-0 flex-1 overflow-y-auto px-5 py-4"
13491351 onScroll={onMessagesScroll}
13501352 >
scripts/desktop-perf-trace.mjs+13−0
@@ -162,6 +162,9 @@ function summarizeTrace(tracePath) { · 161 unmodified lines
162162 }
163163
164164 function createMarkdownSummary({ tracePath, donePayload, summary, thresholds }) {
165+ const largeThreadRenderStats = Array.isArray(donePayload.interactions?.largeThreadRenderStats)
166+ ? donePayload.interactions.largeThreadRenderStats
167+ : [];
165168 const lines = [
166169 marker,
167170 "## Desktop Dev Perf Trace",
@@ -178,6 +181,16 @@ function createMarkdownSummary({ tracePath, donePayload, summary, thresholds }) · 10 unmodified lines
178181 `- Typed chars: ${donePayload.interactions?.typedChars ?? "n/a"}`,
179182 `- Model selected: ${donePayload.interactions?.selectedModel ?? "n/a"}`,
180183 "",
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+ "",
181194 "### Input Event Metrics",
182195 "",
183196 "| Event | Count | Avg (ms) | Max (ms) | Total (ms) |",