Skip to content

Conversation

@jcpetruzza
Copy link
Contributor

This fixes a performance regression accidentally introduced in #265755.

Before that change, if the continued event had allThreadsContinued set, then we'd call this.model.clearThreads() passing undefined as threadId, which would update all threads in one pass. After that change, a call to this.model.clearThreads() would be done for each thread.

Because each call to this.model.clearThreads() ends up calling this._onDidChangeCallStack.fire(), we end up with quadratic overhead as some of the handlers traverse all threads in the call-stack box. This would lead to minute long freezes when using the Erlang debugger, to debug a system with ~20K Erlang processes.

The solution is to go back to the previous implementation, where clearThreads() is ever called with one thread-id or with undefined, but first wait on this.fetchThreads() if needed

@jcpetruzza
Copy link
Contributor Author

@jcpetruzza please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree company="Meta"


if (needFreshFetchThreads) {
this.fetchThreadsScheduler.rawValue?.cancel();
await this.fetchThreads();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to await outside the statusQueue since that opens up to race conditions (e.g. DA continues threads and pauses again before this fetchThreads finishes and we end in a torn state)

I think we probably just want to debounce the _onDidChangeCallStack.fire call in DebugModel.clearThreads. You can see how we do that with the fetchThreadsScheduler already today in the debug session, we'd do the same thing there.

Btw reviewing the code I notice a bug in DebugModel.clearThreads -- we don't check the reference (thread ID) when clearing the schedulers. If a thread ID is set, we should only clear its scheduler, not all schedulers. If you're up for fixing a low hanging bug there, feel free to do so 🙂

This fixes a performance regression accidentally introduced in microsoft#265755.

Before that change, if the `continued` event had `allThreadsContinued` set,
then we'd call `this.model.clearThreads()` passing `undefined` as `threadId`,
which would update all threads in one pass. After that change, a call to
`this.model.clearThreads()` would be done for each thread.

Because each call to `this.model.clearThreads()` ends up calling
`this._onDidChangeCallStack.fire()`, we end up with quadratic overhead
as some of the handlers traverse all threads in the call-stack box.
This would lead to minute long freezes when using the Erlang debugger,
to debug a system with ~20K Erlang processes.

We now debounce the calls to `_onDidChangeCallStack.fire()` to avoid
the issue.
When calling `clearThreads()` on the model, we'd cancel every scheduled
action, for every session, instead of those relevant only for the
given session and, optionally, thread.
@jcpetruzza jcpetruzza force-pushed the debugger_continue_fast branch from 00a2256 to d96d639 Compare December 15, 2025 21:35
@jcpetruzza
Copy link
Contributor Author

@connor4312 thanks for the review! I've updated the branch with a new version that instead debounces the calls, and did a drive-by fix of the bug you spotted in clearThreads()


session.clearThreads(removeThreads, reference);
this._onDidChangeCallStack.fire(undefined);
this._onDidChangeCallStackFire.schedule();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing: we should schedule only if not already scheduled. Otherwise a DA with a ton of threads and very frequent events could result in the call stack never updating.

Comment on lines 1461 to 1463
private _onDidChangeCallStackFire = new RunOnceScheduler(() => {
this._onDidChangeCallStack.fire(undefined);
}, 100);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private _onDidChangeCallStackFire = new RunOnceScheduler(() => {
this._onDidChangeCallStack.fire(undefined);
}, 100);
private _onDidChangeCallStackFire = this._register(new RunOnceScheduler(() => {
this._onDidChangeCallStack.fire(undefined);
}, 100));

@jcpetruzza
Copy link
Contributor Author

Added a fixup for both issues

@connor4312
Copy link
Member

thanks for the contribution!

@connor4312 connor4312 enabled auto-merge (squash) December 15, 2025 22:06
@vs-code-engineering vs-code-engineering bot added this to the December / January 2026 milestone Dec 15, 2025
@connor4312 connor4312 merged commit f8a2167 into microsoft:main Dec 15, 2025
17 checks passed
przpl pushed a commit to przpl/vscode that referenced this pull request Dec 16, 2025
…rosoft#283635)

* debug: Fix UI freezing on "continue" with high number of threads

This fixes a performance regression accidentally introduced in microsoft#265755.

Before that change, if the `continued` event had `allThreadsContinued` set,
then we'd call `this.model.clearThreads()` passing `undefined` as `threadId`,
which would update all threads in one pass. After that change, a call to
`this.model.clearThreads()` would be done for each thread.

Because each call to `this.model.clearThreads()` ends up calling
`this._onDidChangeCallStack.fire()`, we end up with quadratic overhead
as some of the handlers traverse all threads in the call-stack box.
This would lead to minute long freezes when using the Erlang debugger,
to debug a system with ~20K Erlang processes.

We now debounce the calls to `_onDidChangeCallStack.fire()` to avoid
the issue.

* debug: Avoid cancelling too many scheduled actions when clearing threads

When calling `clearThreads()` on the model, we'd cancel every scheduled
action, for every session, instead of those relevant only for the
given session and, optionally, thread.

* fixup! debug: Fix UI freezing on "continue" with high number of threads
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.

3 participants