154 lines
6.2 KiB
TypeScript
154 lines
6.2 KiB
TypeScript
/**
|
|
* 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)
|