Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

debug: improve performance of stepping #197397

Merged
merged 1 commit into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
271 changes: 190 additions & 81 deletions src/vs/workbench/contrib/debug/browser/debugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance
import { canceled } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { DisposableStore, IDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle';
import { mixin } from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
import * as resources from 'vs/base/common/resources';
Expand Down Expand Up @@ -980,81 +980,96 @@ export class DebugSession implements IDebugSession, IDisposable {
}
}));

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);
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 });
}

if (this.configurationService.getValue<IDebugConfiguration>('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) {
await this.hostService.focus(window, { force: true /* Application may not be active */ });
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 */ });
}
}
}
};

await promises.topCallStack;
if (token.isCancellationRequested) {
return;
}
};

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

focus();

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();
}
}
}
this._onDidChangeState.fire();
});
this._onDidChangeState.fire();
},
);
}));

this.rawListeners.add(this.raw.onDidThread(event => {
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 });
}
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);
}
});
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 });
}
}
}));

this.rawListeners.add(this.raw.onDidTerminateDebugee(async event => {
Expand All @@ -1067,23 +1082,25 @@ export class DebugSession implements IDebugSession, IDisposable {
}));

this.rawListeners.add(this.raw.onDidContinued(event => {
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();
});
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();
}));

const outputQueue = new Queue<void>();
Expand Down Expand Up @@ -1366,3 +1383,95 @@ export class DebugSession implements IDebugSession, IDisposable {
}
}
}

/**
* 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);
}
}
}
}
}
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/debug/common/debugModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,13 +1324,13 @@ export class DebugModel extends Disposable implements IDebugModel {
return;
}

refreshTopOfCallstack(thread: Thread): { topCallStack: Promise<void>; wholeCallStack: Promise<void> } {
refreshTopOfCallstack(thread: Thread, fetchFullStack = true): { topCallStack: Promise<void>; wholeCallStack: Promise<void> } {
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(() => {
if (!this.schedulers.has(thread.getId())) {
if (!this.schedulers.has(thread.getId()) && fetchFullStack) {
const deferred = new DeferredPromise<void>();
this.schedulers.set(thread.getId(), {
completeDeferred: deferred,
Expand Down