Skip to content

Commit e0e80ea

Browse files
authored
Merge pull request #175 from Shougo/copilot/optimize-cpu-spike-during-input
perf: cache keywordPattern/RegExp, parallelize filter resolution, batch denops RPCs
2 parents 9f5a7e9 + 816f014 commit e0e80ea

4 files changed

Lines changed: 43 additions & 13 deletions

File tree

denops/ddc/base/source.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
Previewer,
1111
SourceOptions,
1212
} from "../types.ts";
13-
import { convertKeywordPattern } from "../utils.ts";
13+
import { convertKeywordPattern, getKeywordRegExp } from "../utils.ts";
1414

1515
import type { Denops } from "@denops/std";
1616

@@ -106,7 +106,7 @@ export abstract class BaseSource<
106106
);
107107

108108
const completePos = args.context.input.search(
109-
new RegExp("(?:" + keywordPattern + ")$"),
109+
getKeywordRegExp("(?:" + keywordPattern + ")$"),
110110
);
111111
return completePos;
112112
}

denops/ddc/ddc.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import type { Denops } from "@denops/std";
3131
import * as autocmd from "@denops/std/autocmd";
3232
import * as op from "@denops/std/option";
3333
import * as fn from "@denops/std/function";
34-
import { batch } from "@denops/std/batch";
34+
import { batch, collect } from "@denops/std/batch";
3535

3636
import { assertEquals } from "@std/assert/equals";
3737

@@ -622,9 +622,12 @@ export class Ddc {
622622
return;
623623
}
624624

625-
const input = denops.call("ddc#util#get_input", context.event);
626-
const mode = fn.mode(denops);
627-
if (context.input !== await input || context.mode !== await mode) {
625+
const [currentInput, currentMode] = await collect(denops, (denops) => [
626+
// ddc#util#get_input always returns a string; cast for type inference.
627+
denops.call("ddc#util#get_input", context.event) as Promise<string>,
628+
fn.mode(denops),
629+
]);
630+
if (context.input !== currentInput || context.mode !== currentMode) {
628631
// Input is changed. Skip invalid completion.
629632
await this.hide(denops, context, options);
630633
return;

denops/ddc/ext.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,11 @@ export async function filterItems(
237237
userFilters: UserFilter[],
238238
items: Item[],
239239
): Promise<Item[]> {
240+
const resolvedList = await Promise.all(
241+
userFilters.map((uf) => getFilter(denops, loader, options, uf)),
242+
);
240243
const resolved: ResolvedFilter[] = [];
241-
for (const userFilter of userFilters) {
242-
const [filter, filterOptions, filterParams] = await getFilter(
243-
denops,
244-
loader,
245-
options,
246-
userFilter,
247-
);
244+
for (const [filter, filterOptions, filterParams] of resolvedList) {
248245
if (!filter) {
249246
return [];
250247
}

denops/ddc/utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import { fromFileUrl } from "@std/path/from-file-url";
1515
import { join } from "@std/path/join";
1616
import { dirname } from "@std/path/dirname";
1717

18+
// Cache size limit: in practice only a handful of distinct keywordPattern /
19+
// iskeyword combinations appear, so 64 entries is more than enough.
20+
const KEYWORD_CACHE_MAX = 64;
21+
const convertKeywordPatternCache = new Map<string, string>();
22+
const keywordRegExpCache = new Map<string, RegExp>();
23+
1824
export async function convertKeywordPattern(
1925
denops: Denops,
2026
keywordPattern: string,
@@ -23,13 +29,37 @@ export async function convertKeywordPattern(
2329
const iskeyword = bufnr === undefined
2430
? await op.iskeyword.getLocal(denops)
2531
: await op.iskeyword.getBuffer(denops, bufnr);
32+
// Neither iskeyword nor keywordPattern contain NUL bytes, so this
33+
// composite key is unambiguous.
34+
const cacheKey = keywordPattern + "\0" + iskeyword;
35+
const cached = convertKeywordPatternCache.get(cacheKey);
36+
if (cached !== undefined) {
37+
return cached;
38+
}
2639
const keyword = vimoption2ts(iskeyword);
2740
const replaced = keywordPattern
2841
.replaceAll("\\k", "[" + keyword + "]")
2942
.replaceAll("[:keyword:]", keyword);
43+
if (convertKeywordPatternCache.size >= KEYWORD_CACHE_MAX) {
44+
convertKeywordPatternCache.clear();
45+
}
46+
convertKeywordPatternCache.set(cacheKey, replaced);
3047
return replaced;
3148
}
3249

50+
export function getKeywordRegExp(expandedPattern: string): RegExp {
51+
const cached = keywordRegExpCache.get(expandedPattern);
52+
if (cached !== undefined) {
53+
return cached;
54+
}
55+
const re = new RegExp(expandedPattern);
56+
if (keywordRegExpCache.size >= KEYWORD_CACHE_MAX) {
57+
keywordRegExpCache.clear();
58+
}
59+
keywordRegExpCache.set(expandedPattern, re);
60+
return re;
61+
}
62+
3363
// See https://github.com/vim-denops/denops.vim/issues/358 for details
3464
export function isDenoCacheIssueError(e: unknown): boolean {
3565
const expects = [

0 commit comments

Comments
 (0)