Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
60d45aa
test: specify timeline layout transaction policy
Astro-Han May 21, 2026
81ff418
feat: add timeline layout transaction coordinator
Astro-Han May 21, 2026
c51aaf0
feat: tag timeline scroll commands with layout transactions
Astro-Han May 21, 2026
08c8e14
fix: route timeline resize through layout transactions
Astro-Han May 21, 2026
be676eb
perf: widen timeline stable band during layout transactions
Astro-Han May 21, 2026
0e78e8e
test: add transaction context to runtime CLS diagnostics
Astro-Han May 21, 2026
9942a11
test: verify timeline layout transaction gates
Astro-Han May 21, 2026
68e253d
fix(app): settle layout transactions asynchronously
Astro-Han May 21, 2026
38695fe
fix(app): cancel stale layout transaction fallbacks
Astro-Han May 21, 2026
ecebc11
test(app): stabilize long scroll perf reveal
Astro-Han May 21, 2026
13b7d86
fix(app): keep resize recovery inside layout transactions
Astro-Han May 21, 2026
10d4d4d
fix(app): bind dock resize recovery to layout transactions
Astro-Han May 21, 2026
5a77e0e
test(app): stabilize timeline perf history reveal
Astro-Han May 21, 2026
4f37a83
test(app): stabilize runtime CLS history reveal
Astro-Han May 21, 2026
d07e985
test(app): guard timeline e2e driver
Astro-Han May 21, 2026
455e7ff
test(app): tighten timeline e2e driver boundary
Astro-Han May 21, 2026
5163408
test(app): bound perf load-earlier clicks
Astro-Han May 21, 2026
235aa24
test(app): isolate timeline e2e boundary
Astro-Han May 21, 2026
91e5e15
test(app): preserve long scroll perf intent
Astro-Han May 21, 2026
868d76d
ci(app): confirm perf failures by scenario
Astro-Han May 21, 2026
1f4cf7c
ci(app): infer confirmed perf failures
Astro-Han May 21, 2026
478ee62
test(app): scope confirmed perf missing base checks
Astro-Han May 21, 2026
ace4b94
test(app): use shared timeline event
Astro-Han May 21, 2026
d697e08
test(app): keep perf driver event base compatible
Astro-Han May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions packages/app/e2e/perf/perf-probe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,22 +202,56 @@ async function readPromptText(page: Parameters<typeof snapshotPerfProbe>[0]) {
}

