Skip to content

Improve autoCompleteDelay: per-buffer debounce + early callback cancellation#173

Merged
Shougo merged 3 commits intomainfrom
copilot/add-autocomplete-delay-improvements
Apr 25, 2026
Merged

Improve autoCompleteDelay: per-buffer debounce + early callback cancellation#173
Shougo merged 3 commits intomainfrom
copilot/add-autocomplete-delay-improvements

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

Rapid input with autoCompleteDelay > 0 caused CPU spikes because cancelCompletion (and its hide/show UI overhead) was called on every keystroke, and the event lock was held for the entire delay duration blocking subsequent event processing.

Changes

denops/ddc/types.ts / denops/ddc/context.ts

  • Added bufNr: number to Context, populated from the already-fetched world.bufnr, to support per-buffer timer keying.

denops/ddc/app.ts

Early callback revocationcbContext.revoke() is now called in onEvent dispatcher before acquiring the lock. When new input arrives, any in-flight callback waits from the previous completion are cancelled immediately, letting the previous _onEvent unblock and release the lock sooner.

Per-buffer debounce timers — Replaces the single blocking cancelCompletion + setTimeout pattern with a Map<number, number> of per-buffer timers:

  • Each keystroke cancels the previous timer for that buffer and schedules a new one.
  • cancelCompletion is called once, only when the timer fires (user paused typing), eliminating repeated hide/show cycles during rapid input.
  • The timer callback acquires the lock for safe serialization with concurrent _onEvent calls.
  • autoCompleteDelay = 0 behaviour is unchanged.
// Before: blocks lock for the full delay, calls cancelCompletion on every keystroke
if (options.autoCompleteDelay > 0) {
  await ddc.cancelCompletion(denops, context, options);
  await new Promise((resolve) => setTimeout(resolve, options.autoCompleteDelay));
}

