Skip to content
Merged
12 changes: 9 additions & 3 deletions src/client/datascience/debugLocationTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
'use strict';
import { DebugAdapterTracker, Event, EventEmitter } from 'vscode';
import { DebugProtocol } from 'vscode-debugprotocol';
import { DebugStackTraceTracker } from './jupyter/debugStackTraceTracker';

import { IDebugLocation } from './types';

// When a python debugging session is active keep track of the current debug location
export class DebugLocationTracker implements DebugAdapterTracker {
export class DebugLocationTracker extends DebugStackTraceTracker implements DebugAdapterTracker {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we still need implements DebugAdapterTracker?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, because DebugLocationTracker has other custom message handling besides what is handled in the base class, so it still needs to be able to intercept DAP messages via onDidSendMessage and onWillReceiveMessage.

private waitingForStackTrace: boolean = false;
private _debugLocation: IDebugLocation | undefined;
private debugLocationUpdatedEvent: EventEmitter<void> = new EventEmitter<void>();
private sessionEndedEmitter: EventEmitter<DebugLocationTracker> = new EventEmitter<DebugLocationTracker>();

constructor(private _sessionId: string) {
super();
this.DebugLocation = undefined;
}

Expand All @@ -34,7 +36,8 @@ export class DebugLocationTracker implements DebugAdapterTracker {
}

// tslint:disable-next-line:no-any
public onDidSendMessage(message: DebugProtocol.ProtocolMessage) {
public onDidSendMessage(message: DebugProtocol.Response) {
super.onDidSendMessage(message);
if (this.isStopEvent(message)) {
// Some type of stop, wait to see our next stack trace to find our location
this.waitingForStackTrace = true;
Expand Down Expand Up @@ -88,7 +91,10 @@ export class DebugLocationTracker implements DebugAdapterTracker {
const responseMessage = message as DebugProtocol.Response;
if (responseMessage.command === 'stackTrace') {
const messageBody = responseMessage.body;
if (messageBody.stackFrames.length > 0) {
if (
messageBody.stackFrames.length > 0 &&
this.stackFrameRequestSequenceNumber === responseMessage.request_seq
) {
const lineNumber = messageBody.stackFrames[0].line;
const fileName = this.normalizeFilePath(messageBody.stackFrames[0].source.path);
const column = messageBody.stackFrames[0].column;
Expand Down
29 changes: 29 additions & 0 deletions src/client/datascience/jupyter/debugStackTraceTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DebugProtocol } from 'vscode-debugprotocol';

export class DebugStackTraceTracker {
protected topMostFrameId = 0;
protected stackFrameRequestSequenceNumber: number = -1; // Keep track of the sequence number

public onDidSendMessage(message: DebugProtocol.Response) {
if (
message.type === 'response' &&
message.command === 'stackTrace' &&
message.body.stackFrames[0] &&
message.request_seq === this.stackFrameRequestSequenceNumber
) {
// This should be the top frame. We need to use this to compute the value of a variable
this.topMostFrameId = message.body.stackFrames[0].id;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This feels inconsistent because this state is only needed in debuggerVariables.ts, but here I've made it shared, and the equivalent state for debugLocationTracker.ts still lives in that class and not here.

Wanted people's thoughts on whether I should either

  1. move debugLocationTracker.ts's handling for a stackTrace response here, or
  2. move this function back into debuggerVariables

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I vote for 1.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This class's job is to track the list of stack frames. That shouldn't be done elsewhere. I'd say you could probably put almost all (if not all) of the code from the DebugLocationTracker here. Isn't the DebuggerVariable class really just a subclass of something that's tracking stack frames?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Meaning you'd have this class hierarchy instead:

DebugVariables extends DebugLocationTracker
DebugLocationTracker

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given the name / function of this class as I read it #1 makes more sense.

}
}

public onWillReceiveMessage(message: DebugProtocol.Request) {
if (message.type === 'request' && message.command === 'stackTrace' && message.arguments.startFrame === 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From the specification this is not quite right I believe. Looks like startFrame === 0 or startFrame not being defined means to fetch everything. I think this might not handle the undefined case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, I missed that when looking at the spec. Thank you!

// VSCode sometimes sends multiple stackTrace requests. The true topmost frame is determined
// based on the response to a stackTrace request where the startFrame is 0 (i.e. this request
// retrieves all frames). Here, remember the sequence number of the outgoing request whose
// startFrame === 0, and update this.topMostFrameId only when we receive the response with a
// matching sequence number.
this.stackFrameRequestSequenceNumber = message.seq;
}
}
}
19 changes: 7 additions & 12 deletions src/client/datascience/jupyter/debuggerVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,25 @@ import {
IJupyterVariablesResponse,
INotebook
} from '../types';
import { DebugStackTraceTracker } from './debugStackTraceTracker';

const DataViewableTypes: Set<string> = new Set<string>(['DataFrame', 'list', 'dict', 'ndarray', 'Series']);
const KnownExcludedVariables = new Set<string>(['In', 'Out', 'exit', 'quit']);

@injectable()
export class DebuggerVariables implements IConditionalJupyterVariables, DebugAdapterTracker {
export class DebuggerVariables extends DebugStackTraceTracker
implements IConditionalJupyterVariables, DebugAdapterTracker {
private refreshEventEmitter = new EventEmitter<void>();
private lastKnownVariables: IJupyterVariable[] = [];
private topMostFrameId = 0;
private importedIntoKernel = new Set<string>();
private watchedNotebooks = new Map<string, Disposable[]>();
private debuggingStarted = false;
constructor(
@inject(IJupyterDebugService) @named(Identifiers.MULTIPLEXING_DEBUGSERVICE) private debugService: IDebugService,
@inject(IConfigurationService) private configService: IConfigurationService
) {}
) {
super();
}

public get refreshRequired(): Event<void> {
return this.refreshEventEmitter.event;
Expand Down Expand Up @@ -166,6 +169,7 @@ export class DebuggerVariables implements IConditionalJupyterVariables, DebugAda

// tslint:disable-next-line: no-any
public onDidSendMessage(message: any) {
super.onDidSendMessage(message);
// When the initialize response comes back, indicate we have started.
if (message.type === 'response' && message.command === 'initialize') {
this.debuggingStarted = true;
Expand All @@ -174,9 +178,6 @@ export class DebuggerVariables implements IConditionalJupyterVariables, DebugAda
// tslint:disable-next-line: no-suspicious-comment
// TODO: Figure out what resource to use
this.updateVariables(undefined, message as DebugProtocol.VariablesResponse);
} else if (message.type === 'response' && message.command === 'stackTrace') {
// This should be the top frame. We need to use this to compute the value of a variable
this.updateStackFrame(message as DebugProtocol.StackTraceResponse);
} else if (message.type === 'event' && message.event === 'terminated') {
// When the debugger exits, make sure the variables are cleared
this.lastKnownVariables = [];
Expand Down Expand Up @@ -240,12 +241,6 @@ export class DebuggerVariables implements IConditionalJupyterVariables, DebugAda
}
}

private updateStackFrame(stackResponse: DebugProtocol.StackTraceResponse) {
if (stackResponse.body.stackFrames[0]) {
this.topMostFrameId = stackResponse.body.stackFrames[0].id;
}
}

private async getFullVariable(variable: IJupyterVariable, notebook: INotebook): Promise<IJupyterVariable> {
// See if we imported or not into the kernel our special function
await this.importDataFrameScripts(notebook);
Expand Down