Skip to content

perf: cache keywordPattern/RegExp, parallelize filter resolution, batch denops RPCs#175

Merged
Shougo merged 3 commits intomainfrom
copilot/optimize-cpu-spike-during-input
Apr 28, 2026
Merged

perf: cache keywordPattern/RegExp, parallelize filter resolution, batch denops RPCs#175
Shougo merged 3 commits intomainfrom
copilot/optimize-cpu-spike-during-input

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 27, 2026

Fast typing causes repeated redundant work on every keypress: iskeyword-derived string processing, fresh RegExp compilation, sequential filter loading, and two separate RPC round-trips in doCompletion.

Changes

denops/ddc/utils.ts — keyword pattern and RegExp caching

  • convertKeywordPattern now caches results keyed by keywordPattern + NUL + iskeyword, skipping vimoption2ts string processing on repeated calls (the common case)
  • New exported getKeywordRegExp(expandedPattern) caches compiled RegExp objects, avoiding re-compilation on every getCompletePosition call
  • Both caches cap at 64 entries and auto-clear to bound memory use

denops/ddc/base/source.ts — use RegExp cache in getCompletePosition

// Before
new RegExp("(?:" + keywordPattern + ")$")
// After
getKeywordRegExp("(?:" + keywordPattern + ")$")

denops/ddc/ext.ts — parallel filter resolution in callFilters

Sequential for await over getFilter calls replaced with Promise.all, so sorters/converters are resolved concurrently rather than one at a time.

denops/ddc/ddc.ts — batch RPC round-trips in doCompletion