// After: debounce per buffer, lock released during delay
clearTimeout(autoCompleteTimers.get(context.bufNr));
const timerId = setTimeout(() => {
  autoCompleteTimers.delete(context.bufNr);
  lock.lock(async () => {
    await ddc.cancelCompletion(denops, context, options);
    // ...
    await ddc.doCompletion(denops, context, cbContext, options);
  });
}, options.autoCompleteDelay);
autoCompleteTimers.set(context.bufNr, timerId);
return;

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • dl.deno.land
    • Triggering command: /usr/bin/curl curl -s REDACTED (dns block)
  • jsr.io
    • Triggering command: /tmp/deno /tmp/deno check denops/ddc/types.ts denops/ddc/context.ts denops/ddc/app.ts (dns block)
    • Triggering command: /tmp/deno /tmp/deno check --no-lock denops/ddc/types.ts (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

目的

  • 高速入力時の CPU スパイクを抑えるため、autoCompleteDelay の挙動を改善しつつ「次の入力が来たら前の補完処理を確実にキャンセルする」機能を ddc.vim に PR として追加する。

変更内容(概要)

  1. dispatcher.onEvent の受信直後に既存の CallbackContext を revoke して、外部コールバック待ち(createOnCallback を使っている処理)を即時キャンセルする。これにより、次の入力が来たときに以前の補完処理が callback 待ちで足止めされているケースを早期に解除できます。

  2. autoCompleteDelay の実装を単一グローバルタイマーから per-buffer デバウンスタイマーに変更する。具体的には denops/ddc/app.ts にバッファ単位のタイマー管理 Map を追加し、複数バッファでの並行操作や干渉を避けます。

  3. autoCompleteDelay が 0 の場合は既存挙動を保ち、オプション互換性を維持します。

  4. タイマーが実際に発火したときに一度だけ ddc.cancelCompletion を呼んで補完をキャンセル → doCompletion を実行する方式に切り替え、素早い連続入力で cancelCompletion/hide の呼び出しが大量に発生するのを防ぎます。

対象ファイル

  • denops/ddc/app.ts

変更理由と逸失ケースの説明

  • 既存実装では _onEvent 内で autoCompleteDelay > 0 のときに毎回 cancelCompletion を呼んでから setTimeout で待っていました。素早い継続入力時には cancelCompletion が大量呼び出され UI の hide/show や state 更新のオーバーヘッドが頻発し、CPU 使用率が急上昇します。

  • dispatcher.onEvent の実行フローでは新しいイベントが来ても既に走っている _onEvent が終了するまで待たされるため、createOnCallback 等で待機している処理が残ったままになりうる。そこで onEvent の入口で cbContext.revoke() を行うことで、外部コールバック待ち部分を早期に打ち切り、新しいイベント処理を速やかに始められるようにします。

設計の詳細(実装ポイント)

  • 変数(denops/ddc/app.ts のファイルスコープ)

    • const autoCompleteTimers = new Map<number, { timerId: number | null, resolve: (() => void) | null }>();
  • dispatcher.onEvent の変更

    • queuedEvent = ... の直後(ロックを取る前)に cbContext.revoke(); を呼び出す。これにより、進行中のコールバック待ちを即時キャンセルする。
  • _onEvent 内の autoCompleteDelay 処理を置換

    • 既存の「await ddc.cancelCompletion(); await new Promise(setTimeout)」シーケンスを、次のような per-buffer デバウンスに置換する:
      1. 現在のバッファ番号を取得(await denops.call('bufnr'))
      2. 既存タイマーがあれば clearTimeout して resolve を無効化。
      3. 新しい Promise を作り、setTimeout で delay 後に resolve する。resolve 後に ddc.cancelCompletion を一度だけ呼ぶ。
  • エラーハンドリング・互換性

    • autoCompleteDelay === 0 の場合は既存コードパスを保持。
    • 既存の cbContext.revoke() の意味を損なわずに位置を移動させる。

差分草案(要約)

  • 追加: ファイル先頭のスコープに per-buffer timer 用 Map を追加。
  • 追加: dispatcher.onEvent で queuedEvent をセットした直後に cbContext.revoke() を呼ぶ。
  • 置換: _onEvent 内の autoCompleteDelay ブロックを per-buffer debounce 実装に変更。

具体的なコードの抜粋(変更案、抜粋)

  • 追加箇所(ファイルスコープ)
const autoCompleteTimers = new Map<number, { timerId: number | null; resolve: (() => void) | null }>();
  • dispatcher.onEvent の先頭に追加
queuedEvent = ensure(arg1, is.String) as DdcEvent;
// Immediately cancel any callback-waiting operations to make in-flight
// completions cancellable by new input.
cbContext.revoke();
  • _onEvent の autoCompleteDelay 部分の置換(要点)
if (options.autoCompleteDelay > 0) {
  const bufnr = await denops.call('bufnr') as number;
  const prev = autoCompleteTimers.get(bufnr);
  if (prev?.timerId != null) {
    clearTimeout(prev.timerId);
    // invalidate previous resolve
    if (prev.resolve) prev.resolve = null;
  }

  await new Promise<void>((resolve) => {
    const timerId = setTimeout(() => {
      // clear stored timer
      const item = autoCompleteTimers.get(bufnr);
      if (item && item.timerId === timerId) {
        autoCompleteTimers.delete(bufnr);
      }
      resolve();
    }, options.autoCompleteDelay);
    autoCompleteTimers.set(bufnr, { timerId, resolve });
  });

  // After debounce actually occurred, call cancelCompletion once.
  await ddc.cancelCompletion(denops, context, options);
}

テスト/検証

  • 既存の自動化ベンチ手順(headless nvim スクリプト)で、以下シナリオを比較します。

    • (A) master(現状) autoCompleteDelay=0
    • (B) master autoCompleteDelay=50
    • (C) この PR 版 autoCompleteDelay=50

    測定: nvim の CPU% ピーク、単一入力→補完までのレイテンシ(ログで計測)、cancelCompletion 呼び出し回数(ログ追加で計測)。期待: C が最も cancel 呼び出しを減らし CPU ピークが下がる。

PR の詳細

  • PR タイトル案: "debounce(autoCompleteDelay): per-buffer debounce and cancel in-flight completion on new input"
  • ブランチ: 新しいブランチを作成して PR を起票します。ブランチ名は "fix/debounce-cancel-on-new-input" を想定します。

追加で行う作業(PR 後)

  • per-buffer timers の単体テスト(必要なら mock denops を使ったテスト)。
  • もし sources の gather が同期的に長時間走るケースが問題であれば、gather のキャンセル性(AbortController 相当)を検討するための追加設計。

レビューの観点

  • 互換性: autoCompleteDelay が 0 の場合は既存経路が維持されることを重点的にレビューしてください。
  • 副作用: dispatcher.onEvent で cbContext.revoke() を呼ぶことにより、外部 onEvent/onCompleteDone の待ちが想定外に短くキャンセルされないか確認してください。必要ならオプションで制御可能にします。

以上の内容で 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 程度に制限して試す。
  • 重いフィルタ・ソートを同期で呼ばない/頻度を下げる
    • ソースや...

This pull request was created from Copilot chat.

Copilot AI changed the title [WIP] Add improvements to autoCompleteDelay for CPU spike reduction Improve autoCompleteDelay: per-buffer debounce + early callback cancellation Apr 24, 2026
Copilot AI requested a review from Shougo April 24, 2026 10:22
@Shougo Shougo marked this pull request as ready for review April 25, 2026 10:27
@Shougo Shougo merged commit 8783ab6 into main Apr 25, 2026
3 checks passed
@Shougo Shougo deleted the copilot/add-autocomplete-delay-improvements branch April 25, 2026 10:31
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