| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- /**
- * Opt-in browser stress reproduction for reasoning-stream renderer stalls.
- * The fixture emits 100,000 individual chunks through the normal async
- * carrier; the test measures event-loop and scheduled-interaction delay while
- * the assembled React surface keeps a collapsed Think row live.
- */
- import type { Browser, Page } from 'playwright'
- import { chromium } from 'playwright'
- import { expect, it, onTestFailed } from 'vitest'
- import { launchWebScaffold, watchConsole, type WebScaffold } from '../tests/scaffold.ts'
- import { newEnglishPage, saveFailureShot } from '../tests/support.ts'
- const CHUNK_COUNT = 100_000
- const CHUNKS_PER_INTERVAL = 128
- const CHUNK_INTERVAL_MS = 16
- const MAIN_THREAD_DELAY_BUDGET_MS = 250
- interface ReasoningChunkStormState {
- sessionId: string
- chunkCount: number
- chunksPerInterval: number
- intervalMs: number
- emitted: number
- marker: string
- emitting: boolean
- }
- interface StressProbe {
- intervalId: number
- intervalMs: number
- lastTickAt: number
- maxDelayMs: number
- samples: number
- interactionDueAt: number
- interactionHandledAt: number | null
- }
- interface StressWindow extends Window {
- __fxTiming?: {
- startReasoningChunkStorm(id: string, chunkCount: number, chunksPerInterval: number, intervalMs: number): string
- reasoningChunkStormState(): ReasoningChunkStormState | null
- }
- __reasoningStressProbe?: StressProbe
- }
- it('keeps the browser responsive while rendering 100,000 reasoning chunks', async () => {
- let scaffold: WebScaffold | undefined
- let browser: Browser | undefined
- let page: Page | undefined
- try {
- scaffold = await launchWebScaffold()
- browser = await chromium.launch({ headless: process.env.DSH_WEB_STRESS_HEADFUL !== '1' })
- page = await newEnglishPage(browser)
- const activePage = page
- await activePage.addInitScript(() => {
- localStorage.setItem('dsh.sessions.current', JSON.stringify({ sessionId: 'fx-alpha' }))
- })
- const tripwire = watchConsole(activePage)
- onTestFailed(() => saveFailureShot(activePage, 'web-stress-reasoning-chunks'))
- await activePage.goto(`${scaffold.baseUrl}?fixture`, { waitUntil: 'load' })
- await activePage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
- // Fixture settings deliberately reject writes, so its welcome notice
- // cannot acknowledge. Hide only that test overlay; the assembled chat
- // tree beneath it remains mounted and exercises the production renderer.
- await activePage.addStyleTag({ content: '[class*="onboardingOverlay"] { display: none !important; }' })
- await activePage.locator('[data-sample="bash"]').first().waitFor({ timeout: 30_000 })
- await activePage.evaluate(() => {
- const intervalMs = 50
- const now = performance.now()
- const probe: StressProbe = {
- intervalId: 0,
- intervalMs,
- lastTickAt: now,
- maxDelayMs: 0,
- samples: 0,
- interactionDueAt: now + 1_000,
- interactionHandledAt: null,
- }
- probe.intervalId = window.setInterval(() => {
- const tickAt = performance.now()
- probe.maxDelayMs = Math.max(probe.maxDelayMs, tickAt - probe.lastTickAt - intervalMs)
- probe.lastTickAt = tickAt
- probe.samples++
- }, intervalMs)
- document.body.addEventListener('reasoning-stress-interaction', () => {
- probe.interactionHandledAt = performance.now()
- }, { once: true })
- window.setTimeout(() => {
- document.body.dispatchEvent(new CustomEvent('reasoning-stress-interaction'))
- }, 1_000)
- ;(window as StressWindow).__reasoningStressProbe = probe
- })
- const marker = await activePage.evaluate(({ chunkCount, chunksPerInterval, intervalMs }) => {
- const hooks = (window as StressWindow).__fxTiming
- if (hooks === undefined) throw new Error('reasoning stress fixture hooks unavailable')
- return hooks.startReasoningChunkStorm('fx-alpha', chunkCount, chunksPerInterval, intervalMs)
- }, {
- chunkCount: CHUNK_COUNT,
- chunksPerInterval: CHUNKS_PER_INTERVAL,
- intervalMs: CHUNK_INTERVAL_MS,
- })
- const liveThink = activePage.locator('[data-variant="think"][data-state="running"]').last()
- await liveThink.waitFor({ timeout: 60_000 })
- await expect.poll(async () => await activePage.evaluate(() => {
- const hooks = (window as StressWindow).__fxTiming
- return hooks?.reasoningChunkStormState()?.emitted ?? 0
- }), { timeout: 540_000, interval: 100 }).toBe(CHUNK_COUNT)
- await expect.poll(() => liveThink.textContent(), { timeout: 60_000, interval: 100 }).toContain(marker)
- const report = await activePage.evaluate(() => {
- const win = window as StressWindow
- const probe = win.__reasoningStressProbe
- const state = win.__fxTiming?.reasoningChunkStormState()
- if (probe === undefined || state === undefined || state === null) {
- throw new Error('reasoning stress metrics unavailable')
- }
- window.clearInterval(probe.intervalId)
- const interactionDelayMs = probe.interactionHandledAt === null
- ? null
- : probe.interactionHandledAt - probe.interactionDueAt
- return {
- chunkCount: state.chunkCount,
- chunksPerInterval: state.chunksPerInterval,
- intervalMs: state.intervalMs,
- emitted: state.emitted,
- maxMainThreadDelayMs: Math.max(0, probe.maxDelayMs),
- interactionDelayMs,
- heartbeatSamples: probe.samples,
- }
- })
- process.stdout.write(`reasoning-chunk stress report: ${JSON.stringify(report)}\n`)
- expect(report).toMatchObject({
- chunkCount: CHUNK_COUNT,
- chunksPerInterval: CHUNKS_PER_INTERVAL,
- intervalMs: CHUNK_INTERVAL_MS,
- emitted: CHUNK_COUNT,
- })
- expect(report.heartbeatSamples).toBeGreaterThan(0)
- const interactionDelayMs = report.interactionDelayMs
- if (interactionDelayMs === null) throw new Error(`scheduled interaction was not handled: ${JSON.stringify(report)}`)
- expect(report.maxMainThreadDelayMs, JSON.stringify(report)).toBeLessThan(MAIN_THREAD_DELAY_BUDGET_MS)
- expect(interactionDelayMs, JSON.stringify(report)).toBeLessThan(MAIN_THREAD_DELAY_BUDGET_MS)
- expect(tripwire.pageErrors).toEqual([])
- expect(tripwire.warnings).toEqual([])
- } finally {
- await browser?.close()
- await scaffold?.close()
- }
- }, 600_000)
|