Two sequential denops calls (ddc#util#get_input + fn.mode) merged into a single collect batch, eliminating one RPC round-trip per completion cycle.

// Before: two awaits, two round-trips
const input = denops.call("ddc#util#get_input", context.event);
const mode = fn.mode(denops);
if (context.input !== await input || context.mode !== await mode) {  }

// After: one round-trip
const [currentInput, currentMode] = await collect(denops, (denops) => [
  denops.call("ddc#util#get_input", context.event) as Promise<string>,
  fn.mode(denops),
]);
if (context.input !== currentInput || context.mode !== currentMode) {  }
Original prompt

目的

高速入力時の CPU スパイクをさらに抑えるため、以下の3点を一つの PR として実装・提出します。

変更項目(まとめ)

  1. convertKeywordPattern の結果キャッシュ化(denops/ddc/utils.ts)

    • convertKeywordPattern が毎回 op.iskeyword を取得してパターン生成を行っているため、同じ keywordPattern と iskeyword で何度も同一の文字列操作が発生している。結果をキャッシュして不要な再計算を防ぎます。
    • Map ベースのシンプルキャッシュを追加。将来的に LRU に置換可能。
    • 併せて、expand されたパターンに対する RegExp キャッシュも追加して getCompletePosition 側で使えるようにする。
  2. filterItems 内での getFilter の重複解決を回避(denops/ddc/ext.ts)

    • runMatchersConcurrently / callFilters の実装で同じ filter を複数チャンクで繰り返し取得している可能性があるため、UserFilter[] を事前に getFilter し resolvedFilters を作成して共有して使用するよう変更します。
    • これにより loader.getFilter / checkFilterOnInit の重複コストを削減します。
  3. doCompletion 内の denops RPC のバッチ化(denops/ddc/ddc.ts)

    • doCompletion で denops.call や fn.mode を個別に await している箇所をバッチ呼び出しにまとめて RPC ラウンドトリップを削減します(@denops/std/batch.collect を利用)。

対象ファイル(変更予定)

  • denops/ddc/utils.ts

    • convertKeywordPattern にキャッシュ Map を追加
    • export function getKeywordRegExp(keywordPatternExpanded: string): RegExp を追加して RegExp キャッシュを提供
  • denops/ddc/base/source.ts

    • getCompletePosition の RegExp 生成箇所を utils.getKeywordRegExp を使うように変更
  • denops/ddc/ext.ts

    • filterItems の callFilters / runMatchersConcurrently のロジックを調整し、先に resolvedFilters を作成して再利用する実装に変更
  • denops/ddc/ddc.ts

    • doCompletion 内の denops.call/ fn.mode 呼び出しを collect にまとめる(必要な import を追加)

実装の注意点

  • 既存の外部挙動には影響を与えないようデフォルト動作は維持します(autoCompleteDelay 等とは独立した改善)。
  • convertKeywordPattern のキャッシュキーは keywordPattern + iskeyword 文字列で構成する方針(変更が少ない想定)。
  • resolvedFilters の導入は並列化ロジック(matcherConcurrency)や filterOptions.parallelSafe の判定と整合するように行���。並列実行の安全判定は現行ロジックを維持しつつ、getFilter の重複取得だけを排除する。
  • collect の導入は @denops/std/batch への依存を加えるが、既に denops を使っているため互換性は保たれる想定。インポート文を追加します。

テスト/検証

  • 単体: convertKeywordPattern の既存テストがあるため(deno.test を利用)、キャッシュ導入後も同様に通ることを確認する。必要に応じてキャッシュに関する追加テストを追加。
  • パフォーマンス: 既存のベンチ手順(headless スクリプト)で master と本 PR の比較を行い、RegExp 生成回数・getFilter 呼び出し回数・RPC 回数の削減を確認する。

PR タイトル(提案)
"perf: cache keywordPattern and RegExp, pre-resolve filters and batch denops RPCs"

備考

  • 変更は小さな追加・置換で済む想定ですが、ext.ts の filterItems は若干複雑なので入念に型チェック・動作確認を行います。
  • ブランチは fix/perf-cache-resolve-batch を作成します。

以上の内容で PR を作成してよいですか?問題なければこのままブランチを作り実装・PR を作成します。

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: Shougo/ddc.vim のパフォーマンスを改善したいです。特にユーザーが素早く入力したときに CPU 使用率が上がってしまう問題があります

Assistant: 続行するには先ほどのツール呼び出しの許可ダイアログで応答していただく必要があるため、ご返答をお待ちしています。

User: @copilot Accepted Confirmation: Are you sure?

Assistant: 結論(まずやるべきこと)

  • まずは設定で緩和できる即効策を試し、同時に軽い計測(ログ追加+CPUモニタ)をしてホットスポットを特定します。その後、リポジトリを深掘りして具体的な設計変更案/PR(差分)を作ります。リポジトリ調査と PR 作成を進めてよいですか?

やることの流れ(短く)

  1. 今すぐ試せる緩和策を適用して効果を確認する。
  2. 軽い計測(ログ+CPUモニタ)でどの処理が頻発しているか把握する。
  3. リポジトリ横断で入力→補完までのフローを追跡し、CPUホットスポットを特定する(ソース/フィルタ/同期呼び出しなど)。
  4. 優先度付きの改善案を作り、差分(patch/PR)案を提示・作成する。

今すぐ試せる緩和策(優先度高)

  • 入力→補完トリガーに debounce(待ち)を入れる
    • 「最後のキー入力から X ms 経ってから補完処理を走らせる」方式。X は 50–200ms を試す。高速入力で補完を連続再計算しないようにするだけで CPU 使用は大きく下がります。
  • 候補数の上限を下げる(ソース側で返す候補数を制限)
    • 大量候補に対するフィルタ/ソートが高コストな場合に有効。まず 50–100 程度に制限して試す。
  • 重いフィルタ・ソートを同期で呼ばない/頻度を下げる
    • ソースや filter が毎入力で重い vim.fn 呼び出しや正規表現をやっているなら、非同期化または頻度削減を検討。
  • 補完のプレビュー/リスト更新の頻度を下げる
    • UI 更新(popup)の再描画コストを下げるため、更新条件を厳しくする(テキストが実質変わったら更新等)。

簡単な計測(ユーザー側で素早くできること)

  • CPUモニタ(端末側)
    • htop / top で nvim の CPU を観察。高速入力中にどのプロセスが何%使うか確認。PID を控える。
  • 軽いログ出力を ddc のキートリガー直後/候補取得前後に挿入して時間を取得する例(Vimscript):
    • 例(テスト用、実行前にバックアップを):
      " /tmp/ddc_prof.log に追記する簡易ログ
      function! s:log(msg)
        let l = printf('%s %s', reltimefloat(reltime()), a:msg)
        call writefile([l], '/tmp/ddc_prof.log', 'a')
      endfunction
      
      " 補完トリガー直後に呼ぶ例
      call s:log('on_input start')
      " → 候補取得前後にも log を入れる
      call s:log('gather_candidates start')
      " gather_candidates 終了時
      call s:log('gather_candidates end')
    • 上記ログを高速入力で取得し、差分(期間)を確認することで何が多く走っているか推定可能。
  • プロファイラ(Vim/Neovim の :profile)を使う(より詳細)
    • :profile start / :profile func / :profile file の手順で関数実行回数や時間を計測できます(Neovim でも利用可)。ただしプロファイリングはオーバーヘッドが大きいので注意。

推測されるホットスポット(優先的にチェックすべき箇所)

  • 毎キーで発生する処理
    • Insertモードのイベント → 毎キーごとに補完処理が起動される経路(autocmd / InsertCharPre 相当)。ここに debounce が無いと高頻度で処理が走る。
  • 各ソースの gather_candidates/同期的 API 呼び出し
    • 大きな I/O、vim.fn の重い操作(ファイルI/O、シェルコマンド実行、辞書走査など)を同期的にやっていると毎入力で大きな負荷。
  • フィルタ/ソート処理
    • 候補数が多いまま複雑な正規表現フィルタやスコア計算を毎入力でやると CPU を食う。
  • ジョブ(spawn)やプロセスの頻繁な再生成
    • 「入力ごとに外部 job を立ち上げて結果を待つ/キャンセルする」実装だと非常に重い。
  • GC(Vimscript の繰り返し生成)や Lua→Vim 握手の過剰な往復
    • 候補を大量に作って捨てるような実装は GC/alloc の負荷を増やす。

計測で見るべき指標

  • nvim プロセスの CPU%(短時間でのピーク値)
  • 単一入力サイクル(キー入力→補完表示まで)の経過時間(ms):ログで測定
  • 補完トリガー発生回数/秒(イベント数)
  • ソース毎の gather_candidates 実行回数と平均時間

改善案(優先度付き、設計観点)
短期(設定で済む/小変更)

  1. 入力デバウンス(50–200ms)を導入(最初に試す)。
    • 実装: Insert イベントのハンドラで timer を使い、既存タイマーをキャンセルして最後のタイマーが走ったときに補完処理を実行する。
  2. ソースの候補数上限を設定(例えば 100)。
  3. 高負荷ソースを一時的に無効化して影響を切り分ける。
  4. UI 更新(popup)条件を厳しくして再描画回数を減らす。

中期(コード修正:比較的小さなリファクタ)
5. ソースの同期 API を非同期化(可能なら job/async)に移行。
6. リクエストの coalescing(前のリクエストをキャンセルし、新しい入力に対して一度だけ計算する...

This pull request was created from Copilot chat.

Copilot AI changed the title [WIP] Optimize CPU spike during fast input by caching results perf: cache keywordPattern/RegExp, parallelize filter resolution, batch denops RPCs Apr 27, 2026
Copilot AI requested a review from Shougo April 27, 2026 10:06
@Shougo Shougo marked this pull request as ready for review April 28, 2026 11:19
@Shougo Shougo merged commit e0e80ea into main Apr 28, 2026
3 checks passed
@Shougo Shougo deleted the copilot/optimize-cpu-spike-during-input branch April 28, 2026 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants