| @@ -125,6 +125,74 @@ interface PerfPersistedState { · 124 unmodified lines |
| 125 | activeThreadId: string | null; | 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 | function buildPerfSeedState(): PerfPersistedState { | 196 | function 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 | |
| 217 | async function runRendererPerfInteractions( | 289 | async 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 | `; |