Change:
Type: <bPerformance Issue</bWhen I step into my code (NodeJS), I noticed lately that the debugger is slower upon stepping in or over. I don't recall having this issue before though. It's a slight lag. I tried clearing my cache but that seemed to have no effect. VS Code version: Code 1.79.2 (, 2023-06-14T08:57:04.379Z) OS version: WindowsNT x64 10.0. Modes: <details<summarySystem Info</summary </details<details<summaryWorkspace Info</summary</details<details<summaryExtensions (9)</summaryExtensionVersion ------ vscode-css-formatter1.0.2 classdiagram-ts0.1.67 jsonviewer1.3.2 javascript-ejs-support1.3.3 gitlens14.0.1 vscode-firefox-debug2.9.8 debug-visualizer2.4.0 hexeditor1.9.11 php-debug1.32.1 </details<!-- generated by issue reporter --
Just following up. I actually reformatted my computer and upgraded to Windows 11 if that helps. And I am still getting lag with the step cursor (specifically with the Node debugger). When using the step cursor and XDebug, it's normal responsive speed.
/jsDebugLogs
Please collect trace logs using the following instructions:
It doesn't look like you ran a "step into" when you recorded those logs. Please do so so I can see what's going on.
Sorry about that. More data should be available in this log file.
There's a lot going on there. I think your case might have already been fixed by You can try out the of the debugger and see.
Oh sorry I did too much lol. Ok, I can live with this until the next fix. I'll keep an eye out for this on the next update. If I notice the debugger is still slow after next official update, I will just open this ticket again.
Also my title is a little inaccurate. It's not slow on just "step into". But also "step over" as well.
Sorta like VSCode decided to throttle the keystrokes. When the instruction is executed, its normal speed. But when I keep pressing F10 its not as responsive as it used to be. For example, when I press F10 10 times, I expect responsiveness. But lately there seems to be latency between keystrokes.
There will always be some latency because events are queued and ultimately multiple round trips through the debugger and back to are required. But from the logs it looks like your rapid strokes might be getting you into the "bad state" I referenced in the PR where spooky things start happening. I'll close this for now, please comment if you hit this on nightly/in the next release (scheduled for 6 to 7 days from now)
I haven't tried the nightly release because I figured I would wait. But it's been awhile now, and I am still experiencing this problem. Debugging is still slow on version 1.81.0
Please grab another log file if you're able. Thanks!
Sure, I just debugged a very simple hello world app, barebones. Looping through an array from 1 through 10.
Can you grab a record of your screen during this? is useful. Also, please run so we can see when you trigger actions using keybindings, if any.
Yes, no problem. Please see the issue via the gif. If you need a video for something smoother, let me know. But this should give you an idea of the delay I have when I step through. Please note that the delay was never this severe. !
Thanks, I saw similar. It looks there is no longer a problem on the JS debugger side; it seems like vscode is being a bit slow to process things. For example, in the log, I see requests for stacktrace information come in for the previous event even after session has already been continued. Rob, might be a side-effect of the queuing a few iterations ago?
Just FYI, this is also a node.JS problem too. My example is only showing client side, but I work in Node all the time, and its the same problem.
Just checking for a status update on this issue.
<!-- AUTHORVERIFICATIONREQUEST --This bug has been fixed in the latest release of ! you can help us out by commenting if things are now working as expected. If things still don't seem right, please ensure you're on version of Insiders (today's or later - you can use in the command palette to check), and leave a comment letting us know what isn't working as expected. Happy Coding!
Just as I was accepting that this debugger would always have latency issues, I get this notification today. I immediately installed Insiders, from someone who uses this style of debugging all the time, I can confirm this has been fixed! I will continue to test of course, but so far this is the fastest I've ever seen the step into debugger work, even before this had been a verifiable problem. Thanks for addressing this. As someone who debugs code 90% of the time, this was pleasant surprise to see fixed! /verified

Discussion:
import { canceled } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { normalizeDriveLetter } from 'vs/base/common/labels'; <del> import { DisposableStore, IDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; </del> <ins> import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; </ins> import { mixin } from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import * as resources from 'vs/base/common/resources';

} })); <del> const statusQueue = new Queue<void>(); this.rawListeners.add(this.raw.onDidStop(async event => { statusQueue.queue(async () => { this.passFocusScheduler.cancel(); this.stoppedDetails.push(event.body); await this.fetchThreads(event.body); // If the focus for the current session is on a non-existent thread, clear the focus. const focusedThread = this.debugService.getViewModel().focusedThread; const focusedThreadDoesNotExist = focusedThread !== undefined && focusedThread.session === this && !this.threads.has(focusedThread.threadId); if (focusedThreadDoesNotExist) { this.debugService.focusStackFrame(undefined, undefined); } const thread = typeof event.body.threadId === 'number' ? this.getThread(event.body.threadId) : undefined; if (thread) { // Call fetch call stack twice, the first only return the top stack frame. // Second retrieves the rest of the call stack. For performance reasons #25605 const promises = this.model.refreshTopOfCallstack(<Thread>thread); const focus = async () => { if (focusedThreadDoesNotExist || (!event.body.preserveFocusHint && thread.getCallStack().length)) { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || focusedStackFrame.thread.session === this) { // Only take focus if nothing is focused, or if the focus is already on the current session const preserveFocus = !this.configurationService.getValue<IDebugConfiguration>('debug').focusEditorOnBreak; await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); } if (thread.stoppedDetails) { if (thread.stoppedDetails.reason === 'breakpoint' && this.configurationService.getValue<IDebugConfiguration>('debug').openDebug === 'openOnDebugBreak' && !this.suppressDebugView) { await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar); </del> <ins> const statusQueue = this.rawListeners.add(new ThreadStatusScheduler()); this.rawListeners.add(this.raw.onDidStop(async event => { this.passFocusScheduler.cancel(); this.stoppedDetails.push(event.body); statusQueue.run( this.fetchThreads(event.body).then(() => event.body.threadId === undefined ? this.threadIds : [event.body.threadId]), async (threadId, token) => { const hasLotsOfThreads = event.body.threadId === undefined && this.threadIds.length > 10; // If the focus for the current session is on a non-existent thread, clear the focus. const focusedThread = this.debugService.getViewModel().focusedThread; const focusedThreadDoesNotExist = focusedThread !== undefined && focusedThread.session === this && !this.threads.has(focusedThread.threadId); if (focusedThreadDoesNotExist) { this.debugService.focusStackFrame(undefined, undefined); } const thread = typeof threadId === 'number' ? this.getThread(threadId) : undefined; if (thread) { // Call fetch call stack twice, the first only return the top stack frame. // Second retrieves the rest of the call stack. For performance reasons #25605 // Second call is only done if there's few threads that stopped in this event. const promises = this.model.refreshTopOfCallstack(<Thread>thread, /* fetchFullStack= */!hasLotsOfThreads); const focus = async () => { if (focusedThreadDoesNotExist || (!event.body.preserveFocusHint && thread.getCallStack().length)) { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || focusedStackFrame.thread.session === this) { // Only take focus if nothing is focused, or if the focus is already on the current session const preserveFocus = !this.configurationService.getValue<IDebugConfiguration>('debug').focusEditorOnBreak; await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); </ins> } <del> if (this.configurationService.getValue<IDebugConfiguration>('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) { await this.hostService.focus(window, { force: true /* Application may not be active */ }); </del> <ins> if (thread.stoppedDetails && !token.isCancellationRequested) { if (thread.stoppedDetails.reason === 'breakpoint' && this.configurationService.getValue<IDebugConfiguration>('debug').openDebug === 'openOnDebugBreak' && !this.suppressDebugView) { await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar); } if (this.configurationService.getValue<IDebugConfiguration>('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) { await this.hostService.focus(window, { force: true /* Application may not be active */ }); } </ins> } } <ins> }; await promises.topCallStack; if (token.isCancellationRequested) { return; </ins> } <del> }; await promises.topCallStack; focus(); await promises.wholeCallStack; const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || !focusedStackFrame.source || focusedStackFrame.source.presentationHint === 'deemphasize' || focusedStackFrame.presentationHint === 'deemphasize') { // The top stack frame can be deemphesized so try to focus again #68616 </del> focus(); <ins> await promises.wholeCallStack; if (token.isCancellationRequested) { return; } const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || !focusedStackFrame.source || focusedStackFrame.source.presentationHint === 'deemphasize' || focusedStackFrame.presentationHint === 'deemphasize') { // The top stack frame can be deemphesized so try to focus again #68616 focus(); } </ins> } <del> } this._onDidChangeState.fire(); }); </del> <ins> this._onDidChangeState.fire(); }, ); </ins> })); this.rawListeners.add(this.raw.onDidThread(event => { <del> statusQueue.queue(async () => { if (event.body.reason === 'started') { // debounce to reduce threadsRequest frequency and improve performance if (!this.fetchThreadsScheduler) { this.fetchThreadsScheduler = new RunOnceScheduler(() => { this.fetchThreads(); }, 100); this.rawListeners.add(this.fetchThreadsScheduler); } if (!this.fetchThreadsScheduler.isScheduled()) { this.fetchThreadsScheduler.schedule(); } } else if (event.body.reason === 'exited') { this.model.clearThreads(this.getId(), true, event.body.threadId); const viewModel = this.debugService.getViewModel(); const focusedThread = viewModel.focusedThread; this.passFocusScheduler.cancel(); if (focusedThread && event.body.threadId === focusedThread.threadId) { // De-focus the thread in case it was focused this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); } </del> <ins> statusQueue.cancel([event.body.threadId]); if (event.body.reason === 'started') { // debounce to reduce threadsRequest frequency and improve performance if (!this.fetchThreadsScheduler) { this.fetchThreadsScheduler = new RunOnceScheduler(() => { this.fetchThreads(); }, 100); this.rawListeners.add(this.fetchThreadsScheduler); </ins> } <del> }); </del> <ins> if (!this.fetchThreadsScheduler.isScheduled()) { this.fetchThreadsScheduler.schedule(); } } else if (event.body.reason === 'exited') { this.model.clearThreads(this.getId(), true, event.body.threadId); const viewModel = this.debugService.getViewModel(); const focusedThread = viewModel.focusedThread; this.passFocusScheduler.cancel(); if (focusedThread && event.body.threadId === focusedThread.threadId) { // De-focus the thread in case it was focused this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); } } </ins> })); this.rawListeners.add(this.raw.onDidTerminateDebugee(async event => {

})); this.rawListeners.add(this.raw.onDidContinued(event => { <del> statusQueue.queue(async () => { const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; if (typeof threadId === 'number') { this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId); const tokens = this.cancellationMap.get(threadId); this.cancellationMap.delete(threadId); tokens?.forEach(t => t.dispose(true)); } else { this.stoppedDetails = []; this.cancelAllRequests(); } this.lastContinuedThreadId = threadId; // We need to pass focus to other sessions / threads with a timeout in case a quick stop event occurs #130321 this.passFocusScheduler.schedule(); this.model.clearThreads(this.getId(), false, threadId); this._onDidChangeState.fire(); }); </del> <ins> const allThreads = event.body.allThreadsContinued !== false; statusQueue.cancel(allThreads ? undefined : [event.body.threadId]); const threadId = allThreads ? undefined : event.body.threadId; if (typeof threadId === 'number') { this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId); const tokens = this.cancellationMap.get(threadId); this.cancellationMap.delete(threadId); tokens?.forEach(t => t.dispose(true)); } else { this.stoppedDetails = []; this.cancelAllRequests(); } this.lastContinuedThreadId = threadId; // We need to pass focus to other sessions / threads with a timeout in case a quick stop event occurs #130321 this.passFocusScheduler.schedule(); this.model.clearThreads(this.getId(), false, threadId); this._onDidChangeState.fire(); </ins> })); const outputQueue = new Queue<void>();

} } } <ins> /** * Keeps track of events for threads, and cancels any previous operations for * a thread when the thread goes into a new state. Currently, the operations a thread has are: * * - started * - stopped * - continue * - exited * * In each case, the new state preempts the old state, so we don't need to * queue work, just cancel old work. It's up to the caller to make sure that * no UI effects happen at the point when the `token` is cancelled. */ export class ThreadStatusScheduler extends Disposable { /** * An array of set of thread IDs. When a 'stopped' event is encountered, the * editor refreshes its thread IDs. In the meantime, the thread may change * state it again. So the editor puts a Set into this array when it starts * the refresh, and checks it after the refresh is finished, to see if * any of the threads it looked up should now be invalidated. */ private pendingCancellations: Set<number | undefined>[] = []; /** * Cancellation tokens for currently-running operations on threads. */ private readonly threadOps = this._register(new DisposableMap<number, CancellationTokenSource>()); /** * Runs the operation. * If thread is undefined it affects all threads. */ public async run(threadIdsP: Promise<number[]>, operation: (threadId: number, ct: CancellationToken) => Promise<unknown>) { const cancelledWhileLookingUpThreads = new Set<number | undefined>(); this.pendingCancellations.push(cancelledWhileLookingUpThreads); const threadIds = await threadIdsP; // Now that we got our threads, // 1. Remove our pending set, and // 2. Cancel any slower callers who might also have found this thread for (let i = 0; i < this.pendingCancellations.length; i++) { const s = this.pendingCancellations[i]; if (s === cancelledWhileLookingUpThreads) { this.pendingCancellations.splice(i, 1); break; } else { for (const threadId of threadIds) { s.add(threadId); } } } if (cancelledWhileLookingUpThreads.has(undefined)) { return; } await Promise.all(threadIds.map(threadId => { if (cancelledWhileLookingUpThreads.has(threadId)) { return; } this.threadOps.get(threadId)?.cancel(); const cts = new CancellationTokenSource(); this.threadOps.set(threadId, cts); return operation(threadId, cts.token); })); } /** * Cancels all ongoing state operations on the given threads. * If threads is undefined it cancel all threads. */ public cancel(threadIds?: readonly number[]) { if (!threadIds) { for (const [_, op] of this.threadOps) { op.cancel(); } this.threadOps.clearAndDisposeAll(); for (const s of this.pendingCancellations) { s.add(undefined); } } else { for (const threadId of threadIds) { this.threadOps.get(threadId)?.cancel(); this.threadOps.deleteAndDispose(threadId); for (const s of this.pendingCancellations) { s.add(threadId); } } } } } </ins>

return; } <del> refreshTopOfCallstack(thread: Thread): { topCallStack: Promise<void>; wholeCallStack: Promise<void> } { </del> <ins> refreshTopOfCallstack(thread: Thread, fetchFullStack = true): { topCallStack: Promise<void>; wholeCallStack: Promise<void> } { </ins> if (thread.session.capabilities.supportsDelayedStackTraceLoading) { // For improved performance load the first stack frame and then load the rest async. let topCallStack = Promise.resolve(); const wholeCallStack = new Promise<void>((c, e) => { topCallStack = thread.fetchCallStack(1).then(() => { <del> if (!this.schedulers.has(thread.getId())) { </del> <ins> if (!this.schedulers.has(thread.getId()) && fetchFullStack) { </ins> const deferred = new DeferredPromise<void>(); this.schedulers.set(thread.getId(), { completeDeferred: deferred,

<ins> /*--------------------------------------------------------------------------------------------- *  Copyright (c) Microsoft Corporation. All rights reserved. *  Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ThreadStatusScheduler } from 'vs/workbench/contrib/debug/browser/debugSession'; suite('DebugSession - ThreadStatusScheduler', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); test('cancel base case', async () => { const scheduler = ds.add(new ThreadStatusScheduler()); await scheduler.run(Promise.resolve([1]), async (threadId, token) => { assert.strictEqual(threadId, 1); assert.strictEqual(token.isCancellationRequested, false); scheduler.cancel([1]); assert.strictEqual(token.isCancellationRequested, true); }); }); test('cancel global', async () => { const scheduler = ds.add(new ThreadStatusScheduler()); await scheduler.run(Promise.resolve([1]), async (threadId, token) => { assert.strictEqual(threadId, 1); assert.strictEqual(token.isCancellationRequested, false); scheduler.cancel(undefined); assert.strictEqual(token.isCancellationRequested, true); }); }); test('cancels when new work comes in', async () => { const scheduler = ds.add(new ThreadStatusScheduler()); let innerCalled = false; await scheduler.run(Promise.resolve([1]), async (threadId, token1) => { assert.strictEqual(threadId, 1); assert.strictEqual(token1.isCancellationRequested, false); await scheduler.run(Promise.resolve([1]), async (_threadId, token2) => { innerCalled = true; assert.strictEqual(token1.isCancellationRequested, true); assert.strictEqual(token2.isCancellationRequested, false); }); }); assert.strictEqual(innerCalled, true); }); test('cancels slower lookups when new lookup is made', async () => { const scheduler = ds.add(new ThreadStatusScheduler()); const innerCalled1: number[] = []; const innerCalled2: number[] = []; await Promise.all([ scheduler.run(Promise.resolve().then(() => { }).then(() => [1, 3]), async threadId => { innerCalled1.push(threadId); }), scheduler.run(Promise.resolve([1, 2]), async threadId => { innerCalled2.push(threadId); }) ]); assert.deepEqual(innerCalled1, [3]); assert.deepEqual(innerCalled2, [1, 2]); }); test('allows work with other IDs', async () => { const scheduler = ds.add(new ThreadStatusScheduler()); let innerCalled = false; await scheduler.run(Promise.resolve([1]), async (threadId, token1) => { assert.strictEqual(threadId, 1); assert.strictEqual(token1.isCancellationRequested, false); await scheduler.run(Promise.resolve([2]), async (_threadId, token2) => { innerCalled = true; assert.strictEqual(token1.isCancellationRequested, false); assert.strictEqual(token2.isCancellationRequested, false); }); }); assert.strictEqual(innerCalled, true); }); test('cancels when called during reslution', async () => { const scheduler = ds.add(new ThreadStatusScheduler()); let innerCalled = false; await scheduler.run(Promise.resolve().then(() => scheduler.cancel([1])).then(() => [1]), async () => { innerCalled = true; }); assert.strictEqual(innerCalled, false); }); test('global cancels when called during reslution', async () => { const scheduler = ds.add(new ThreadStatusScheduler()); let innerCalled = false; await scheduler.run(Promise.resolve().then(() => scheduler.cancel(undefined)).then(() => [1]), async () => { innerCalled = true; }); assert.strictEqual(innerCalled, false); }); }); </ins>