Skip to content

Commit 30875cc

Browse files
committed
feat: request async stacks lazily to speed up terminal commands
tl;dr of the investigation from #120 was that the big culprit of slower commands in the terminal were async stack traces. In this PR, we introduce logic, enabled by default in the terminal, which does not turn on async stack tracking until a breakpoint is hit or the debugger pauses, whichever happens first. This allows standard build commands like Webpack to run much faster if the developer is using the debugger terminal like any other terminal and isn't interested in debugging the current task. On my machine it reduces debug overhead from 120% to 30%: ``` PS > Measure-Command { webpack } Normal Terminal: 12.46 / 12.16 / 13.07 Debugger Terminal (master): 22.58 / 23.33 / 22.82 (2.2x) Debugger Terminal (this PR): 16.90 / 16.86 / 16.71 (1.3x) ```
1 parent fd559e0 commit 30875cc

16 files changed

+859
-215
lines changed

scripts/generate-cdp-api.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ async function generate() {
4242
result.push(` * Auto-generated by generate-cdp-api.js, do not edit manually. *`);
4343
result.push(` ****************************************************************/`);
4444
result.push(``);
45+
result.push(`import { IDisposable } from '../common/disposable'; `);
46+
result.push(``);
4547
result.push(`export namespace Cdp {`);
4648
result.push(` export type integer = number;`);
4749
interfaceSeparator();
@@ -104,7 +106,7 @@ async function generate() {
104106
for (const event of events) {
105107
apiSeparator();
106108
appendText(event.description, ' ');
107-
result.push(` on(event: '${event.name}', listener: (event: ${name}.${toTitleCase(event.name)}Event) => void): void;`);
109+
result.push(` on(event: '${event.name}', listener: (event: ${name}.${toTitleCase(event.name)}Event) => void): IDisposable;`);
108110
}
109111
result.push(` }`);
110112

src/adapter/asyncStackPolicy.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { IDisposable, noOpDisposable, DisposableList } from '../common/disposable';
6+
import Cdp from '../cdp/api';
7+
import { AsyncStackMode } from '../configuration';
8+
import { EventEmitter } from '../common/events';
9+
10+
/**
11+
* Controls when async stack traces are enabled in the debugee, either
12+
* at start or only when we resolve a breakpoint.
13+
*
14+
* This is useful because requesting async stacktraces increases bookkeeping
15+
* that V8 needs to do and can cause significant slowdowns.
16+
*/
17+
export interface IAsyncStackPolicy {
18+
/**
19+
* Installs the policy on the given CDP API.
20+
*/
21+
connect(cdp: Cdp.Api): Promise<IDisposable>;
22+
}
23+
24+
const disabled: IAsyncStackPolicy = { connect: async () => noOpDisposable };
25+
26+
const eager = (maxDepth: number): IAsyncStackPolicy => ({
27+
async connect(cdp) {
28+
await cdp.Debugger.setAsyncCallStackDepth({ maxDepth });
29+
return noOpDisposable;
30+
},
31+
});
32+
33+
const onceBp = (maxDepth: number): IAsyncStackPolicy => {
34+
const onEnable: EventEmitter<void> | undefined = new EventEmitter<void>();
35+
let enabled = false;
36+
const tryEnable = () => {
37+
if (!enabled) {
38+
enabled = true;
39+
onEnable.fire();
40+
}
41+
};
42+
43+
return {
44+
async connect(cdp) {
45+
if (enabled) {
46+
await cdp.Debugger.setAsyncCallStackDepth({ maxDepth });
47+
return noOpDisposable;
48+
}
49+
50+
const disposable = new DisposableList();
51+
52+
disposable.push(
53+
// Another session enabled breakpoints. Turn this on as well, e.g. if
54+
// we have a parent page and webworkers, when we debug the webworkers
55+
// should also have their async stacks turned on.
56+
onEnable.event(() => {
57+
disposable.dispose();
58+
cdp.Debugger.setAsyncCallStackDepth({ maxDepth });
59+
}),
60+
// when a breakpoint resolves, turn on stacks because we're likely to
61+
// pause sooner or later
62+
cdp.Debugger.on('breakpointResolved', tryEnable),
63+
// start collecting on a pause event. This can be from source map
64+
// instrumentation, entrypoint breakpoints, debugger statements, or user
65+
// defined breakpoints. Instrumentation points happen all the time and
66+
// can be ignored. For others, including entrypoint breaks (which
67+
// indicate there's a user break somewhere in the file) we should turn on.
68+
cdp.Debugger.on('paused', evt => {
69+
if (evt.reason !== 'instrumentation') {
70+
tryEnable();
71+
}
72+
}),
73+
);
74+
75+
return disposable;
76+
},
77+
};
78+
};
79+
80+
const defaultPolicy = eager(32);
81+
82+
export const getAsyncStackPolicy = (mode: AsyncStackMode) => {
83+
if (mode === false) {
84+
return disabled;
85+
}
86+
87+
if (mode === true) {
88+
return defaultPolicy;
89+
}
90+
91+
if ('onAttach' in mode) {
92+
return eager(mode.onAttach);
93+
}
94+
95+
if ('onceBreakpointResolved' in mode) {
96+
return onceBp(mode.onceBreakpointResolved);
97+
}
98+
99+
return defaultPolicy;
100+
};

src/adapter/debugAdapter.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5-
import { IDisposable } from '../common/events';
65
import * as nls from 'vscode-nls';
76
import Dap from '../dap/api';
87
import * as sourceUtils from '../common/sourceUtils';
@@ -23,6 +22,10 @@ import { join } from 'path';
2322
import { IDeferred, getDeferred } from '../common/promiseUtil';
2423
import { SourceMapCache } from './sourceMapCache';
2524
import { logPerf } from '../telemetry/performance';
25+
import { IAsyncStackPolicy } from './asyncStackPolicy';
26+
import { logger } from '../common/logging/logger';
27+
import { LogTag } from '../common/logging';
28+
import { DisposableList } from '../common/disposable';
2629

2730
const localize = nls.loadMessageBundle();
2831

@@ -32,7 +35,7 @@ export class DebugAdapter {
3235
readonly dap: Dap.Api;
3336
readonly sourceContainer: SourceContainer;
3437
readonly breakpointManager: BreakpointManager;
35-
private _disposables: IDisposable[] = [];
38+
private _disposables = new DisposableList();
3639
private _pauseOnExceptionsState: PauseOnExceptionsState = 'none';
3740
private _customBreakpoints = new Set<string>();
3841
private _thread: Thread | undefined;
@@ -42,6 +45,7 @@ export class DebugAdapter {
4245
dap: Dap.Api,
4346
rootPath: string | undefined,
4447
sourcePathResolver: ISourcePathResolver,
48+
private readonly asyncStackPolicy: IAsyncStackPolicy,
4549
private readonly launchConfig: AnyLaunchConfiguration,
4650
private readonly _rawTelemetryReporter: IRawTelemetryReporter,
4751
) {
@@ -283,6 +287,12 @@ export class DebugAdapter {
283287
);
284288
for (const breakpoint of this._customBreakpoints)
285289
this._thread.updateCustomBreakpoint(breakpoint, true);
290+
291+
this.asyncStackPolicy
292+
.connect(cdp)
293+
.then(d => this._disposables.push(d))
294+
.catch(err => logger.error(LogTag.Internal, 'Error enabling async stacks', err));
295+
286296
this._thread.setPauseOnExceptionsState(this._pauseOnExceptionsState);
287297
this.breakpointManager.setThread(this._thread);
288298
return this._thread;
@@ -357,7 +367,6 @@ export class DebugAdapter {
357367
}
358368

359369
dispose() {
360-
for (const disposable of this._disposables) disposable.dispose();
361-
this._disposables = [];
370+
this._disposables.dispose();
362371
}
363372
}

src/adapter/threads.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,9 +455,6 @@ export class Thread implements IVariableStoreDelegate {
455455

456456
this._ensureDebuggerEnabledAndRefreshDebuggerId();
457457
this._delegate.initialize();
458-
if (this.launchConfig.showAsyncStacks) {
459-
this._cdp.Debugger.setAsyncCallStackDepth({ maxDepth: 32 });
460-
}
461458
const scriptSkipper = this._delegate.skipFiles();
462459
if (scriptSkipper) {
463460
// Note: here we assume that source container does only have a single thread.

src/binder.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as errors from './dap/errors';
2424
import { ILauncher, ILaunchResult, ITarget } from './targets/targets';
2525
import { RawTelemetryReporterToDap } from './telemetry/telemetryReporter';
2626
import { filterErrorsReportedToTelemetry } from './telemetry/unhandledErrorReporter';
27+
import { IAsyncStackPolicy, getAsyncStackPolicy } from './adapter/asyncStackPolicy';
2728

2829
const localize = nls.loadMessageBundle();
2930

@@ -47,6 +48,7 @@ export class Binder implements IDisposable {
4748
private _launchParams?: AnyLaunchConfiguration;
4849
private _rawTelemetryReporter: RawTelemetryReporterToDap | undefined;
4950
private _clientCapabilities: Dap.InitializeParams | undefined;
51+
private _asyncStackPolicy?: IAsyncStackPolicy;
5052

5153
constructor(
5254
delegate: IBinderDelegate,
@@ -240,10 +242,16 @@ export class Binder implements IDisposable {
240242
if (!cdp) return;
241243
const connection = await this._delegate.acquireDap(target);
242244
const dap = await connection.dap();
245+
246+
if (!this._asyncStackPolicy) {
247+
this._asyncStackPolicy = getAsyncStackPolicy(this._launchParams!.showAsyncStacks);
248+
}
249+
243250
const debugAdapter = new DebugAdapter(
244251
dap,
245252
this._launchParams?.rootPath || undefined,
246253
target.sourcePathResolver(),
254+
this._asyncStackPolicy,
247255
this._launchParams!,
248256
this._rawTelemetryReporter!,
249257
);

src/build/generate-contributions.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,33 @@ const baseConfigurationAttributes: ConfigurationAttributes<IBaseConfiguration> =
102102
default: false,
103103
},
104104
showAsyncStacks: {
105-
type: 'boolean',
106105
description: refString('node.showAsyncStacks.description'),
107106
default: true,
107+
oneOf: [
108+
{
109+
type: 'boolean',
110+
},
111+
{
112+
type: 'object',
113+
required: ['onAttach'],
114+
properties: {
115+
onAttach: {
116+
type: 'number',
117+
default: 32,
118+
},
119+
},
120+
},
121+
{
122+
type: 'object',
123+
required: ['onceBreakpointResolved'],
124+
properties: {
125+
onceBreakpointResolved: {
126+
type: 'number',
127+
default: 32,
128+
},
129+
},
130+
},
131+
],
108132
},
109133
skipFiles: {
110134
type: 'array',

0 commit comments

Comments
 (0)