Summary
CommandTree startup is very slow in a real VS Code workspace. The user-visible startup can take multiple minutes before the views feel usable. In the public CodeDedup workspace, the persisted CommandTree debug log shows extension activation taking about 66 seconds, with two full discovery passes started almost simultaneously.
The duplicate discovery appears to be caused by initialDiscovery() calling syncQuickTasks() -> treeProvider.refresh() while the TreeView asks getChildren() before discoveryResult is set. getChildren() then starts another full refresh(). Every discovery category in the log appears twice.
Environment
- Extension:
nimblesite.commandtree@0.10.0
- VS Code:
1.116.0
- OS: macOS 26.3.1 arm64
- Workspace used for the public evidence: CodeDedup
.commandtree/commandtree.sqlite3: about 192 KiB
- Discovered commands in slow startup run: 124
Observed behaviour
On extension activation, CommandTree blocks startup on full discovery and command registration. The debug log shows:
03:50:50.168 Extension activating
03:50:50.182 SQLite database initialised
03:50:50.183 CommandTreeProvider.refresh() starting
03:50:50.186 Discovery started
03:50:50.188 getChildren: no discovery result yet, triggering refresh
03:50:50.188 CommandTreeProvider.refresh() starting
03:50:50.188 Discovery started
03:50:55.431 Discovery [vscode] count=4
03:50:55.431 Discovery [vscode] count=4
03:51:52.898 Discovery [npm] count=16
03:51:53.036 Discovery [markdown] count=41
03:51:53.129 Discovery [markdown] count=41
03:51:53.134 Discovery [npm] count=16
03:51:53.517 Discovery [shell] count=1
03:51:54.124 Discovery [shell] count=1
03:51:54.353 Discovery [make] count=21
03:51:54.357 Discovery [make] count=21
03:51:54.388 Discovery [cargo] count=41
03:51:54.388 Discovery complete totalCount=124
03:51:54.393 Discovery [cargo] count=41
03:51:54.393 Discovery complete totalCount=124
03:51:56.153 Commands registered in DB count=124
03:51:56.153 [SUMMARY] Starting taskCount=124
03:51:56.153 Extension activation complete
03:51:59.077 [SUMMARY] All summaries up to date
Timing from the same log:
| Event |
Elapsed from activation |
First refresh() starts |
15 ms |
Second refresh() starts from getChildren() |
20 ms |
vscode discovery returns |
5.263 s |
npm/markdown discovery returns |
~62.7-63.0 s |
| Both full discoveries complete |
~64.2 s |
| Commands are registered |
~65.985 s |
| Activation complete |
~65.985 s |
Why this looks actionable
Installed out/extension.js has this activation flow:
registerTreeViews(context);
registerCommands(context);
setupWatchers(context, workspaceRoot);
await initialDiscovery(workspaceRoot);
initAiSummaries(workspaceRoot);
logger.info("Extension activation complete");
initialDiscovery() calls:
await syncQuickTasks();
await registerDiscoveredCommands(workspaceRoot);
await syncTagsFromJson(workspaceRoot);
syncQuickTasks() calls:
await treeProvider.refresh();
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
CommandTreeProvider.getChildren() currently does:
if (!this.discoveryResult) {
logger.info("getChildren: no discovery result yet, triggering refresh");
await this.refresh();
}
So the TreeView can re-enter discovery while the activation-triggered discovery is still in flight. In the log, that happens 2 ms after the first Discovery started line.
discoverAllTasks() runs all discoverers in Promise.all, which is good, but without an in-flight refresh guard the whole discoverer set is duplicated. The current logs also only show completion counts, so it is hard to tell which finder is responsible for the long gap before npm/markdown finish.
Expected behaviour
- Extension activation should return quickly and should not block VS Code for a full workspace scan plus DB registration.
- Opening the CommandTree view during startup should not trigger a second full discovery if one is already running.
- Discovery should run once per explicit refresh/change event, or concurrent callers should share the same in-flight promise.
- The tree can show a loading/empty state while discovery continues in the background.
Suggested fix direction
-
Add an in-flight refresh guard in CommandTreeProvider.
- Store a
refreshPromise while discovery is running.
- If
refresh() is called again, return/await the same promise instead of starting another discoverAllTasks().
- Clear the promise in
finally.
-
Avoid awaiting full discovery in activate().
- Register providers/commands and return activation promptly.
- Kick initial discovery, DB registration, tag sync, and optional summary work into a background task.
- Fire tree updates when discovery completes.
-
Make getChildren() non-reentrant.
- If discovery is in progress, either await the existing promise or return a stable loading node.
- Do not start a new refresh from
getChildren() when initial discovery is already running.
-
Add per-discoverer duration logging.
- Log start/end/duration for each discoverer (
npm, markdown, cargo, etc.).
- This will make the next slow-start report immediately attributable.
-
Consider startup tests around concurrent refresh.
- A test should call
refresh() and getChildren() concurrently and assert discoverAllTasks() is invoked once.
- Another test should assert
activate() does not await slow discovery before returning, if activation is changed to background discovery.
Acceptance criteria
- In the CodeDedup-style startup sequence, the log should show only one
Discovery started before the first Discovery complete.
- Extension activation should complete before long-running workspace discovery and command registration finish.
- Quick Launch and All views should populate after the shared discovery result is available.
- A slow discoverer should be visible in logs with its elapsed duration.
Summary
CommandTree startup is very slow in a real VS Code workspace. The user-visible startup can take multiple minutes before the views feel usable. In the public CodeDedup workspace, the persisted CommandTree debug log shows extension activation taking about 66 seconds, with two full discovery passes started almost simultaneously.
The duplicate discovery appears to be caused by
initialDiscovery()callingsyncQuickTasks() -> treeProvider.refresh()while the TreeView asksgetChildren()beforediscoveryResultis set.getChildren()then starts another fullrefresh(). Every discovery category in the log appears twice.Environment
nimblesite.commandtree@0.10.01.116.0.commandtree/commandtree.sqlite3: about 192 KiBObserved behaviour
On extension activation, CommandTree blocks startup on full discovery and command registration. The debug log shows:
Timing from the same log:
refresh()startsrefresh()starts fromgetChildren()vscodediscovery returnsnpm/markdowndiscovery returnsWhy this looks actionable
Installed
out/extension.jshas this activation flow:initialDiscovery()calls:syncQuickTasks()calls:CommandTreeProvider.getChildren()currently does:So the TreeView can re-enter discovery while the activation-triggered discovery is still in flight. In the log, that happens 2 ms after the first
Discovery startedline.discoverAllTasks()runs all discoverers inPromise.all, which is good, but without an in-flight refresh guard the whole discoverer set is duplicated. The current logs also only show completion counts, so it is hard to tell which finder is responsible for the long gap beforenpm/markdownfinish.Expected behaviour
Suggested fix direction
Add an in-flight refresh guard in
CommandTreeProvider.refreshPromisewhile discovery is running.refresh()is called again, return/await the same promise instead of starting anotherdiscoverAllTasks().finally.Avoid awaiting full discovery in
activate().Make
getChildren()non-reentrant.getChildren()when initial discovery is already running.Add per-discoverer duration logging.
npm,markdown,cargo, etc.).Consider startup tests around concurrent refresh.
refresh()andgetChildren()concurrently and assertdiscoverAllTasks()is invoked once.activate()does not await slow discovery before returning, if activation is changed to background discovery.Acceptance criteria
Discovery startedbefore the firstDiscovery complete.