perf: measure code completion over a 2,000-item list

The harness's probe can answer completion with generated items through the
command route; language server requests go through a route when one is
installed, not only under Tauri. The scenario opens the popup by typing,
narrows it letter by letter and moves the selection, with budgets for each.
Цей коміт міститься в:
l0ng-ai committed 2026-11-06 23:16:55 +08:00
1 джерело 95c501ea8a
коміт f33b56b92b
6 files changed
+131 -2

No files matched your search

+8
Переглянути файл
@@ -32,6 +32,14 @@
"typingAngular.mainThreadMsPerKey": 30,
"typingAngular.tbtMs": 50,
"completion.openMs": 150,
"completion.narrowP95Ms": 20,
"completion.narrowMissedFramePct": 2,
"completion.narrowMainThreadMsPerKey": 35,
"completion.arrowP95Ms": 20,
"completion.arrowMissedFramePct": 2,
"completion.tbtMs": 50,
"keyRepeat.p95Ms": 20,
"keyRepeat.maxMs": 33,
"keyRepeat.missedFramePct": 2,
+43
Переглянути файл
@@ -110,6 +110,48 @@ const typingAngular: Scenario = {
},
};
/**
* Code completion with a language server's 2,000 items: the first letter opens
* the popup (after CodeMirror's 100 ms typing delay), each further letter
* narrows it, arrows move the selection.
*/
const completion: Scenario = {
id: "completion",
title: "Code completion over 2,000 items",
async run(h) {
await h.page.evaluate(() =>
(window as never as { __openideaPerf: { fakeCompletion(n: number): void } }).__openideaPerf.fakeCompletion(2_000),
);
try {
await h.openFile(`${ROOT}/src/completion/Orders.ts`, tsSource(2_000, "completion"));
const st = await h.editorState();
await h.setCursor(Math.floor((st?.length ?? 0) / 2));
await h.parserIdle();
await h.page.keyboard.press("End");
await h.page.keyboard.press("Enter");
const refresh = await h.refreshMs();
const openMs = await h.measure(() => h.page.keyboard.press("g"), ".cm-tooltip-autocomplete li", "Result<Order>");
const t0 = await h.now();
const before = await h.counters();
const narrow = await h.typeMeasured("etOrd", 150);
const work = Harness.delta(before, await h.counters());
const arrows = await h.pressMeasured(Array(20).fill("ArrowDown"), 60);
await h.page.keyboard.press("Escape");
return {
openMs,
...keySummary(narrow, refresh, "narrow"),
narrowMainThreadMsPerKey: work.taskMs / 5,
...keySummary(arrows, refresh, "arrow"),
tbtMs: totalBlockingTime(await h.longTasksSince(t0)),
};
} finally {
await h.page.evaluate(() =>
(window as never as { __openideaPerf: { fakeCompletion(n: number): void } }).__openideaPerf.fakeCompletion(0),
);
}
},
};
/** Held key: auto-repeat at ~30 Hz must never fall behind. */
const keyRepeat: Scenario = {
id: "keyRepeat",
@@ -369,6 +411,7 @@ export const SCENARIOS: Scenario[] = [
typing,
typingLarge,
typingAngular,
completion,
keyRepeat,
scroll,
tabs,
+17 -1
Переглянути файл
@@ -3,13 +3,15 @@ import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { _addEditor, _removeEditor, documentSynced, type EditorDocContext } from "@/editor/api";
let tauri = true;
vi.mock("@/platform/ipc", async (orig) => {
const actual = await orig<typeof import("@/platform/ipc")>();
return { ...actual, isTauri: () => true };
return { ...actual, isTauri: () => tauri };
});
const { lspRequest, reportLspError } = await import("./lsp");
const { IpcError } = await import("@/platform/ipc");
const { setInvokeRoute } = await import("@/platform/tauriCore");
describe("lspRequest", () => {
it("waits until open editors' edits reached the backend", async () => {
@@ -27,6 +29,20 @@ describe("lspRequest", () => {
_removeEditor(handle);
view.destroy();
});
it("outside Tauri, goes through an installed command route only", async () => {
tauri = false;
try {
const run = vi.fn(async () => ({ status: "ok" as const, data: 2 }));
await expect(lspRequest(run)).rejects.toMatchObject({ kind: "unavailable" });
expect(run).not.toHaveBeenCalled();
setInvokeRoute(() => null);
expect(await lspRequest(run)).toBe(2);
} finally {
setInvokeRoute(null);
tauri = true;
}
});
});
describe("reportLspError", () => {
+3 -1
Переглянути файл
@@ -3,6 +3,7 @@ import { Facet } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import { whenAllSynced, type EditorDocContext } from "@/editor/api";
import { IpcError, call, isCancelled, isTauri, unavailableText } from "@/platform/ipc";
import { hasInvokeRoute } from "@/platform/tauriCore";
import { tx } from "@/platform/i18n";
import type { IdeError, ReferencesIncomplete } from "@/bindings";
import { showHint } from "./hint";
@@ -25,7 +26,8 @@ type Result<T> = { status: "ok"; data: T } | { status: "error"; error: IdeError
* meanwhile is not sent (the backend ignores cancelling an id it hasn't seen).
*/
export async function lspRequest<T>(run: () => Promise<Result<T>>, wanted?: () => boolean): Promise<T> {
if (!isTauri()) throw new IpcError({ kind: "unavailable", message: "No language server available" });
if (!isTauri() && !hasInvokeRoute())
throw new IpcError({ kind: "unavailable", message: "No language server available" });
try {
// Edits typed just before (⌘B right after typing) must reach the server first.
await whenAllSynced();
+5
Переглянути файл
@@ -19,6 +19,11 @@ export function setInvokeRoute(next: InvokeRoute | null) {
route = next;
}
/** Whether commands have somewhere to go besides this process's Tauri runtime. */
export function hasInvokeRoute(): boolean {
return route !== null;
}
export function invoke<T>(cmd: string, args?: InvokeArgs, options?: InvokeOptions): Promise<T> {
return (route?.(cmd, args, options) ?? invokeLocal<T>(cmd, args, options)) as Promise<T>;
}
+55
Переглянути файл
@@ -8,6 +8,8 @@ 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 { setInvokeRoute } from "@/platform/tauriCore";
import { editorService } from "./contributions";
export interface PerfProbe {
@@ -23,6 +25,48 @@ export interface PerfProbe {
parsing(): boolean;
/** Document length and caret of the active editor. */
editorState(): { path: string; length: number; lines: number; head: number } | null;
/**
* Answers code completion with `items` generated items (a language server's
* list); other language server requests report no server. `0` removes it.
*/
fakeCompletion(items: number): void;
}
const KINDS: CompletionKind[] = [
"method",
"function",
"field",
"variable",
"class",
"interface",
"property",
"keyword",
];
function completionItem(i: number): CompletionItem {
const kind = KINDS[i % KINDS.length]!;
// Shared prefixes so typing narrows the list gradually, like real symbols.
const label = `${["get", "set", "create", "update", "find", "to", "is", "on"][i % 8]}${["Order", "Item", "Customer", "Invoice", "Payment", "Status"][(i >> 3) % 6]}${i}`;
return {
label,
labelDetail: kind === "method" || kind === "function" ? "(id: string, options?: Options)" : null,
detail: kind === "class" || kind === "interface" ? `com.acme.${label}` : "Promise<Result<Order>>",
kind,
documentation: null,
sortText: String(i).padStart(5, "0"),
filterText: null,
preselect: false,
deprecated: i % 97 === 0,
insertText: label,
isSnippet: false,
replaceRange: null,
replaceRangeTab: null,
additionalEdits: [],
commitCharacters: [],
command: null,
data: null,
server: 1,
};
}
export function installPerfProbe(): () => void {
@@ -45,6 +89,17 @@ export function installPerfProbe(): () => void {
const view = activeEditor()?.view;
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;
});
},
editorState() {
const e = activeEditor();
if (!e) return null;