From f363f76e578f9d0df24bb61557e65385d7ed3464 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:38:45 +0800 Subject: [PATCH] perf: measure the Git Log and splitter dragging gitLog shows 20k generated commits served page by page through a fake vcs_log, walks the selection with the arrow keys and wheel-scrolls the list. resizePanels drags the side and bottom splitters with an editor, the Project tree and the Log open. The perf probe's fake backends can now be combined. --- perf/budgets.json | 10 ++++ perf/scenarios.ts | 99 ++++++++++++++++++++++++++++++++++++++ src/workbench/perfProbe.ts | 88 +++++++++++++++++++++++++++++---- 3 files changed, 188 insertions(+), 9 deletions(-) diff --git a/perf/budgets.json b/perf/budgets.json index 31f13c7e7..d42ef7103 100644 --- a/perf/budgets.json +++ b/perf/budgets.json @@ -46,6 +46,16 @@ "consoleOutput.keyMissedFramePct": 10, "consoleOutput.tbtMs": 50, + "gitLog.showMs": 150, + "gitLog.droppedFramesPct": 2, + "gitLog.tbtMs": 50, + "gitLog.arrowKeyP95Ms": 20, + "gitLog.arrowKeyMissedFramePct": 5, + + "resizePanels.droppedFramesPct": 2, + "resizePanels.bottomDroppedFramesPct": 2, + "resizePanels.tbtMs": 50, + "keyRepeat.p95Ms": 20, "keyRepeat.maxMs": 33, "keyRepeat.missedFramePct": 2, diff --git a/perf/scenarios.ts b/perf/scenarios.ts index 951e9ce6c..9feba524f 100644 --- a/perf/scenarios.ts +++ b/perf/scenarios.ts @@ -267,6 +267,103 @@ const scroll: Scenario = { }, }; +/** + * Git Log with 20,000 commits on branching lanes, loaded page by page as the + * backend does: showing it, walking the selection with the arrow keys (the + * details pane follows) and wheel-scrolling through it. + */ +const gitLog: Scenario = { + id: "gitLog", + title: "Show, navigate and scroll a 20k-commit Git Log", + async run(h) { + await h.page.evaluate(() => + (window as never as { __openideaPerf: { fakeGitLog(n: number): void } }).__openideaPerf.fakeGitLog(20_000), + ); + const showMs = await h.measure( + () => + h.page.evaluate(() => + (window as never as { __openideaPerf: { runAction(id: string): Promise } }).__openideaPerf.runAction( + "vcs.showLog", + ), + ), + '[role="listbox"][aria-label="Commits"]', + "order flow step 0", + ); + const refresh = await h.refreshMs(); + const list = h.page.getByRole("listbox", { name: "Commits" }); + await list.getByRole("option").first().click(); + const keyLat = await h.pressMeasured(Array(40).fill("ArrowDown"), 50); + const box = (await list.boundingBox())!; + await h.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + const t0 = await h.now(); + await h.inPage((p) => p.startFrames()); + for (let i = 0; i < 120; i++) { + await h.page.mouse.wheel(0, i < 80 ? 400 : -400); + await h.page.waitForTimeout(16); + } + await h.page.waitForTimeout(200); + const frames = await h.inPage((p) => p.stopFrames()); + return { + showMs, + ...frameSummary(frames, refresh), + tbtMs: totalBlockingTime(await h.longTasksSince(t0)), + ...keySummary(keyLat, refresh, "arrowKey"), + }; + }, +}; + +/** + * Dragging the splitters with an editor, the Project tree and the Git Log + * open: every pointer move resizes the islands, so each frame relays them out. + */ +const resizePanels: Scenario = { + id: "resizePanels", + title: "Drag the tool window splitters", + async run(h) { + await h.openFile(SAMPLE); + await h.page.evaluate(() => + (window as never as { __openideaPerf: { fakeGitLog(n: number): void } }).__openideaPerf.fakeGitLog(2_000), + ); + await h.measure( + () => + h.page.evaluate(() => + (window as never as { __openideaPerf: { runAction(id: string): Promise } }).__openideaPerf.runAction( + "vcs.showLog", + ), + ), + '[role="listbox"][aria-label="Commits"]', + "order flow step 0", + ); + const refresh = await h.refreshMs(); + const drag = async (name: string, dx: number, dy: number) => { + const box = (await h.page.getByRole("separator", { name }).boundingBox())!; + const x = box.x + box.width / 2; + const y = box.y + box.height / 2; + await h.page.mouse.move(x, y); + await h.page.mouse.down(); + await h.inPage((p) => p.startFrames()); + // Out and back twice, one pointer move a frame. + for (let i = 0; i < 120; i++) { + const t = Math.sin((i / 60) * Math.PI); + await h.page.mouse.move(x + dx * t, y + dy * t); + await h.page.waitForTimeout(16); + } + await h.page.mouse.up(); + await h.page.waitForTimeout(200); + return frameSummary(await h.inPage((p) => p.stopFrames()), refresh); + }; + const t0 = await h.now(); + const side = await drag("Resize left tool window", 300, 0); + const bottom = await drag("Resize bottom tool window", 0, -250); + return { + ...side, + tbtMs: totalBlockingTime(await h.longTasksSince(t0)), + bottomFrameP95Ms: bottom.frameP95Ms, + bottomDroppedFramesPct: bottom.droppedFramesPct, + }; + }, +}; + /** Cold load: navigation → Project tree painted. */ const startup: Scenario = { id: "startup", @@ -453,6 +550,8 @@ export const SCENARIOS: Scenario[] = [ typingAngular, completion, consoleOutput, + gitLog, + resizePanels, keyRepeat, scroll, tabs, diff --git a/src/workbench/perfProbe.ts b/src/workbench/perfProbe.ts index 071664ef3..1b84eefdf 100644 --- a/src/workbench/perfProbe.ts +++ b/src/workbench/perfProbe.ts @@ -8,7 +8,7 @@ import { executeAction } from "@/platform/actions"; import { bindingsFor } from "@/platform/keymap"; import { activeEditor } from "@/editor/api"; import { docBackend, type MemoryBackend } from "@/editor/core/backend"; -import type { CompletionItem, CompletionKind } from "@/bindings"; +import type { Commit, CompletionItem, CompletionKind, LogQuery } from "@/bindings"; import { setInvokeRoute } from "@/platform/tauriCore"; import { editorService } from "./contributions"; @@ -30,6 +30,11 @@ export interface PerfProbe { * list); other language server requests report no server. `0` removes it. */ fakeCompletion(items: number): void; + /** + * Answers the Git Log with `commits` generated commits (a main line with + * feature branches merging into it), paged like the backend. `0` removes it. + */ + fakeGitLog(commits: number): void; } const KINDS: CompletionKind[] = [ @@ -69,6 +74,52 @@ function completionItem(i: number): CompletionItem { }; } +function commit(i: number, count: number): Commit { + const id = (n: number) => n.toString(16).padStart(40, "0"); + // Newest first: parents have higher indices. Every 12th commit merges a branch + // started 7 commits back; every third commit skips ahead, keeping a few lanes open. + const merge = i % 12 === 0 && i + 7 < count; + const next = i % 3 === 2 && i + 3 < count ? i + 3 : i + 1; + const parents = i + 1 >= count ? [] : merge ? [id(i + 1), id(i + 7)] : [id(next)]; + const time = Date.UTC(2026, 10, 5) - i * 3_600_000; + const summary = `${["feat", "fix", "refactor", "test"][i % 4]}(orders): ${merge ? "merge" : "update"} order flow step ${i}`; + return { + id: id(i), + shortId: id(i).slice(-8), + parents, + author: ["Ada Lovelace", "Linus Torvalds", "Grace Hopper", "Ken Thompson"][i % 4]!, + email: `dev${i % 4}@acme.dev`, + committer: "CI", + committerEmail: "ci@acme.dev", + timeMs: time, + commitTimeMs: time, + summary, + message: summary, + messageTruncated: false, + refs: i === 0 ? ["HEAD -> main", "origin/main"] : i % 250 === 0 ? [`tag: v1.${count - i}`] : [], + }; +} + +type Route = (cmd: string, args: unknown) => Promise | null; +const routes = new Map(); + +/** Installs (or with `null` removes) one fake backend; the first that answers a command wins. */ +function setRoute(name: string, next: Route | null) { + if (next) routes.set(name, next); + else routes.delete(name); + setInvokeRoute( + routes.size + ? (cmd, args) => { + for (const r of routes.values()) { + const answer = r(cmd, args); + if (answer) return answer; + } + return null; + } + : null, + ); +} + export function installPerfProbe(): () => void { const probe: PerfProbe = { seed(path, text) { @@ -90,15 +141,34 @@ export function installPerfProbe(): () => void { return !!view && syntaxParserRunning(view); }, fakeCompletion(count) { - if (!count) return setInvokeRoute(null); const items = Array.from({ length: count }, (_, i) => completionItem(i)); - setInvokeRoute((cmd, args) => { - if (cmd === "lsp_completion") return Promise.resolve({ isIncomplete: false, items }); - if (cmd === "lsp_resolve_completion") return Promise.resolve((args as { item: CompletionItem }).item); - if (cmd.startsWith("lsp_")) - return Promise.reject({ kind: "unavailable", message: "No language server available" }); - return null; - }); + setRoute( + "completion", + count + ? (cmd, args) => { + if (cmd === "lsp_completion") return Promise.resolve({ isIncomplete: false, items }); + if (cmd === "lsp_resolve_completion") return Promise.resolve((args as { item: CompletionItem }).item); + if (cmd.startsWith("lsp_")) + return Promise.reject({ kind: "unavailable", message: "No language server available" }); + return null; + } + : null, + ); + }, + fakeGitLog(count) { + const commits = Array.from({ length: count }, (_, i) => commit(i, count)); + setRoute( + "gitLog", + count + ? (cmd, args) => { + if (cmd !== "vcs_log") return null; + const { skip, limit, range } = (args as { query: LogQuery }).query; + // Outgoing commits (`@{u}..HEAD`) and the like: none. + if (range) return Promise.resolve([]); + return Promise.resolve(commits.slice(skip, limit ? skip + limit : undefined)); + } + : null, + ); }, editorState() { const e = activeEditor();