microsoft / microsoft/vscode-cmake-tools

[Bug] Per-test CTest source resolution rescans every target source, freezing the extension host for minutes after each configure

Open
#5,093 5 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
TypeScript
Stars
1.7k
Forks
546
Avg merge
2d 16h
Merged PRs (30d)
32

Description

### Brief Issue Summary

After 1.24.42 landed our workspace became unusable in VS Code.

On window open the status bar sits on "Configuring..." for several minutes with nothing further appearing in the CMake/Build output channel, and the extension host is unresponsive the whole time (other extensions stop answering, and opening a view shows "Activating Extensions..."). Launching a target looks the same. ninja reports no work to do, the log prints

```
[driver] Build completed: 00:00:00.748
[build] Build finished with exit code 0
```

and then nothing happens for minutes before the executable starts. This is every configure and every launch, not a one-off.

The project itself is fine. The same configure takes 2.1 s from the command line, a clean 662-step build takes 28.8 s, and the target is up in 0.26 s.

### What it is doing

I profiled the extension host while it was stuck. All of the time is in CTest source resolution:

```
36.9% existsSync
35.4% readFileUtf8
16.7% readFileSync
8.3% parseDoctestCases dist/main.js:7834
0.6% tryResolveTestSourceFromExecutableSources dist/main.js:7760
```

`tryResolveTestSourceFromExecutableSources` walks every source file of the test's target once per test, and `tryResolveDoctestLine` does `existsSync` + `readFileSync` + a regex parse for each of those files, with nothing cached between tests:

```js
for (const source of targetInfo.sources) {
const candidateFile = path.resolve(targetInfo.sourceDir, source);
const candidateLine = this.tryResolveDoctestLine(candidateFile, test.name);
if (candidateLine !== undefined) { ... }
}
```

We use GoogleTest through `gtest_discover_tests()`, which registers 4093 CTest tests, and the test target has 250 sources. Those sources contain no doctest `TEST_CASE`s, so `parseDoctestCases` never matches, the loop never exits early, and every test ends up scanning all 250 files. That is up to 1,023,250 `existsSync` + `readFileSync` + parse calls, synchronously on the extension host, per configure.

Two things make it avoidable:

The expensive path runs before the cheap one. `resolveTestSourceLocation` calls `tryResolveTestSourceFromExecutableSources` first and only checks the `DEF_SOURCE_LINE` test property afterwards. `gtest_discover_tests` already writes `DEF_SOURCE_LINE` for every test it registers, so the correct file and line were available without touching the disk at all.

