| @@ -125,6 +125,74 @@ interface PerfPersistedState { · 124 unmodified lines |
| 125 | 125 | activeThreadId: string | null; |
| 126 | 126 | } |
| 127 | 127 | |
| 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 | + |
| 128 | 196 | function buildPerfSeedState(): PerfPersistedState { |
| 129 | 197 | const now = Date.now(); |
| 130 | 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 | 268 | return { |
| 197 | 269 | version: 7, |
| 198 | 270 | runtimeMode: "approval-required", |
| @@ -216,13 +288,27 @@ async function seedRendererState(window: BrowserWindow): Promise<void> { · 17 unmodified lines |
| 216 | 288 | |
| 217 | 289 | async function runRendererPerfInteractions( |
| 218 | 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 | 297 | const script = ` |
| 221 | 298 | (async () => { |
| 222 | 299 | const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); |
| 223 | 300 | const must = (condition, message) => { |
| 224 | 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 | 313 | const clickElement = (node) => { |
| 228 | 314 | if (!(node instanceof HTMLElement)) return false; |
| @@ -242,6 +328,43 @@ async function runRendererPerfInteractions( · 13 unmodified lines |
| 242 | 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 | 368 | const scroller = document.querySelector("[data-perf-messages-scroll]"); |
| 246 | 369 | if (scroller instanceof HTMLElement) { |
| 247 | 370 | scroller.scrollTo({ top: scroller.scrollHeight, behavior: "instant" }); |
| @@ -319,6 +442,7 @@ async function runRendererPerfInteractions( · 71 unmodified lines |
| 319 | 442 | threadClicks: clickCount, |
| 320 | 443 | typedChars: inputText.length, |
| 321 | 444 | selectedModel, |
| 445 | + largeThreadRenderStats, |
| 322 | 446 | }; |
| 323 | 447 | })(); |
| 324 | 448 | `; |