async function revealCachedSessionMessages(page: Parameters<typeof snapshotPerfProbe>[0], expectedCount: number) {
const messages = page.locator(sessionMessageItemSelector)
if ((await messages.count()) < expectedCount) {
for (let attempt = 0; attempt < 12; attempt += 1) {
const budget = await readTimelineDomBudget(page)
if (budget.totalRows >= expectedCount) return

await page.locator(scrollViewportSelector).first().hover()
await page.mouse.wheel(0, -2400)
await markTimelineWheelIntent(page, -2400)
await settleFrames(page, 2)
await scrollTimelineTo(page, 0)
await settleFrames(page, 2)
const loadEarlier = page.getByRole("button", { name: /Load earlier messages|加载更早的消息/i }).first()
await expect(loadEarlier).toBeVisible({ timeout: 30_000 })
await loadEarlier.click()
if (await loadEarlier.isVisible().catch(() => false))
await loadEarlier.click({ timeout: 1_000 }).catch(() => undefined)
try {
await expect
.poll(async () => (await readTimelineDomBudget(page)).totalRows, { timeout: 1_500 })
.toBeGreaterThanOrEqual(expectedCount)
return
} catch {
// Continue nudging the cached history window; final assertion below reports failure details.
}
}
await expect
.poll(async () => (await readTimelineDomBudget(page)).totalRows, { timeout: 30_000 })
.toBeGreaterThanOrEqual(expectedCount)
}

async function revealCachedSessionMessagesThroughDriver(page: Parameters<typeof snapshotPerfProbe>[0]) {
const event = await readTimelineDriverEvent()
await page.evaluate((event) => {
window.dispatchEvent(
new CustomEvent(event, {
detail: { action: "reveal-cached" },
}),
)
}, event)
}

async function readTimelineDriverEvent() {
try {
const timeline = await import("../../src/testing/timeline")
return timeline.timelineEvent
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
// The perf workflow copies this harness into the base checkout, where PR-only testing helpers may not exist yet.
if (message.includes("src/testing/timeline")) return "opencode:e2e:timeline"
throw error
}
}

async function scrollTimelineTo(page: Parameters<typeof snapshotPerfProbe>[0], top: number) {
const found = await page.evaluate(
({ top, scrollViewportSelector, turnListSelector }) => {
Expand All @@ -233,6 +267,20 @@ async function scrollTimelineTo(page: Parameters<typeof snapshotPerfProbe>[0], t
expect(found).toBe(true)
}

async function markTimelineWheelIntent(page: Parameters<typeof snapshotPerfProbe>[0], deltaY: number) {
const found = await page.evaluate(
({ deltaY, scrollViewportSelector, turnListSelector }) => {
const list = document.querySelector(turnListSelector)
const viewport = list?.closest(scrollViewportSelector)
if (!(viewport instanceof HTMLElement)) return false
viewport.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY }))
return true
},
{ deltaY, scrollViewportSelector, turnListSelector: sessionTurnListSelector },
)
expect(found).toBe(true)
}

async function hoverTimelineScrollLane(page: Parameters<typeof snapshotPerfProbe>[0]) {
const box = await page.locator(scrollViewportSelector).first().boundingBox()
expect(box).toBeTruthy()
Expand Down Expand Up @@ -282,12 +330,20 @@ async function revealLongScrollWindow(page: Parameters<typeof snapshotPerfProbe>
const budget = await readTimelineDomBudget(page)
if (budget.totalRows >= longScrollMinimumAvailableRows) return
await page.mouse.wheel(0, -2400)
await markTimelineWheelIntent(page, -2400)
await settleFrames(page, 2)
await scrollTimelineTo(page, 0)
await settleFrames(page, 2)
try {
await expect
.poll(async () => (await readTimelineDomBudget(page)).totalRows, { timeout: 1_500 })
.toBeGreaterThanOrEqual(longScrollMinimumAvailableRows)
} catch {
// Continue nudging the history window; final assertion below reports failure details.
}
}
await expect
.poll(async () => (await readTimelineDomBudget(page)).totalRows, { timeout: 1_000 })
.poll(async () => (await readTimelineDomBudget(page)).totalRows, { timeout: 10_000 })
.toBeGreaterThanOrEqual(longScrollMinimumAvailableRows)
}

Expand All @@ -300,6 +356,15 @@ async function installComposerPerfDriver(page: Parameters<typeof snapshotPerfPro
})
}

async function installTimelinePerfDriver(page: Parameters<typeof snapshotPerfProbe>[0]) {
const apply = () => {
const win = window as Window & { __opencode_e2e?: { timeline?: { enabled?: boolean } } }
win.__opencode_e2e = { ...win.__opencode_e2e, timeline: { enabled: true } }
}
await page.addInitScript(apply)
await page.evaluate(apply)
}

