fix(vcs): keep Git Log rows painted while dragging the scrollbar (#1914)

Dragging the Git Log scrollbar through a long history stuttered and showed
blank rows: the native scrollbar scrolls asynchronously in WebKit, so the
virtual list rendered rows a frame or more after the new offset was painted.

The list now uses a scrollbar drawn in the DOM (`Scrollbar` in `src/ui`) that
sets the offset from pointer events and notifies scroll listeners at once, so
the virtualizer renders the matching rows before the next paint. Rows are
keyed by slot so a large jump updates the mounted rows instead of rebuilding
them, overscan is lowered, and rows rendered mid-scroll draw their graph
without edge hit targets, which come back once scrolling stops.

Adds a `gitLogScrollbar` perf scenario with budgets.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-07 12:14:09 +08:00
1 джерело 159c2d6fd2
коміт b99fe1704b
8 files changed
+414 -46

No files matched your search

+3
Переглянути файл
@@ -51,6 +51,9 @@
"gitLog.tbtMs": 50,
"gitLog.arrowKeyP95Ms": 20,
"gitLog.arrowKeyMissedFramePct": 5,
"gitLogScrollbar.frameP95Ms": 17.5,
"gitLogScrollbar.droppedFramesPct": 2,
"gitLogScrollbar.tbtMs": 50,
"resizePanels.droppedFramesPct": 2,
"resizePanels.bottomDroppedFramesPct": 2,
+47
Переглянути файл
@@ -316,6 +316,52 @@ const gitLog: Scenario = {
},
};
/** Scrollbar-sized jumps across a loaded graph, rather than incremental wheel scrolling. */
const gitLogScrollbar: Scenario = {
id: "gitLogScrollbar",
title: "Drag through a loaded 6k-commit Git Log",
async run(h) {
await h.page.evaluate(async () => {
const probe = (
window as never as {
__openideaPerf: {
fakeGitLog(n: number): void;
runAction(id: string): Promise<boolean>;
};
}
).__openideaPerf;
probe.fakeGitLog(6000);
await probe.runAction("vcs.showLog");
});
const list = h.page.getByRole("listbox", { name: "Commits" });
await list.waitFor();
await list.evaluate(async (element) => {
for (let i = 0; i < 300 && element.scrollHeight < 6000 * 26; i++) {
element.scrollTop = element.scrollHeight;
await new Promise(requestAnimationFrame);
}
if (element.scrollHeight < 6000 * 26) throw new Error("Git Log did not finish loading");
element.scrollTop = 0;
});
await h.page.waitForTimeout(200);
const refresh = await h.refreshMs();
const t0 = await h.now();
await h.inPage((p) => p.startFrames());
await list.evaluate(async (element) => {
const max = element.scrollHeight - element.clientHeight;
for (let i = 0; i < 180; i++) {
element.scrollTop = ((i < 90 ? i : 180 - i) / 90) * max;
await new Promise(requestAnimationFrame);
}
});
const frames = await h.inPage((p) => p.stopFrames());
return {
...frameSummary(frames, refresh),
tbtMs: totalBlockingTime(await h.longTasksSince(t0)),
};
},
};
/**
* 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.
@@ -555,6 +601,7 @@ export const SCENARIOS: Scenario[] = [
completion,
consoleOutput,
gitLog,
gitLogScrollbar,
resizePanels,
keyRepeat,
scroll,