Slice synthetic OpenCode pane injection (#4827)
This commit is contained in:
parent
13c31fdce1
commit
7cf445778d
|
|
@ -0,0 +1,134 @@
|
|||
import type { Page } from '@stablyai/playwright-test'
|
||||
|
||||
type SyntheticOpenCodeInjectionWindow = Window & {
|
||||
__terminalPtyDataInjection?: {
|
||||
inject: (paneKey: string, data: string) => boolean
|
||||
}
|
||||
__syntheticOpenCodeLoadState?: SyntheticOpenCodeLoadState
|
||||
}
|
||||
|
||||
type SyntheticOpenCodeLoadState = {
|
||||
errorMessage?: string
|
||||
intervalTimer?: number
|
||||
pendingTimers: number[]
|
||||
stopped: boolean
|
||||
}
|
||||
|
||||
const SYNTHETIC_PANES_PER_TIMER_TASK = 4
|
||||
|
||||
export async function startSyntheticOpenCodeInjection({
|
||||
frameCount,
|
||||
intervalMs,
|
||||
page,
|
||||
paneKeys
|
||||
}: {
|
||||
frameCount: number
|
||||
intervalMs: number
|
||||
page: Page
|
||||
paneKeys: string[]
|
||||
}): Promise<{ stop: () => Promise<void> }> {
|
||||
await page.evaluate(
|
||||
({ frameCount, intervalMs, paneKeys, panesPerTimerTask }) => {
|
||||
const target = window as SyntheticOpenCodeInjectionWindow
|
||||
const injector = target.__terminalPtyDataInjection
|
||||
if (!injector) {
|
||||
throw new Error('terminal PTY data injection API is unavailable')
|
||||
}
|
||||
let frameIndex = 0
|
||||
const previousState = target.__syntheticOpenCodeLoadState
|
||||
if (previousState?.intervalTimer != null) {
|
||||
window.clearInterval(previousState.intervalTimer)
|
||||
}
|
||||
for (const timer of previousState?.pendingTimers ?? []) {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
const state: SyntheticOpenCodeLoadState = {
|
||||
pendingTimers: [],
|
||||
stopped: false
|
||||
}
|
||||
target.__syntheticOpenCodeLoadState = state
|
||||
state.intervalTimer = window.setInterval(() => {
|
||||
const frame = frameIndex
|
||||
frameIndex += 1
|
||||
for (let paneOffset = 0; paneOffset < paneKeys.length; paneOffset += panesPerTimerTask) {
|
||||
const paneBatch = paneKeys.slice(paneOffset, paneOffset + panesPerTimerTask)
|
||||
// Why: real PTY chunks arrive over several renderer tasks, but not
|
||||
// necessarily one browser timer per pane. Small batches keep the
|
||||
// harness from creating either one giant callback or timer storms.
|
||||
const timer = window.setTimeout(() => {
|
||||
state.pendingTimers = state.pendingTimers.filter((id) => id !== timer)
|
||||
if (state.stopped || state.errorMessage) {
|
||||
return
|
||||
}
|
||||
for (const [batchIndex, paneKey] of paneBatch.entries()) {
|
||||
const paneIndex = paneOffset + batchIndex
|
||||
try {
|
||||
const injected = injector.inject(
|
||||
paneKey,
|
||||
syntheticOpenCodeFrameSource(paneIndex, frame)
|
||||
)
|
||||
if (!injected) {
|
||||
state.errorMessage = `no PTY data injector registered for pane key ${paneKey}`
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
state.errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
state.pendingTimers.push(timer)
|
||||
}
|
||||
if (frameIndex >= frameCount && state.intervalTimer != null) {
|
||||
window.clearInterval(state.intervalTimer)
|
||||
delete state.intervalTimer
|
||||
}
|
||||
}, intervalMs)
|
||||
|
||||
function syntheticOpenCodeFrameSource(paneIndex: number, frame: number): string {
|
||||
const row = (frame % 18) + 4
|
||||
const spinner = ['|', '/', '-', '\\'][frame % 4]
|
||||
const body = `${'opencode '.repeat(10)}pane=${paneIndex} frame=${frame}`
|
||||
return [
|
||||
'\x1b[?2026h',
|
||||
'\x1b[?25l',
|
||||
`\x1b[1;2H\x1b[38;2;255;138;0m${spinner} OpenCode synthetic agent ${paneIndex}\x1b[0m`,
|
||||
`\x1b[${row};4H\x1b[38;2;231;237;247m${body.padEnd(118, '#')}\x1b[0m`,
|
||||
`\x1b[23;2H\x1b[38;2;106;169;255mstream ${String(frame).padStart(4, '0')} ${'#'.repeat(96)}\x1b[0m`,
|
||||
'\x1b[?2026l'
|
||||
].join('')
|
||||
}
|
||||
},
|
||||
{
|
||||
frameCount,
|
||||
intervalMs,
|
||||
paneKeys,
|
||||
panesPerTimerTask: SYNTHETIC_PANES_PER_TIMER_TASK
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
stop: async () => {
|
||||
await page.evaluate(() => {
|
||||
const target = window as SyntheticOpenCodeInjectionWindow
|
||||
const state = target.__syntheticOpenCodeLoadState
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
state.stopped = true
|
||||
if (state.intervalTimer != null) {
|
||||
window.clearInterval(state.intervalTimer)
|
||||
delete state.intervalTimer
|
||||
}
|
||||
for (const timer of state.pendingTimers) {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
const { errorMessage } = state
|
||||
delete target.__syntheticOpenCodeLoadState
|
||||
if (errorMessage) {
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from './helpers/terminal'
|
||||
import { runHiddenRealPtyPressureScenario } from './artificial-opencode-hidden-pressure-scenario'
|
||||
import { runMainPressureScenario } from './artificial-opencode-main-pressure-scenario'
|
||||
import { startSyntheticOpenCodeInjection } from './artificial-opencode-synthetic-injection'
|
||||
|
||||
type TerminalLoadPane = {
|
||||
paneKey: string
|
||||
|
|
@ -279,68 +280,6 @@ function median(values: number[]): number {
|
|||
return sorted[Math.floor(sorted.length / 2)] ?? 0
|
||||
}
|
||||
|
||||
async function startSyntheticOpenCodeInjection(
|
||||
page: Page,
|
||||
paneKeys: string[]
|
||||
): Promise<{ stop: () => Promise<void> }> {
|
||||
await page.evaluate(
|
||||
({ paneKeys, frameCount, intervalMs }) => {
|
||||
const target = window as SyntheticOpenCodeWindow & {
|
||||
__syntheticOpenCodeLoadTimer?: number
|
||||
}
|
||||
const injector = target.__terminalPtyDataInjection
|
||||
if (!injector) {
|
||||
throw new Error('terminal PTY data injection API is unavailable')
|
||||
}
|
||||
let frameIndex = 0
|
||||
target.__syntheticOpenCodeLoadTimer = window.setInterval(() => {
|
||||
for (const [paneIndex, paneKey] of paneKeys.entries()) {
|
||||
const injected = injector.inject(
|
||||
paneKey,
|
||||
syntheticOpenCodeFrameSource(paneIndex, frameIndex)
|
||||
)
|
||||
if (!injected) {
|
||||
throw new Error(`no PTY data injector registered for pane key ${paneKey}`)
|
||||
}
|
||||
}
|
||||
frameIndex += 1
|
||||
if (frameIndex >= frameCount && target.__syntheticOpenCodeLoadTimer != null) {
|
||||
window.clearInterval(target.__syntheticOpenCodeLoadTimer)
|
||||
delete target.__syntheticOpenCodeLoadTimer
|
||||
}
|
||||
}, intervalMs)
|
||||
|
||||
function syntheticOpenCodeFrameSource(paneIndex: number, frame: number): string {
|
||||
const row = (frame % 18) + 4
|
||||
const spinner = ['|', '/', '-', '\\'][frame % 4]
|
||||
const body = `${'opencode '.repeat(10)}pane=${paneIndex} frame=${frame}`
|
||||
return [
|
||||
'\x1b[?2026h',
|
||||
'\x1b[?25l',
|
||||
`\x1b[1;2H\x1b[38;2;255;138;0m${spinner} OpenCode synthetic agent ${paneIndex}\x1b[0m`,
|
||||
`\x1b[${row};4H\x1b[38;2;231;237;247m${body.padEnd(118, '#')}\x1b[0m`,
|
||||
`\x1b[23;2H\x1b[38;2;106;169;255mstream ${String(frame).padStart(4, '0')} ${'#'.repeat(96)}\x1b[0m`,
|
||||
'\x1b[?2026l'
|
||||
].join('')
|
||||
}
|
||||
},
|
||||
{ paneKeys, frameCount: FRAME_COUNT, intervalMs: FRAME_INTERVAL_MS }
|
||||
)
|
||||
return {
|
||||
stop: async () => {
|
||||
await page.evaluate(() => {
|
||||
const target = window as SyntheticOpenCodeWindow & {
|
||||
__syntheticOpenCodeLoadTimer?: number
|
||||
}
|
||||
if (target.__syntheticOpenCodeLoadTimer != null) {
|
||||
window.clearInterval(target.__syntheticOpenCodeLoadTimer)
|
||||
delete target.__syntheticOpenCodeLoadTimer
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function measureTypingDuringLoad(
|
||||
page: Page,
|
||||
scriptPath: string,
|
||||
|
|
@ -532,10 +471,12 @@ async function measureCrossWorkspaceTypingDuringHiddenLoad({
|
|||
const scriptPath = path.join(testRepoPath, `.orca-opencode-cross-${hiddenPaneCount}-${runId}.mjs`)
|
||||
writeInteractivePromptScript(scriptPath, runId)
|
||||
await resetTerminalPtyOutputDebug(orcaPage)
|
||||
const load = await startSyntheticOpenCodeInjection(
|
||||
orcaPage,
|
||||
hiddenPanes.map((pane) => pane.paneKey)
|
||||
)
|
||||
const load = await startSyntheticOpenCodeInjection({
|
||||
frameCount: FRAME_COUNT,
|
||||
intervalMs: FRAME_INTERVAL_MS,
|
||||
page: orcaPage,
|
||||
paneKeys: hiddenPanes.map((pane) => pane.paneKey)
|
||||
})
|
||||
try {
|
||||
const measurement = await measureTypingDuringLoad(orcaPage, scriptPath, typingPtyId, runId)
|
||||
const debug = await readTerminalPtyOutputDebug(orcaPage)
|
||||
|
|
@ -660,10 +601,12 @@ test.describe('Artificial OpenCode terminal load', () => {
|
|||
const scriptPath = path.join(testRepoPath, `.orca-opencode-typing-${runId}.mjs`)
|
||||
writeInteractivePromptScript(scriptPath, runId)
|
||||
await resetTerminalPtyOutputDebug(orcaPage)
|
||||
const load = await startSyntheticOpenCodeInjection(
|
||||
orcaPage,
|
||||
loadPanes.map((pane) => pane.paneKey)
|
||||
)
|
||||
const load = await startSyntheticOpenCodeInjection({
|
||||
frameCount: FRAME_COUNT,
|
||||
intervalMs: FRAME_INTERVAL_MS,
|
||||
page: orcaPage,
|
||||
paneKeys: loadPanes.map((pane) => pane.paneKey)
|
||||
})
|
||||
try {
|
||||
const measurement = await measureTypingDuringLoad(
|
||||
orcaPage,
|
||||
|
|
@ -733,10 +676,12 @@ test.describe('Artificial OpenCode terminal load', () => {
|
|||
const scriptPath = path.join(testRepoPath, `.orca-opencode-scale-${paneCount}-${runId}.mjs`)
|
||||
writeInteractivePromptScript(scriptPath, runId)
|
||||
await resetTerminalPtyOutputDebug(orcaPage)
|
||||
const load = await startSyntheticOpenCodeInjection(
|
||||
orcaPage,
|
||||
loadPanes.map((pane) => pane.paneKey)
|
||||
)
|
||||
const load = await startSyntheticOpenCodeInjection({
|
||||
frameCount: FRAME_COUNT,
|
||||
intervalMs: FRAME_INTERVAL_MS,
|
||||
page: orcaPage,
|
||||
paneKeys: loadPanes.map((pane) => pane.paneKey)
|
||||
})
|
||||
try {
|
||||
const measurement = await measureTypingDuringLoad(
|
||||
orcaPage,
|
||||
|
|
|
|||
Loading…
Reference in New Issue