async function writeComposerDriver(
page: Parameters<typeof snapshotPerfProbe>[0],
sessionID: string,
Expand Down Expand Up @@ -587,6 +652,7 @@ test.describe("PR0.1 perf probe baseline", () => {
test("long-session-input-lag emits a 3-run JSON baseline", async ({ page, project }) => {
skipUnlessScenario("long-session-input-lag")
await installPerfProbe(page)
await installTimelinePerfDriver(page)
await applyPerfProfile(page, PERF_PROFILE)
await project.open()

Expand All @@ -597,6 +663,7 @@ test.describe("PR0.1 perf probe baseline", () => {
await page.goto(sessionPath(project.directory, session.id))
await expect(page.locator(sessionMessageItemSelector).first()).toBeVisible({ timeout: 30_000 })
await expect(page.locator(promptSelector).first()).toBeVisible({ timeout: 30_000 })
await revealCachedSessionMessagesThroughDriver(page)
await revealCachedSessionMessages(page, TIMELINE_RECOMPUTE_SEED_TURN_COUNT)

const prompt = page.locator(promptSelector).first()
Expand Down Expand Up @@ -825,6 +892,7 @@ test.describe("PR0.1 perf probe baseline", () => {
test.setTimeout(180_000)
await installComposerPerfDriver(page)
await installPerfProbe(page)
await installTimelinePerfDriver(page)
await applyPerfProfile(page, PERF_PROFILE)
await project.open()

Expand All @@ -833,6 +901,8 @@ test.describe("PR0.1 perf probe baseline", () => {
await withSession(project.sdk, `perf scroll long ${Date.now()}-${run}`, async (session) => {
await seedLongScrollSession(project, session.id, run)
await page.goto(sessionPath(project.directory, session.id))
await expect(page.locator(sessionMessageItemSelector).first()).toBeVisible({ timeout: 30_000 })
await revealCachedSessionMessagesThroughDriver(page)
await revealLongScrollWindow(page)
const budget = await readTimelineDomBudget(page)
expect(budget.totalRows).toBeGreaterThanOrEqual(longScrollMinimumAvailableRows)
Expand All @@ -845,6 +915,7 @@ test.describe("PR0.1 perf probe baseline", () => {
await expandLongScrollTodoDock(page)

await hoverTimelineScrollLane(page)
await markTimelineWheelIntent(page, -2400)
await scrollTimelineTo(page, 0)
await settleFrames(page, 4)
const atTop = await readTimelineMetrics(page)
Expand Down Expand Up @@ -909,6 +980,7 @@ test.describe("PR0.1 perf probe baseline", () => {
test("session-timeline-recompute emits a 3-run low-end JSON baseline", async ({ page, project }) => {
skipUnlessScenario("session-timeline-recompute")
await installPerfProbe(page)
await installTimelinePerfDriver(page)
await applyPerfProfile(page, PERF_PROFILE)
await project.open()

Expand All @@ -918,6 +990,8 @@ test.describe("PR0.1 perf probe baseline", () => {
await seedTimelineRecomputeSession(project, session.id)
await page.goto(sessionPath(project.directory, session.id))
await expect(page.locator(sessionMessageItemSelector).first()).toBeVisible({ timeout: 30_000 })
await revealCachedSessionMessagesThroughDriver(page)
await revealCachedSessionMessages(page, TIMELINE_RECOMPUTE_SEED_TURN_COUNT)
await expect.poll(async () => page.locator(sessionMessageItemSelector).count()).toBeGreaterThanOrEqual(8)
await resetPerfProbe(page)
await page.locator(scrollViewportSelector).first().hover()
Expand Down
79 changes: 79 additions & 0 deletions packages/app/e2e/perf/runtime-cls-gate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
sessionTurnListSelector,
} from "../selectors"
import { sessionPath } from "../utils"
import { timelineEvent, type TimelineWindow } from "../../src/testing/timeline"
import { readTimelineDomBudget } from "./timeline-dom-budget"
import {
collectRuntimeClsFailures,
Expand Down Expand Up @@ -129,6 +130,20 @@ async function moveMouseOverTimeline(page: Page) {
await page.mouse.move(box.x + box.width / 2, box.y + Math.min(140, box.height * 0.25))
}

async function markTimelineWheelIntent(page: Page, deltaY: number) {
const found = await page.evaluate(
({ deltaY, scrollViewportSelector, turnListSelector }) => {
const list = document.querySelector(turnListSelector)
const viewport = list?.closest(scrollViewportSelector)
if (!(viewport instanceof HTMLElement)) return false
viewport.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY }))
return true
},
{ deltaY, scrollViewportSelector, turnListSelector: sessionTurnListSelector },
)
expect(found).toBe(true)
}

async function positionTimelineForMeasuredWindow(page: Page) {
// A tiny real wheel gesture marks the timeline as user-scrolled. Without
// that, the active question turn can briefly re-lock to bottom and the
Expand All @@ -153,6 +168,15 @@ async function revealRuntimeClsRows(page: Page) {
await expect(page.locator(sessionMessageItemSelector).first()).toBeVisible({ timeout: 30_000 })
})

await page.evaluate((eventName) => {
window.dispatchEvent(
new CustomEvent(eventName, {
detail: { action: "reveal-cached" },
}),
)
}, timelineEvent)
await settleFrames(page, 4)

for (let attempt = 0; attempt < 24; attempt += 1) {
const budget = await readTimelineDomBudget(page)
if (budget.totalRows >= RUNTIME_CLS_MINIMUM_ROWS) return budget
Expand All @@ -175,6 +199,37 @@ async function revealRuntimeClsRows(page: Page) {
return await readTimelineDomBudget(page)
}

async function revealRuntimeClsRowsNaturally(page: Page) {
await test.step("wait for first runtime CLS message", async () => {
await expect(page.locator(sessionMessageItemSelector).first()).toBeVisible({ timeout: 30_000 })
})

await moveMouseOverTimeline(page)
for (let attempt = 0; attempt < 24; attempt += 1) {
const budget = await readTimelineDomBudget(page)
if (budget.totalRows >= RUNTIME_CLS_MINIMUM_ROWS) return budget

await test.step(`naturally reveal runtime CLS rows attempt ${attempt + 1}`, async () => {
await page.mouse.wheel(0, -1200)
await markTimelineWheelIntent(page, -1200)
await settleFrames(page, 2)
await scrollTimelineToRatio(page, 0)
await settleFrames(page, 4)

const loadEarlier = page.getByRole("button", { name: /Load earlier messages|加载更早的消息/i }).first()
if (await loadEarlier.isVisible().catch(() => false)) {
await loadEarlier.click({ timeout: 1_000 }).catch(() => undefined)
await settleFrames(page, 4)
}
})
}

await expect
.poll(async () => (await readTimelineDomBudget(page)).totalRows, { timeout: 10_000 })
.toBeGreaterThanOrEqual(RUNTIME_CLS_MINIMUM_ROWS)
return await readTimelineDomBudget(page)
}

async function centerVisibleMessageID(page: Page) {
const target = await page.evaluate(
({ messageSelector, scrollViewportSelector }) => {
Expand Down Expand Up @@ -202,7 +257,17 @@ async function centerVisibleMessageID(page: Page) {
return target!
}

async function installTimelineRuntimeClsDriver(page: Page) {
const apply = () => {
const win = window as TimelineWindow
win.__opencode_e2e = { ...win.__opencode_e2e, timeline: { enabled: true } }
}
await page.addInitScript(apply)
await page.evaluate(apply)
}

async function prepareRuntimeClsWindow(page: Page, project: RuntimeClsProject, sessionID: string) {
await installTimelineRuntimeClsDriver(page)
await test.step("navigate to runtime CLS session", async () => {
await page.goto(sessionPath(project.directory, sessionID))
})
Expand Down Expand Up @@ -385,6 +450,20 @@ test.describe("runtime CLS probe lifecycle", () => {
test.describe("runtime CLS source gate", () => {
test.setTimeout(180_000)

test("natural history reveal smoke reaches a virtualized runtime CLS window", async ({ page, project }) => {
await project.open()
await withSession(project.sdk, `runtime cls natural reveal ${Date.now()}`, async (session) => {
await seedRuntimeClsSession(project, session.id)
await page.goto(sessionPath(project.directory, session.id))

const budget = await revealRuntimeClsRowsNaturally(page)

expect(budget.totalRows).toBeGreaterThanOrEqual(RUNTIME_CLS_MINIMUM_ROWS)
expect(budget.hasVirtualizer).toBe(true)
expect(budget.mountedMessages).toBeLessThanOrEqual(RUNTIME_CLS_MAXIMUM_MOUNTED_MESSAGES)
})
})

test("composer growth does not move visible timeline primary sources", async ({ page, project }) => {
await installRuntimeClsProbe(page)
await project.open()
Expand Down
Loading
Loading