Make terminal scroll pressure benchmark diagnose responsive paths (#4846)

* Instrument terminal scroll pressure attempts

* Trace terminal scroll input timing

* Reduce PTY renderer in-flight window

* Revert "Reduce PTY renderer in-flight window"

This reverts commit 86775aeb68968e88b98c3a4c1b710eed5b0833b3.

* Probe alternate scroll paths after slow wheel

* Gate scroll pressure on responsive terminal path

* Address scroll benchmark review cleanup
This commit is contained in:
Neil 2026-06-07 18:16:53 -07:00 committed by GitHub
parent 675376023f
commit 102cbdbda2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 391 additions and 164 deletions

View File

@ -0,0 +1,156 @@
import type { Page } from '@stablyai/playwright-test'
export type ActiveTerminalScrollState = {
viewportY: number
scrollTop: number | null
}
export async function scrollActiveTerminalToBottom(page: Page): Promise<void> {
await page.evaluate(() => {
const pane = (() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const candidate = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!candidate) {
throw new Error('Active terminal pane is unavailable')
}
return candidate
})()
pane.terminal.scrollToBottom()
})
}
export async function scrollActiveTerminalViewportElement(page: Page): Promise<void> {
await page.evaluate(() => {
const pane = (() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const candidate = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!candidate) {
throw new Error('Active terminal pane is unavailable')
}
return candidate
})()
const viewport = pane.container.querySelector<HTMLElement>('.xterm-viewport')
if (!viewport) {
throw new Error('Active terminal viewport is unavailable')
}
// Why: Linux CI can drop wheel delivery entirely under PTY flood; changing
// the viewport scrollTop exercises xterm's DOM scroll synchronization.
viewport.scrollTop = Math.max(0, viewport.scrollTop - 1200)
viewport.dispatchEvent(new Event('scroll', { bubbles: true }))
})
}
export async function scrollActiveTerminalByApi(page: Page): Promise<void> {
await page.evaluate(() => {
const pane = (() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const candidate = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!candidate) {
throw new Error('Active terminal pane is unavailable')
}
return candidate
})()
// Why: Linux/Xvfb can lose synthetic wheel/DOM scroll events under flood;
// xterm's public API keeps this probe about viewport responsiveness.
const targetLine = Math.max(0, pane.terminal.buffer.active.viewportY - 20)
pane.terminal.scrollToLine(targetLine)
})
}
export async function dispatchActiveTerminalWheelEvent(page: Page): Promise<void> {
await page.evaluate(() => {
const pane = (() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const candidate = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!candidate) {
throw new Error('Active terminal pane is unavailable')
}
return candidate
})()
// Why: CI can drop CDP wheel input while the active textarea is focused;
// dispatching on xterm's own surfaces still exercises its user scroll path.
const wheelTargets = [
pane.container.querySelector<HTMLElement>('.xterm'),
pane.container.querySelector<HTMLElement>('.xterm-viewport'),
pane.container.querySelector<HTMLElement>('.xterm-screen')
].filter((target): target is HTMLElement => Boolean(target))
if (wheelTargets.length === 0) {
throw new Error('Active terminal wheel target is unavailable')
}
for (const wheelTarget of wheelTargets) {
wheelTarget.dispatchEvent(
new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: -1200
})
)
}
})
}
export async function readActiveTerminalScrollState(
page: Page
): Promise<ActiveTerminalScrollState> {
return page.evaluate(() => {
const pane = (() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const candidate = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!candidate) {
throw new Error('Active terminal pane is unavailable')
}
return candidate
})()
const viewport = pane.container.querySelector<HTMLElement>('.xterm-viewport')
return {
viewportY: pane.terminal.buffer.active.viewportY,
scrollTop: viewport?.scrollTop ?? null
}
})
}

View File

