mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-10 07:11:25 +03:00
test(app): bench dragging the review's edge
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import type { Page } from "@playwright/test"
|
||||
|
||||
/** A divider to drag: its centre, how far it can travel each way (px), and the display's frame period (ms). */
|
||||
export type Divider = { x: number; y: number; left: number; right: number; period: number }
|
||||
|
||||
type Leg = { seconds: number; at: (progress: number) => number }
|
||||
|
||||
const ease = (from: number, to: number, seconds: number): Leg => ({
|
||||
seconds,
|
||||
at: (progress) => from + ((to - from) * (1 - Math.cos(Math.PI * progress))) / 2,
|
||||
})
|
||||
|
||||
// Whole cycles, so a jiggle ends where it began.
|
||||
const jiggle = (around: number, amplitude: number, cycles: number, seconds: number): Leg => ({
|
||||
seconds,
|
||||
at: (progress) => around + amplitude * Math.sin(2 * Math.PI * cycles * progress),
|
||||
})
|
||||
|
||||
/**
|
||||
* How a hand drags a divider, as offsets from where the drag starts against time, scaled by the room each way: a slow
|
||||
* stretch one way, a jiggle there, a fast throw the other way, a slow pull back, a small jiggle and home. 4.95 s.
|
||||
*/
|
||||
function legs(divider: Divider) {
|
||||
const l = -divider.left
|
||||
const r = divider.right
|
||||
|
||||
return [
|
||||
ease(0, 0.8 * l, 1.2),
|
||||
jiggle(0.8 * l, 0.15 * divider.left, 3, 0.75),
|
||||
ease(0.8 * l, r, 0.6),
|
||||
ease(r, l, 1.5),
|
||||
ease(l, 0.4 * l, 0.4),
|
||||
jiggle(0.4 * l, 0.1 * divider.left, 3, 0.5),
|
||||
ease(0.4 * l, 0, 0.5),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Presses the divider, moves the pointer once per display frame along the drag above without letting go, and releases
|
||||
* it where it started. strobe's driver has no pointer drag (`page.move` does not hold a button, and its `until:
|
||||
* "frame"` waits two animation frames and a round trip per move), so Playwright's mouse drives the drag, from the
|
||||
* runner, paced by the display's frame period on the wall clock. A frame the page misses is skipped, as a hand keeps
|
||||
* moving while the screen lags.
|
||||
*/
|
||||
export async function dragDivider(mouse: Page["mouse"], divider: Divider) {
|
||||
const path = legs(divider)
|
||||
const total = path.reduce((sum, leg) => sum + leg.seconds, 0)
|
||||
|
||||
const offset = (seconds: number) => {
|
||||
let left = seconds
|
||||
|
||||
for (const leg of path) {
|
||||
if (left <= leg.seconds) return leg.at(left / leg.seconds)
|
||||
left -= leg.seconds
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
await mouse.move(divider.x, divider.y)
|
||||
await mouse.down()
|
||||
const began = performance.now()
|
||||
|
||||
for (let frame = 1; ; frame++) {
|
||||
const wait = began + frame * divider.period - performance.now()
|
||||
|
||||
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait))
|
||||
frame = Math.max(frame, Math.floor((performance.now() - began) / divider.period))
|
||||
const seconds = (performance.now() - began) / 1000
|
||||
|
||||
if (seconds >= total) break
|
||||
await mouse.move(divider.x + offset(seconds), divider.y)
|
||||
}
|
||||
|
||||
await mouse.move(divider.x, divider.y)
|
||||
await mouse.up()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads, in the page, where the divider `selector` sits, the room its panel `panel` has to shrink to `min` and grow to
|
||||
* the row `row` less `rest` (px), and the display's frame period over 30 frames. Only in setup: it reads layout.
|
||||
*/
|
||||
export function measureDivider(input: { selector: string; panel: string; row: string; min: number; rest: number }) {
|
||||
return new Promise<Divider>((resolve) => {
|
||||
const box = document.querySelector(input.selector)!.getBoundingClientRect()
|
||||
const width = document.querySelector(input.panel)!.getBoundingClientRect().width
|
||||
const row = document.querySelector(input.row)!.getBoundingClientRect().width
|
||||
const times: number[] = []
|
||||
|
||||
const tick = (time: number) => {
|
||||
times.push(time)
|
||||
|
||||
if (times.length < 31) return requestAnimationFrame(tick)
|
||||
resolve({
|
||||
x: box.left + box.width / 2,
|
||||
y: box.top + box.height / 2,
|
||||
left: Math.max(0, Math.min(360, width - input.min - 10)),
|
||||
right: Math.max(0, Math.min(160, row - input.rest - width - 10)),
|
||||
period: (times[30]! - times[0]!) / 30,
|
||||
})
|
||||
}
|
||||
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// One long session (120 turns of markdown, code fences, tool calls and diffs) at normal speed.
|
||||
import { bench, web } from "@hona/strobe"
|
||||
import { editTools } from "./fixtures/diffs"
|
||||
import { editTools, reviewDiffs } from "./fixtures/diffs"
|
||||
import { dragDivider, measureDivider, type Divider } from "./fixtures/drag"
|
||||
import { cached } from "./fixtures/mock"
|
||||
import { openReview } from "./fixtures/review"
|
||||
import { composer, mockTimelineSession, oldestPrompt, timeline } from "./fixtures/timeline"
|
||||
|
||||
const find = '[data-component="timeline-search-bar"] input'
|
||||
@@ -13,25 +15,47 @@ const edit = '[data-scope="apply-patch"] [data-slot="accordion-trigger"]'
|
||||
|
||||
const diff = '[data-component="apply-patch-file-diff"] diffs-container >>> [data-line]'
|
||||
|
||||
const drag = "drag the review's edge"
|
||||
|
||||
// The divider between the session and the side panel, read once the review is open. The session panel keeps at least
|
||||
// 400px, the side panel 320px and the 8px gap.
|
||||
let divider: Divider | undefined
|
||||
|
||||
export default bench({
|
||||
targets: {
|
||||
web: web({
|
||||
viewport: { width: 1366, height: 768 },
|
||||
prepare: async ({ case: name, playwright, mock, url }) =>
|
||||
mockTimelineSession(cached(playwright.page, mock), {
|
||||
prepare: async ({ case: name, playwright, mock, url }) => ({
|
||||
...(await mockTimelineSession(cached(playwright.page, mock), {
|
||||
server: new URL(url).origin,
|
||||
// The edits alone, so the case measures the diff, not the history around it.
|
||||
historyTurns: name === "expand an edit" ? 0 : 120,
|
||||
collapsedEdits: name === "expand an edit",
|
||||
tools: name === "expand an edit" ? await editTools() : undefined,
|
||||
}),
|
||||
vcsDiff: name === drag ? reviewDiffs() : undefined,
|
||||
})),
|
||||
mouse: playwright.page.mouse,
|
||||
}),
|
||||
url: ({ prepared }) => prepared.path,
|
||||
}),
|
||||
},
|
||||
|
||||
async setup({ page }) {
|
||||
async setup({ case: name, page }) {
|
||||
await page.waitFor(timeline, { timeout: 60_000 })
|
||||
await page.waitFor(composer)
|
||||
|
||||
if (name === drag) {
|
||||
await openReview(page)
|
||||
await page.idle()
|
||||
divider = await page.eval(measureDivider, {
|
||||
selector: '[data-slot="session-chat-panel"] > div > [data-component="resize-handle"]',
|
||||
panel: '[data-slot="session-chat-panel"]',
|
||||
row: "[data-pane-row]",
|
||||
min: 400,
|
||||
rest: 328,
|
||||
})
|
||||
}
|
||||
|
||||
await page.idle()
|
||||
},
|
||||
|
||||
@@ -85,5 +109,16 @@ export default bench({
|
||||
await page.idle()
|
||||
},
|
||||
},
|
||||
|
||||
// The divider beside the review (72 changed files) held for five seconds and dragged as a hand does, a pointer
|
||||
// move every display frame: a slow stretch toward the session, jiggles, a fast throw toward the review and a slow
|
||||
// pull back (fixtures/drag.ts). The timeline narrows and widens on every frame, so the rows in view rewrap; each
|
||||
// frame should keep up with the display.
|
||||
[drag]: {
|
||||
primary: "frames.dropped.pct",
|
||||
metrics: ["main.work", "main.layout", "main.style", "main.script", "frames.interval.p95"],
|
||||
run: ({ prepared }) => dragDivider(prepared.mouse, divider!),
|
||||
reset: ({ page }) => page.idle(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user