Copilot 0.64.1: bulk exclusion filtering stalls shared extension host in large workspaces (Limiter.consume / Array.shift)
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
### Summary
Copilot's bulk file-exclusion filtering has a reproducible large-batch performance regression consistent with minute-scale shared extension-host stalls observed after upgrading from VS Code 1.135.0 / bundled Copilot 0.63.0 to 1.136.1 / bundled Copilot 0.64.1.
The source changes in #332368 match the shipped bundle differences: unconditional per-file filtering plus enqueueing the entire result array through a Limiter(20). Its pending queue uses Array.shift(). Limiting active work to 20 does not bound the pending queue.
This report is about the filtering algorithm, not a request to bypass content-exclusion checks.
### Environment and observed behavior
- OS: Windows, x64.
- Affected: VS Code 1.136.1, bundled Copilot 0.64.1.
- Comparison: official VS Code 1.135.0 portable, bundled Copilot 0.63.0.
- Feature: reproduced with Copilot CLI selected and with Local explicitly selected.
- Selected chat model: not isolated as a variable; the observed stall concerns background startup/file-search processing.
- Large private two-root workspace: approximately 433,000 files in an independent rg enumeration, including approximately 16,800 Lua files. This is a scale estimate, NOT a measurement of Copilot's actual search-result/queue length.
- Default shared extension host; no extension-host affinity workaround.
- Latest Insiders has not been tested.
When the host stalls, another extension's language server has already returned highlighting data, but the extension-host client cannot apply it promptly. Hover remains loading and language interactions are delayed.
### Controlled version comparison
Separate copied test profiles used the same trusted workspace, activated extension IDs, LuaForQC 0.6.0 server binary and normalized initialization options. Client timing instrumentation and a transparent LSP observer separated server response from client application. The network/proxy configuration was not changed between runs.
Elapsed seconds from the test startup reference:
| VS Code / bundled Copilot | Server returned color data | Client applied highlighting |
| --- | ---: | ---: |
| 1.136.1 / 0.64.1, CLI selected | 27.123 | 160.843 |
| 1.135.0 / 0.63.0 | 23.808 | 27.398 |
| 1.136.1 / 0.64.1, explicit Local | 17.647 | 143.715 |
The older-version run remained responsive during a subsequent six-minute observation. These are individual controlled runs, not statistical latency guarantees. Switching the selected session to Local did not resolve the current-version stall; that does not prove every background CLI service was absent.
The released LuaForQC 0.5.0 also exhibited the delay on current VS Code, so the symptom is not exclusive to that extension's 0.6.0 memory changes.
Repeated current-version CPU profiles attributed over 99% of sampled CPU time to Copilot's Limiter.consume, at line 24, column 83324 in the installed minified extension.js. Raw profiles/configuration are not attached because they contain private paths and environment data.
### Source-level finding
PR #332368 (merged August 24) changes:
1. extensions/copilot/src/platform/search/vscode-node/searchServiceImpl.ts:
removes the isRegexExclusionsEnabled gate around the extra filtering pass.
2. extensions/copilot/src/platform/ignore/common/ignoreService.ts:
changes filterIngoredResources (spelling as in source) from sequential awaits to:
```ts
const limiter = new Limiter(20);
try {
const ignored = await Promise.all(
resources.map(resource =>
limiter.queue(() => ignoreService.isCopilotIgnored(resource))
)
);
return resources.filter((_, index) => !ignored[index]);
} finally {
limiter.dispose();
}
```
The Limiter in the shipped bundle pushes pending factories into outstandingPromises and removes them in consume with outstandingPromises.shift(). The limiter already existed in the older bundle; using it for the whole filtering batch, and removing the entry gate, are the relevant changes.
With fast/resolved exclusion predicates, the full batch is queued before it drains. Large-array front removal is costly, and the promise-microtask chain can prevent timers/other event-loop work from running for the duration.
### Isolated algorithm reproduction
An independent local benchmark extracted the actual old/new filtering functions and current Limiter from the two shipped bundles, with only event-emitter notifications stubbed. Input and predicate:
```js
const resources = Array.from({ length: 100000 }, (_, id) => ({ id }));
const ignoreService = {
async isCopilotIgnored(resource) {
return resource.id % 7 === 0;
}
};
// Time filterIngoredResources(ignoreService, resources).
// Repeat with 10,000 and 50,000 resources and compare with sequential awaits.
```
This synthetic test needs no private workspace, file I/O or network inside the predicate. To reproduce against source, invoke the source filterIngoredResources with this predicate and a large synthetic resource array; the predicate does not inspect URI fields.
Measured using Node 20.20.2 / V8 11.3.244.8-node.38 (one run):
| Resources | Old sequential filter | New full queue, concurrency 20 | Fixed 20-worker reference |
| --- | ---: | ---: | ---: |
| 10,000 | 5.08 ms | 20.52 ms | 1.45 ms |
| 50,000 | 11.08 ms | 3,894.29 ms | 6.14 ms |
| 100,000 | 15.56 ms | 14,033.20 ms | 10.00 ms |
All implementations returned identical ordered results. A zero-delay timer did not run before filter completion in these fast-promise tests. The fixed-worker reference claims an index from a shared counter rather than enqueueing every resource.
These timings are from independent Node, NOT a benchmark of VS Code's embedded runtime. They establish an isolated algorithmic regression, not an exact attribution of the entire real startup delay.
### Expected behavior / possible fix direction
Preserve every required exclusion check while avoiding the unbounded shift-based pending queue, for example with fixed workers or efficient O(1) dequeue. Also consider cancellation and periodic event-loop yielding: removing shift cost alone does not guarantee fairness for large chains of immediately resolved promises.
A regression test should cover large arrays and fast predicates, preserve result ordering/exclusion correctness, and check host responsiveness as appropriate.
### Remaining uncertainty and related reports
The actual live queue lengths, number of calls, and full transition from responsive startup to the stall have not been captured. A patched Copilot has not yet been tested in the real workspace. Therefore the evidence strongly implicates this path but does not establish that every second of the observed delay has the same cause.
Similar symptoms were reported in #299718 and #304546, but I have not established identical root causes. #327021 concerns repeated CLI session-store enumeration and should not be conflated with this file-filtering report.
No installed Copilot files or content-exclusion policies were modified during the investigation.
Contributor guide
Assessment
This issue has not been assessed yet.