There is also no way to opt out. `cmake.ctest.testExplorerIntegrationEnabled: false` gates the post-build `refreshTests` call (#4909, #4914), but the post-configure call is unconditional:

```js
const result = await drv.configure(trigger, []);
if (result.exitCode === 0) { ... }
await this.cTestController.refreshTests(drv); // not gated
```

`cmake.configureOnOpen: false` does not help either, since it still runs off the cached configuration. I measured both and neither changed the profile.

### Numbers

Same workspace, same settings, only the extension version changed. CPU time measured 240 s after opening the folder, with the machine otherwise idle:

| Version | Extension host process | All VS Code processes |
| --- | --- | --- |
| 1.24.42 | 220.0 s, still climbing (~92% of one core, indefinitely) | 242.8 s |
| 1.22.28 | 5.0 s, flat after 40 s | 14.2 s |

We have pinned 1.22.28 as a workaround, which is also why the diagnostics block below reports 1.22.28 rather than 1.24.42.

### Repro

1. A CMake project with a GoogleTest target registered through `gtest_discover_tests()`, where the test count multiplied by the number of sources in the test target is large. Ours is 4093 x 250, but it should be visible well below that.
2. Build once so CTest can enumerate the tests.
3. Open the folder with CMake Tools 1.24.42 and let it configure.
4. The extension host pegs a core for minutes after the configure has already finished.

### Suggested fix

Check `DEF_SOURCE_LINE` before falling back to scanning target sources, and memoize the parse per file, since the same 250 files are read and parsed 4093 times in a row. Honoring `cmake.ctest.testExplorerIntegrationEnabled` on the post-configure `refreshTests` call would at least give people a way out in the meantime.

### CMake Tools Diagnostics

```shell
{
"os": "win32",
"vscodeVersion": "1.137.0",
"cmtVersion": "1.22.28",
"configurations": [
{
"folder": "c:\Users\mewio\Documents\GitHub\Shalltear",
"cmakeVersion": "4.2.0",
"configured": true,
"generator": "Ninja",
"usesPresets": true,
"compilers": {
"C": "C:/Users/mewio/Documents/GitHub/Shalltear/External/Prebuilt/llvm-mingw/bin/x86_64-w64-mingw32-clang.exe",
"CXX": "C:/Users/mewio/Documents/GitHub/Shalltear/External/Prebuilt/llvm-mingw/bin/x86_64-w64-mingw32-clang++.exe"
}
}
],
"cpptoolsIntegration": {
"isReady": false,
"hasCodeModel": false,
"activeBuildType": "",
"buildTypesSeen": [],
"requests": [],
"responses": [],
"partialMatches": [],
"targetCount": 0,
"executablesCount": 0,
"librariesCount": 0,
"targets": []
},
"settings": [
{
"communicationMode": "automatic",
"useCMakePresets": "auto",
"configureOnOpen": true
}
]
}
```

### Debug Log

```shell
cmake.loggingLevel: trace, cmake.showTimestampsInOutput: true. Configure finishes at 13:38:24.481 and the last line of any kind is 13:38:25.092; the extension host then stays pegged at ~93% of one core for minutes with nothing further logged.

2026-09-13T13:38:23.441Z [info] [proc] Executing command: "C:\Program Files\CMake\bin\cmake.EXE" -DCMAKE_BUILD_TYPE=Debug -DSHALLTEAR_BUILD_SHARED=ON -DCMAKE_C_COMPILER=... -DCMAKE_CXX_COMPILER=... -DCMAKE_MAKE_PROGRAM=... -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -S C:/Users/mewio/Documents/GitHub/Shalltear -B C:/Users/mewio/Documents/GitHub/Shalltear/build -G Ninja
2026-09-13T13:38:24.478Z [trace] [cache] Read line in cache with name=GIT_EXECUTABLE-ADVANCED, typename=INTERNAL, valuestr=1
2026-09-13T13:38:24.481Z [trace] [cache] Parsed 161 cache entries
2026-09-13T13:38:24.481Z [trace] [cache] Parsed 161 entries from C:/Users/mewio/Documents/GitHub/Shalltear/build/CMakeCache.txt
2026-09-13T13:38:25.092Z [trace] [rollbar] Invoking function [$] with Rollbar wrapping [$Resolving process on "close" event]

```

### Additional Information

The profile was taken by launching VS Code with `--inspect-extensions` and running a CPU profile against the extension host over the DevTools protocol while the freeze was in progress, on an otherwise idle machine. The A/B was run in a separate `--user-data-dir` and `--extensions-dir` so that nothing but the extension version differed between the two runs.

Environment: VS Code 1.137.0, Windows 11 build 26200, CMake 4.2.0, Ninja, clang 23.1.1 (llvm-mingw), 28 logical cores. Test target is 250 sources and 4093 CTest tests registered by `gtest_discover_tests`.

Contributor guide

Open the contributing guide

Research direction

Start at resolveTestSourceLocation, tryResolveTestSourceFromExecutableSources, and tryResolveDoctestLine in dist/main.js around lines 7760-7834, then inspect the post-configure refreshTests call. Reproduce with a large gtest_discover_tests target and verify that DEF_SOURCE_LINE avoids source scanning, repeated file parsing is cached, and the integration setting can prevent refreshTests when disabled.

Written by the indexing model from the issue text.

Assessment

Tech stack
cmake, typescript
Domain
build-system, devtools, performance, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.