Skip to content

Commit d8298aa

Browse files
committed
feat: support new canBeRestarted flag from the runtime
Fixes #1283
1 parent a6e4d57 commit d8298aa

File tree

7 files changed

+435
-107
lines changed

7 files changed

+435
-107
lines changed

src/adapter/console/textualMessage.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { formatMessage } from '../messageFormat';
1111
import { messageFormatters, previewAsObject } from '../objectPreview';
1212
import { AnyObject } from '../objectPreview/betterTypes';
1313
import { IUiLocation } from '../sources';
14-
import { StackTrace } from '../stackTrace';
14+
import { StackFrame, StackTrace } from '../stackTrace';
1515
import { Thread } from '../threads';
1616
import { IConsoleMessage } from './consoleMessage';
1717

@@ -41,8 +41,12 @@ export abstract class TextualMessage<T extends { stackTrace?: Cdp.Runtime.StackT
4141
}
4242

4343
let firstExistingLocation: IUiLocation | undefined;
44-
for (let i = 0; i < stackTrace.frames.length; i++) {
45-
const uiLocation = await stackTrace.frames[i].uiLocation();
44+
for (const frame of stackTrace.frames) {
45+
if (!(frame instanceof StackFrame)) {
46+
continue;
47+
}
48+
49+
const uiLocation = await frame.uiLocation();
4650
if (!uiLocation) {
4751
continue;
4852
}

src/adapter/smartStepping.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { inject, injectable } from 'inversify';
66
import { ILogger, LogTag } from '../common/logging';
7+
import { isInstanceOf } from '../common/objUtils';
78
import { AnyLaunchConfiguration } from '../configuration';
89
import { isSourceWithMap, UnmappedReason } from './sources';
910
import { StackFrame } from './stackTrace';
@@ -78,8 +79,8 @@ export class SmartStepper {
7879
return;
7980
}
8081

81-
const frame = (await pausedDetails.stackTrace.loadFrames(1))[0];
82-
const should = await shouldStepOverStackFrame(frame);
82+
const frame = (await pausedDetails.stackTrace.loadFrames(1)).find(isInstanceOf(StackFrame));
83+
const should = frame && (await shouldStepOverStackFrame(frame));
8384
if (should === StackFrameStepOverReason.NotStepped) {
8485
this.resetSmartStepCount();
8586
return;

src/adapter/stackTrace.ts

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,28 @@ import { IExtraProperty, IScopeRef, IVariableContainer } from './variableStore';
1616

1717
const localize = nls.loadMessageBundle();
1818

19+
export interface IFrameElement {
20+
/** Formats the stack element as V8 would format it */
21+
formatAsNative(): Promise<string>;
22+
/** Pretty formats the stack element as text */
23+
format(): Promise<string>;
24+
/** Formats the element for DAP */
25+
toDap(format?: Dap.StackFrameFormat): Promise<Dap.StackFrame>;
26+
}
27+
28+
type FrameElement = StackFrame | AsyncSeparator;
29+
1930
export class StackTrace {
20-
public readonly frames: StackFrame[] = [];
31+
public readonly frames: FrameElement[] = [];
2132
private _frameById: Map<number, StackFrame> = new Map();
2233
private _asyncStackTraceId?: Cdp.Runtime.StackTraceId;
2334
private _lastFrameThread?: Thread;
2435

2536
public static fromRuntime(thread: Thread, stack: Cdp.Runtime.StackTrace): StackTrace {
2637
const result = new StackTrace(thread);
27-
for (let frameNo = 0; frameNo < stack.callFrames.length; frameNo++) {
28-
if (!stack.callFrames[frameNo].url.endsWith(SourceConstants.InternalExtension)) {
29-
result.frames.push(StackFrame.fromRuntime(thread, stack.callFrames[frameNo], false));
38+
for (const frame of stack.callFrames) {
39+
if (!frame.url.endsWith(SourceConstants.InternalExtension)) {
40+
result.frames.push(StackFrame.fromRuntime(thread, frame, false));
3041
}
3142
}
3243

@@ -88,7 +99,7 @@ export class StackTrace {
8899
this._lastFrameThread = thread;
89100
}
90101

91-
async loadFrames(limit: number): Promise<StackFrame[]> {
102+
async loadFrames(limit: number): Promise<FrameElement[]> {
92103
while (this.frames.length < limit && this._asyncStackTraceId) {
93104
if (this._asyncStackTraceId.debuggerId)
94105
this._lastFrameThread = Thread.threadForDebuggerId(this._asyncStackTraceId.debuggerId);
@@ -117,9 +128,10 @@ export class StackTrace {
117128
stackTrace.callFrames.shift();
118129

119130
if (stackTrace.callFrames.length) {
120-
this._appendFrame(StackFrame.asyncSeparator(thread, stackTrace.description || 'async'));
121-
for (const callFrame of stackTrace.callFrames)
131+
this._appendFrame(new AsyncSeparator(stackTrace.description || 'async'));
132+
for (const callFrame of stackTrace.callFrames) {
122133
this._appendFrame(StackFrame.fromRuntime(thread, callFrame, true));
134+
}
123135
}
124136

125137
if (stackTrace.parentId) {
@@ -131,9 +143,11 @@ export class StackTrace {
131143
}
132144
}
133145

134-
_appendFrame(frame: StackFrame) {
146+
_appendFrame(frame: FrameElement) {
135147
this.frames.push(frame);
136-
this._frameById.set(frame._id, frame);
148+
if (frame instanceof StackFrame) {
149+
this._frameById.set(frame._id, frame);
150+
}
137151
}
138152

139153
async formatAsNative(): Promise<string> {
@@ -174,7 +188,23 @@ interface IScope {
174188
callFrameId: string;
175189
}
176190

177-
export class StackFrame {
191+
export class AsyncSeparator implements IFrameElement {
192+
constructor(private readonly label = 'async') {}
193+
194+
public async toDap(): Promise<Dap.StackFrame> {
195+
return { name: this.label, id: 0, line: 0, column: 0, presentationHint: 'label' };
196+
}
197+
198+
public async formatAsNative(): Promise<string> {
199+
return ` --- ${this.label} ---`;
200+
}
201+
202+
public async format(): Promise<string> {
203+
return `◀ ${this.label} ▶`;
204+
}
205+
}
206+
207+
export class StackFrame implements IFrameElement {
178208
private static _lastFrameId = 0;
179209

180210
_id: number;
@@ -184,7 +214,6 @@ export class StackFrame {
184214
| Promise<IPreferredUiLocation | undefined>
185215
| IPreferredUiLocation
186216
| undefined;
187-
private _isAsyncSeparator = false;
188217
private _scope: IScope | undefined;
189218
private _thread: Thread;
190219

@@ -198,11 +227,11 @@ export class StackFrame {
198227
callFrame: Cdp.Runtime.CallFrame,
199228
isAsync: boolean,
200229
): StackFrame {
201-
return new StackFrame(thread, callFrame.functionName, thread.rawLocation(callFrame), isAsync);
230+
return new StackFrame(thread, callFrame, thread.rawLocation(callFrame), isAsync);
202231
}
203232

204233
static fromDebugger(thread: Thread, callFrame: Cdp.Debugger.CallFrame): StackFrame {
205-
const result = new StackFrame(thread, callFrame.functionName, thread.rawLocation(callFrame));
234+
const result = new StackFrame(thread, callFrame, thread.rawLocation(callFrame));
206235
result._scope = {
207236
chain: callFrame.scopeChain,
208237
thisObject: callFrame.this,
@@ -214,37 +243,32 @@ export class StackFrame {
214243
return result;
215244
}
216245

217-
static asyncSeparator(thread: Thread, name: string): StackFrame {
218-
// todo@connor4312: this should probably be a different type of object
219-
const result = new StackFrame(
220-
thread,
221-
name,
222-
{ lineNumber: 1, columnNumber: 1, scriptId: '' },
223-
true,
224-
);
225-
result._isAsyncSeparator = true;
226-
return result;
227-
}
228-
229246
constructor(
230247
thread: Thread,
231-
name: string,
248+
private readonly callFrame: Cdp.Debugger.CallFrame | Cdp.Runtime.CallFrame,
232249
rawLocation: RawLocation,
233250
private readonly isAsync = false,
234251
) {
235252
this._id = ++StackFrame._lastFrameId;
236-
this._name = name || '<anonymous>';
253+
this._name = callFrame.functionName || '<anonymous>';
237254
this._rawLocation = rawLocation;
238255
this.uiLocation = once(() => thread.rawLocationToUiLocation(rawLocation));
239256
this._thread = thread;
240257
}
241258

259+
/**
260+
* Gets whether the runtime explicitly said this frame can be restarted.
261+
*/
262+
public get canExplicitlyBeRestarted() {
263+
return !!(this.callFrame as Cdp.Debugger.CallFrame).canBeRestarted;
264+
}
265+
242266
/**
243267
* Gets whether this stackframe is at the same position as the other frame.
244268
*/
245-
public equivalentTo(other: StackFrame | undefined) {
269+
public equivalentTo(other: unknown) {
246270
return (
247-
other &&
271+
other instanceof StackFrame &&
248272
other._rawLocation.columnNumber === this._rawLocation.columnNumber &&
249273
other._rawLocation.lineNumber === this._rawLocation.lineNumber &&
250274
other._rawLocation.scriptId === this._rawLocation.scriptId
@@ -336,15 +360,12 @@ export class StackFrame {
336360
return { scopes };
337361
}
338362

363+
/** @inheritdoc */
339364
async toDap(format?: Dap.StackFrameFormat): Promise<Dap.StackFrame> {
340365
const uiLocation = await this.uiLocation();
341366
const source = uiLocation ? await uiLocation.source.toDap() : undefined;
342367
const isSmartStepped = await shouldStepOverStackFrame(this);
343-
const presentationHint = this._isAsyncSeparator
344-
? 'label'
345-
: isSmartStepped
346-
? 'deemphasize'
347-
: 'normal';
368+
const presentationHint = isSmartStepped ? 'deemphasize' : 'normal';
348369
if (isSmartStepped && source) {
349370
source.origin =
350371
isSmartStepped === StackFrameStepOverReason.SmartStep
@@ -374,12 +395,14 @@ export class StackFrame {
374395
column,
375396
source,
376397
presentationHint,
377-
canRestart: !this.isAsync,
398+
// If `canBeRestarted` is present, use that
399+
// https://github.com/microsoft/vscode-js-debug/issues/1283
400+
canRestart: (this.callFrame as Cdp.Debugger.CallFrame).canBeRestarted ?? !this.isAsync,
378401
} as Dap.StackFrame;
379402
}
380403

404+
/** @inheritdoc */
381405
async formatAsNative(): Promise<string> {
382-
if (this._isAsyncSeparator) return ` --- ${this._name} ---`;
383406
const uiLocation = await this.uiLocation();
384407
const url =
385408
(await uiLocation?.source.existingAbsolutePath()) ||
@@ -389,8 +412,8 @@ export class StackFrame {
389412
return ` at ${this._name} (${url}:${lineNumber}:${columnNumber})`;
390413
}
391414

415+
/** @inheritdoc */
392416
async format(): Promise<string> {
393-
if (this._isAsyncSeparator) return `◀ ${this._name} ▶`;
394417
const uiLocation = await this.uiLocation();
395418
const prettyName = (await uiLocation?.source.prettyName()) || '<unknown>';
396419
const anyLocation = uiLocation || this._rawLocation;

src/adapter/threads.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DebugType } from '../common/contributionUtils';
88
import { EventEmitter } from '../common/events';
99
import { HrTime } from '../common/hrnow';
1010
import { ILogger, LogTag } from '../common/logging';
11+
import { isInstanceOf } from '../common/objUtils';
1112
import { Base1Position } from '../common/positions';
1213
import { delay, getDeferred, IDeferred } from '../common/promiseUtil';
1314
import { IRenameProvider } from '../common/sourceMaps/renameProvider';
@@ -303,12 +304,26 @@ export class Thread implements IVariableStoreLocationProvider {
303304
);
304305
}
305306

306-
await this._cdp.Debugger.restartFrame({ callFrameId });
307+
const ok = !!(await this._cdp.Debugger.restartFrame({ callFrameId }));
308+
if (!ok) {
309+
return errors.createUserError(
310+
localize('error.unknownRestartError', 'Frame could not be restarted'),
311+
);
312+
}
313+
307314
this._expectedPauseReason = {
308315
reason: 'frame_entry',
309316
description: localize('reason.description.restart', 'Paused on frame entry'),
310317
};
311-
await this._cdp.Debugger.stepInto({});
318+
319+
// Chromium versions before 104 didn't have an explicit `canBeRestarted`
320+
// flag on their call frame. And on those versions, when we `restartFrame`,
321+
// we need to manually `stepInto` to unpause. However, with 104, restarting
322+
// the frame will automatically resume execution.
323+
if (!stackFrame.canExplicitlyBeRestarted) {
324+
await this._cdp.Debugger.stepInto({});
325+
}
326+
312327
return {};
313328
}
314329

@@ -770,7 +785,8 @@ export class Thread implements IVariableStoreLocationProvider {
770785
return false;
771786
}
772787

773-
const first = await trace.frames[0]?.uiLocation();
788+
const firstFrame = trace.frames[0];
789+
const first = firstFrame instanceof StackFrame && (await firstFrame.uiLocation());
774790
if (!first) {
775791
return false;
776792
}
@@ -792,9 +808,14 @@ export class Thread implements IVariableStoreLocationProvider {
792808
if (!stackLocations) {
793809
// for some reason, if this is assigned directly to stackLocations,
794810
// then TS will think it can still be undefined below
795-
const x = await trace
796-
.loadFrames(excludedCallerSearchDepth)
797-
.then(frames => Promise.all(frames.slice(1).map(f => f.uiLocation())));
811+
const x = await trace.loadFrames(excludedCallerSearchDepth).then(frames =>
812+
Promise.all(
813+
frames
814+
.slice(1)
815+
.filter(isInstanceOf(StackFrame))
816+
.map(f => f.uiLocation()),
817+
),
818+
);
798819
stackLocations = x;
799820
}
800821

@@ -1431,7 +1452,7 @@ export class Thread implements IVariableStoreLocationProvider {
14311452
Promise.all(
14321453
details.hitBreakpoints
14331454
.map(bp => this._breakpointManager._resolvedBreakpoints.get(bp))
1434-
.filter((bp): bp is UserDefinedBreakpoint => bp instanceof UserDefinedBreakpoint)
1455+
.filter(isInstanceOf(UserDefinedBreakpoint))
14351456
.map(r => r.untilSetCompleted().then(() => r.dapId)),
14361457
),
14371458
]);

0 commit comments

Comments
 (0)