@ -7,6 +7,7 @@ import { sendToTerminal } from './helpers/terminal'
import { writePressureOutputScript } from './artificial-opencode-hidden-pressure-scenario'
import {
annotateScrollMeasurement,
getResponsiveScrollPath,
measureActiveTerminalWheelScroll,
scrollActiveTerminalToBottom,
seedActiveTerminalScrollback
@ -240,9 +241,9 @@ async function measureAndAnnotateScroll<
mainPressureAfterScroll,
ackGateAfterScroll
)
const scrollMoved = scrollMeasurement.afterViewportY < scrollMeasurement.beforeViewportY
if (scrollMoved) {
expect(scrollMeasurement.scrollLatencyMs).toBeLessThan(maxScrollLatencyMs)
const responsivePath = getResponsiveScrollPath(scrollMeasurement)
if (responsivePath) {
expect(responsivePath.latencyMs).toBeLessThan(maxScrollLatencyMs)
}
expect(scrollMeasurement.maxTimerDriftMs).toBeLessThan(maxTimerDriftMs)
await scrollActiveTerminalToBottom(orcaPage)

View File

@ -0,0 +1,75 @@
export type ScrollAttemptMeasurement = {
name: string
actionMs: number
observeMs: number
beforeViewportY: number
afterActionViewportY: number
afterViewportY: number
beforeScrollTop: number | null
afterActionScrollTop: number | null
afterScrollTop: number | null
error?: string
}
export type ResponsiveScrollPath = {
name: string
latencyMs: number
}
type ScrollMeasurementLike = {
scrollLatencyMs: number
beforeViewportY: number
afterViewportY: number
attempts: ScrollAttemptMeasurement[]
}
export function formatScrollAttempts(attempts: ScrollAttemptMeasurement[]): string {
return attempts
.map(
(attempt) =>
`${attempt.name}:${attempt.beforeViewportY}>${attempt.afterActionViewportY}>${
attempt.afterViewportY
}` +
`(${formatNullableNumber(attempt.beforeScrollTop)}>${formatNullableNumber(
attempt.afterActionScrollTop
)}>${formatNullableNumber(
attempt.afterScrollTop
)};action=${attempt.actionMs.toFixed(1)};observe=${attempt.observeMs.toFixed(1)})${
attempt.error ? ':error' : ''
}`
)
.join(',')
}
export function getResponsiveScrollPath(
measurement: ScrollMeasurementLike
): ResponsiveScrollPath | null {
let best: ResponsiveScrollPath | null = null
const recordCandidate = (candidate: ResponsiveScrollPath): void => {
if (!best || candidate.latencyMs < best.latencyMs) {
best = candidate
}
}
const cdpWheel = measurement.attempts.find((attempt) => attempt.name === 'cdpWheel')
if (cdpWheel && cdpWheel.afterViewportY < cdpWheel.beforeViewportY) {
recordCandidate({
name: cdpWheel.name,
latencyMs: measurement.scrollLatencyMs
})
}
for (const attempt of measurement.attempts) {
if (attempt.name === 'cdpWheel' || attempt.afterViewportY >= attempt.beforeViewportY) {
continue
}
recordCandidate({
name: attempt.name,
latencyMs: attempt.actionMs + attempt.observeMs
})
}
return best
}
function formatNullableNumber(value: number | null): string {
return value === null ? 'na' : value.toFixed(0)
}

View File

@ -1,12 +1,28 @@
import type { Page, TestInfo } from '@stablyai/playwright-test'
import {
dispatchActiveTerminalWheelEvent,
readActiveTerminalScrollState,
scrollActiveTerminalByApi,
scrollActiveTerminalToBottom,
scrollActiveTerminalViewportElement,
type ActiveTerminalScrollState
} from './artificial-opencode-active-terminal-scroll'
import {
formatScrollAttempts,
getResponsiveScrollPath,
type ScrollAttemptMeasurement
} from './artificial-opencode-scroll-measurement'
import { sendToTerminal, waitForTerminalOutput } from './helpers/terminal'
export { getResponsiveScrollPath }
export type ScrollMeasurement = {
scrollLatencyMs: number
maxTimerDriftMs: number
beforeViewportY: number
afterViewportY: number
baseY: number
attempts: ScrollAttemptMeasurement[]
}
type ScrollMainPressureSnapshot = {
@ -22,6 +38,7 @@ type ScrollAckGateSnapshot = {
}
const TIMER_SAMPLE_MS = 16
const SLOW_SCROLL_DIAGNOSTIC_MS = 150
export async function seedActiveTerminalScrollback(
page: Page,
@ -38,28 +55,7 @@ export async function seedActiveTerminalScrollback(
await scrollActiveTerminalToBottom(page)
}
export async function scrollActiveTerminalToBottom(page: Page): Promise<void> {
await page.evaluate(() => {
const pane = (() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const candidate = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!candidate) {
throw new Error('Active terminal pane is unavailable')
}
return candidate
})()
pane.terminal.scrollToBottom()
})
}
export { scrollActiveTerminalToBottom }
export async function measureActiveTerminalWheelScroll(page: Page): Promise<ScrollMeasurement> {
const target = await page.evaluate(() => {
@ -120,45 +116,94 @@ export async function measureActiveTerminalWheelScroll(page: Page): Promise<Scro
}
}, TIMER_SAMPLE_MS)
const start = performance.now()
await page.mouse.move(target.x, target.y)
await page.mouse.wheel(0, -1200)
let afterViewportY = target.beforeViewportY
while (performance.now() - start < 75) {
afterViewportY = await readActiveTerminalViewportY(page)
if (afterViewportY < target.beforeViewportY) {
break
let watcherStopped = false
try {
const start = performance.now()
const attempts: ScrollAttemptMeasurement[] = []
let afterViewportY = await measureScrollAttempt(page, attempts, 'cdpWheel', async () => {
await page.mouse.move(target.x, target.y)
await page.mouse.wheel(0, -1200)
})
let scrollLatencyMs = performance.now() - start
const cdpWheelMoved = afterViewportY < target.beforeViewportY
if (cdpWheelMoved && scrollLatencyMs >= SLOW_SCROLL_DIAGNOSTIC_MS) {
await measureAdditionalScrollAttempts(page, attempts)
}
await page.waitForTimeout(5)
if (afterViewportY >= target.beforeViewportY) {
afterViewportY = await measureScrollAttempt(page, attempts, 'domWheel', async () => {
await dispatchActiveTerminalWheelEvent(page)
})
if (afterViewportY < target.beforeViewportY) {
scrollLatencyMs = performance.now() - start
}
}
if (afterViewportY >= target.beforeViewportY) {
afterViewportY = await measureScrollAttempt(page, attempts, 'domScroll', async () => {
await scrollActiveTerminalViewportElement(page)
})
if (afterViewportY < target.beforeViewportY) {
scrollLatencyMs = performance.now() - start
}
}
if (afterViewportY >= target.beforeViewportY) {
afterViewportY = await measureScrollAttempt(page, attempts, 'xtermApi', async () => {
await scrollActiveTerminalByApi(page)
})
if (afterViewportY < target.beforeViewportY) {
scrollLatencyMs = performance.now() - start
}
}
if (afterViewportY >= target.beforeViewportY) {
const remainingMs = Math.max(0, 500 - (performance.now() - start))
const finalState = await waitForActiveTerminalViewportChange(
page,
target.beforeViewportY,
remainingMs
)
afterViewportY = finalState.viewportY
const lastAttempt = attempts.at(-1)
if (lastAttempt) {
lastAttempt.afterViewportY = finalState.viewportY
lastAttempt.afterScrollTop = finalState.scrollTop
}
if (afterViewportY < target.beforeViewportY) {
scrollLatencyMs = performance.now() - start
}
}
const maxTimerDriftMs = await eventLoop.evaluate((watcher) => watcher.stop())
watcherStopped = true
return {
scrollLatencyMs,
maxTimerDriftMs,
beforeViewportY: target.beforeViewportY,
afterViewportY,
baseY: target.baseY,
attempts
}
} finally {
if (!watcherStopped) {
await eventLoop.evaluate((watcher) => watcher.stop()).catch(() => undefined)
}
await eventLoop.dispose().catch(() => undefined)
}
if (afterViewportY >= target.beforeViewportY) {
}
async function measureAdditionalScrollAttempts(
page: Page,
attempts: ScrollAttemptMeasurement[]
): Promise<void> {
await scrollActiveTerminalToBottom(page)
await measureScrollAttempt(page, attempts, 'domWheelAfterSlowCdp', async () => {
await dispatchActiveTerminalWheelEvent(page)
}
afterViewportY = await readActiveTerminalViewportY(page)
if (afterViewportY >= target.beforeViewportY) {
})
await scrollActiveTerminalToBottom(page)
await measureScrollAttempt(page, attempts, 'domScrollAfterSlowCdp', async () => {
await scrollActiveTerminalViewportElement(page)
}
afterViewportY = await readActiveTerminalViewportY(page)
if (afterViewportY >= target.beforeViewportY) {
})
await scrollActiveTerminalToBottom(page)
await measureScrollAttempt(page, attempts, 'xtermApiAfterSlowCdp', async () => {
await scrollActiveTerminalByApi(page)
}
while (performance.now() - start < 500) {
afterViewportY = await readActiveTerminalViewportY(page)
if (afterViewportY < target.beforeViewportY) {
break
}
await page.waitForTimeout(5)
}
const scrollLatencyMs = performance.now() - start
const maxTimerDriftMs = await eventLoop.evaluate((watcher) => watcher.stop())
await eventLoop.dispose()
return {
scrollLatencyMs,
maxTimerDriftMs,
beforeViewportY: target.beforeViewportY,
afterViewportY,
baseY: target.baseY
}
})
}
export function annotateScrollMeasurement(
@ -170,14 +215,22 @@ export function annotateScrollMeasurement(
ackGate: ScrollAckGateSnapshot | null
): void {
const scrollMoved = measurement.afterViewportY < measurement.beforeViewportY
const scrollMetric = scrollMoved ? ` scroll=${measurement.scrollLatencyMs.toFixed(1)}ms` : ''
const responsiveScroll = getResponsiveScrollPath(measurement)
const scrollMetric = responsiveScroll
? ` scroll=${responsiveScroll.latencyMs.toFixed(1)}ms scrollPath=${responsiveScroll.name}${
responsiveScroll.name === 'cdpWheel'
? ''
: ` cdpScroll=${measurement.scrollLatencyMs.toFixed(1)}ms`
}`
: ''
const attempts = formatScrollAttempts(measurement.attempts)
testInfo.annotations.push({
type,
description: `panes=${paneCount}${scrollMetric} scrollMoved=${scrollMoved} maxTimerDrift=${measurement.maxTimerDriftMs.toFixed(
1
)}ms viewportBefore=${measurement.beforeViewportY} viewportAfter=${
measurement.afterViewportY
} baseY=${measurement.baseY} mainPeakPendingChars=${
} baseY=${measurement.baseY} scrollAttempts=${attempts} mainPeakPendingChars=${
mainPressure?.peakPendingChars ?? 0
} mainPeakInFlightChars=${mainPressure?.peakRendererInFlightChars ?? 0} mainAckGatedFlushSkips=${
mainPressure?.ackGatedFlushSkipCount ?? 0
@ -187,111 +240,53 @@ export function annotateScrollMeasurement(
})
}
async function scrollActiveTerminalViewportElement(page: Page): Promise<void> {
await page.evaluate(() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
throw new Error('Active terminal pane is unavailable')
}
const viewport = pane.container.querySelector<HTMLElement>('.xterm-viewport')
if (!viewport) {
throw new Error('Active terminal viewport is unavailable')
}
// Why: Linux CI can drop wheel delivery entirely under PTY flood; changing
// the viewport scrollTop exercises xterm's DOM scroll synchronization.
viewport.scrollTop = Math.max(0, viewport.scrollTop - 1200)
viewport.dispatchEvent(new Event('scroll', { bubbles: true }))
async function measureScrollAttempt(
page: Page,
attempts: ScrollAttemptMeasurement[],
name: string,
action: () => Promise<void>
): Promise<number> {
const before = await readActiveTerminalScrollState(page)
let error: string | undefined
const actionStart = performance.now()
try {
await action()
} catch (caught) {
error = caught instanceof Error ? caught.message : String(caught)
}
const actionMs = performance.now() - actionStart
const afterAction = await readActiveTerminalScrollState(page)
const observeStart = performance.now()
const after = await waitForActiveTerminalViewportChange(page, before.viewportY, 75)
const observeMs = performance.now() - observeStart
attempts.push({
name,
actionMs,
observeMs,
beforeViewportY: before.viewportY,
afterActionViewportY: afterAction.viewportY,
afterViewportY: after.viewportY,
beforeScrollTop: before.scrollTop,
afterActionScrollTop: afterAction.scrollTop,
afterScrollTop: after.scrollTop,
error
})
return after.viewportY
}
async function scrollActiveTerminalByApi(page: Page): Promise<void> {
await page.evaluate(() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
throw new Error('Active terminal pane is unavailable')
async function waitForActiveTerminalViewportChange(
page: Page,
beforeViewportY: number,
timeoutMs: number
): Promise<ActiveTerminalScrollState> {
const start = performance.now()
let state = await readActiveTerminalScrollState(page)
while (performance.now() - start < timeoutMs) {
state = await readActiveTerminalScrollState(page)
if (state.viewportY < beforeViewportY) {
break
}
// Why: Linux/Xvfb can lose synthetic wheel/DOM scroll events under flood;
// xterm's public API keeps this probe about viewport responsiveness.
const targetLine = Math.max(0, pane.terminal.buffer.active.viewportY - 20)
pane.terminal.scrollToLine(targetLine)
})
}
async function dispatchActiveTerminalWheelEvent(page: Page): Promise<void> {
await page.evaluate(() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
throw new Error('Active terminal pane is unavailable')
}
// Why: CI can drop CDP wheel input while the active textarea is focused;
// dispatching on xterm's own surfaces still exercises its user scroll path.
const wheelTargets = [
pane.container.querySelector<HTMLElement>('.xterm'),
pane.container.querySelector<HTMLElement>('.xterm-viewport'),
pane.container.querySelector<HTMLElement>('.xterm-screen')
].filter((target): target is HTMLElement => Boolean(target))
if (wheelTargets.length === 0) {
throw new Error('Active terminal wheel target is unavailable')
}
for (const wheelTarget of wheelTargets) {
wheelTarget.dispatchEvent(
new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: -1200
})
)
}
})
}
async function readActiveTerminalViewportY(page: Page): Promise<number> {
return page.evaluate(() => {
const store = window.__store
const state = store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
throw new Error('Active terminal pane is unavailable')
}
return pane.terminal.buffer.active.viewportY
})
await page.waitForTimeout(5)
}
return state
}