дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
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.
Цей коміт міститься в:
1 джерело
95c501ea8a
коміт
f33b56b92b
6 files changed
+131
-2
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Заблокувати користувача