Skip to content

Commit

Permalink
feat: make sourcemap toggle work for wasm/wat, add disable (#1810)
Browse files Browse the repository at this point in the history
  • Loading branch information
connor4312 authored Sep 20, 2023
1 parent 42b6c74 commit 613b9ea
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he

## Nightly (only)

- feat: enable DWARF-based WebAssembly debugging ([#1789](https://github.com/microsoft/vscode-js-debug/issues/1789))
- feat: show class names of methods in call stack view ([#1770](https://github.com/microsoft/vscode-js-debug/issues/1770))
- fix: edge devtools incorrectly ask for local forwarding ([vscode#193110](https://github.com/microsoft/vscode/issues/193110))

Expand Down
8 changes: 8 additions & 0 deletions OPTIONS.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"add.browser.breakpoint": "Add Browser Breakpoint",
"attach.node.process": "Attach to Node Process",
"base.cascadeTerminateToConfigurations.label": "A list of debug sessions which, when this debug session is terminated, will also be stopped.",
"base.enableDWARF.label": "Toggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the `ms-vscode.wasm-dwarf-debugging` extension to function.",
"browser.address.description": "IP address or hostname the debugged browser is listening on.",
"browser.attach.port.description": "Port to use to remote debugging the browser, given as `--remote-debugging-port` when launching the browser.",
"browser.baseUrl.description": "Base URL to resolve paths baseUrl. baseURL is trimmed when mapping URLs to the files on disk. Defaults to the launch URL domain.",
Expand Down Expand Up @@ -203,4 +204,4 @@
"trace.logFile.description": "Configures where on disk logs are written.",
"trace.stdio.description": "Whether to return trace data from the launched application or browser.",
"workspaceTrust.description": "Trust is required to debug code in this workspace."
}
}
39 changes: 35 additions & 4 deletions src/adapter/dwarf/wasmSymbolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IDisposable } from '../../common/disposable';
import { ILogger, LogTag } from '../../common/logging';
import { flatten, once } from '../../common/objUtils';
import { Base0Position, IPosition, Range } from '../../common/positions';
import { AnyLaunchConfiguration } from '../../configuration';
import { StepDirection } from '../pause';
import { getSourceSuffix } from '../templates';
import { IDwarfModuleProvider } from './dwarfModuleProvider';
Expand All @@ -35,12 +36,17 @@ export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable {
@inject(IDwarfModuleProvider) private readonly dwarf: IDwarfModuleProvider,
@inject(ICdpApi) private readonly cdp: Cdp.Api,
@inject(ILogger) private readonly logger: ILogger,
@inject(AnyLaunchConfiguration) private readonly launchConfig: AnyLaunchConfiguration,
) {}

public async loadWasmSymbols(script: Cdp.Debugger.ScriptParsedEvent): Promise<IWasmSymbols> {
if (!this.launchConfig.enableDWARF) {
return this.defaultSymbols(script);
}

const rpc = await this.getWorker();
if (!rpc) {
const syms = new DecompiledWasmSymbols(script, this.cdp, []);
const syms = this.defaultSymbols(script);
// disassembly is a good signal for a prompt, since that means a user
// will have stepped into and be looking at webassembly code.
syms.onDidDisassemble = this.doPrompt;
Expand All @@ -60,12 +66,12 @@ export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable {
: undefined,
});
} catch {
return new DecompiledWasmSymbols(script, this.cdp, []);
return this.defaultSymbols(script);
}

if (!(result instanceof Array) || result.length === 0) {
rpc.sendMessage('removeRawModule', moduleId); // no await necessary
return new DecompiledWasmSymbols(script, this.cdp, []);
return this.defaultSymbols(script);
}

this.logger.info(LogTag.SourceMapParsing, 'parsed files from wasm', { files: result });
Expand All @@ -79,6 +85,10 @@ export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable {
this.worker = undefined;
}

private defaultSymbols(script: Cdp.Debugger.ScriptParsedEvent) {
return new DecompiledWasmSymbols(script, this.cdp, []);
}

private async getBytecode(scriptId: string) {
const source = await this.cdp.Debugger.getScriptSource({ scriptId });
const bytecode = source?.bytecode;
Expand Down Expand Up @@ -178,6 +188,16 @@ export interface IWasmSymbols extends IDisposable {
compiledPosition: IPosition,
): Promise<{ url: string; position: IPosition } | undefined>;

/**
* Gets the position in the disassembly for the given position in compiled code.
*
* Following CDP semantics, it assumes the column is being the byte offset
* in webassembly. However, we encode the inline frame index in the line.
*/
disassembledPositionFor(
compiledPosition: IPosition,
): Promise<{ url: string; position: IPosition } | undefined>;

/**
* Gets the compiled position for the given position in source code.
*
Expand Down Expand Up @@ -247,7 +267,14 @@ class DecompiledWasmSymbols implements IWasmSymbols {
}

/** @inheritdoc */
public async originalPositionFor(
public originalPositionFor(
compiledPosition: IPosition,
): Promise<{ url: string; position: IPosition } | undefined> {
return this.disassembledPositionFor(compiledPosition);
}

/** @inheritdoc */
public async disassembledPositionFor(
compiledPosition: IPosition,
): Promise<{ url: string; position: IPosition } | undefined> {
const { byteOffsetsOfLines } = await this.doDisassemble();
Expand Down Expand Up @@ -385,6 +412,10 @@ class WasmSymbols extends DecompiledWasmSymbols {
sourceUrl: string,
sourcePosition: IPosition,
): Promise<IPosition[]> {
if (sourceUrl === this.decompiledUrl) {
return super.compiledPositionFor(sourceUrl, sourcePosition);
}

const { lineNumber, columnNumber } = sourcePosition.base0;
const locations = await this.rpc.sendMessage('sourceLocationToRawLocation', {
lineNumber,
Expand Down
5 changes: 5 additions & 0 deletions src/adapter/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ export const isSourceWithWasm = (
): source is ISourceWithMap<IWasmLocationProvider> =>
isSourceWithMap(source) && source.sourceMap.type === SourceLocationType.WasmSymbols;

export const isWasmSymbols = (
source: SourceMap | IWasmSymbols | undefined,
): source is IWasmSymbols =>
!!source && typeof (source as IWasmSymbols).getDisassembly === 'function';

export type ContentGetter = () => Promise<string | undefined>;
export type LineColumn = { lineNumber: number; columnNumber: number }; // 1-based

Expand Down
92 changes: 63 additions & 29 deletions src/adapter/sourceContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
isSourceWithMap,
isSourceWithSourceMap,
isSourceWithWasm,
isWasmSymbols,
rawToUiOffset,
uiToRawOffset,
} from './source';
Expand Down Expand Up @@ -332,31 +333,23 @@ export class SourceContainer {
* This method returns a "preferred" location. This usually means going
* through a source map and showing the source map source instead of a
* compiled one. We use timeout to avoid waiting for the source map for too long.
*
* Similar to {@link getSourceMapUiLocations}, except it only returns the
* single preferred location.
*/
public async preferredUiLocation(uiLocation: IUiLocation): Promise<IPreferredUiLocation> {
let isMapped = false;
let unmappedReason: UnmappedReason | undefined = UnmappedReason.CannotMap;
if (this._doSourceMappedStepping) {
while (true) {
if (!isSourceWithMap(uiLocation.source)) {
break;
}

const map = await SourceLocationProvider.waitForValueWithTimeout(
uiLocation.source.sourceMap,
this._sourceMapTimeouts.resolveLocation,
);
if (!map) return { ...uiLocation, isMapped, unmappedReason };
const sourceMapped = await this._sourceMappedUiLocation(uiLocation, map);
if (!isUiLocation(sourceMapped)) {
unmappedReason = isMapped ? undefined : sourceMapped;
break;
}

uiLocation = sourceMapped;
isMapped = true;
unmappedReason = undefined;
while (true) {
const next = await this._originalPositionFor(uiLocation);
if (!isUiLocation(next)) {
unmappedReason = isMapped ? undefined : next;
break;
}

uiLocation = next;
isMapped = true;
unmappedReason = undefined;
}

return { ...uiLocation, isMapped, unmappedReason };
Expand Down Expand Up @@ -407,24 +400,65 @@ export class SourceContainer {

/**
* Returns all UI locations the given location maps to.
*
* Similar to {@link preferredUiLocation}, except it returns all positions,
* not just one.
*/
private async getSourceMapUiLocations(uiLocation: IUiLocation): Promise<IUiLocation[]> {
if (!isSourceWithMap(uiLocation.source) || !this._doSourceMappedStepping) return [];
const sourceMapUiLocation = await this._originalPositionFor(uiLocation);
if (!isUiLocation(sourceMapUiLocation)) {
return [];
}

const r = await this.getSourceMapUiLocations(sourceMapUiLocation);
r.push(sourceMapUiLocation);
return r;
}

/**
* Gets the compiled position for a single UI location. Is aware of whether
* source map stepping is enabled.
*/
private async _originalPositionFor(uiLocation: IUiLocation) {
if (!isSourceWithMap(uiLocation.source)) {
return UnmappedReason.HasNoMap;
}

const map = await SourceLocationProvider.waitForValueWithTimeout(
uiLocation.source.sourceMap,
this._sourceMapTimeouts.resolveLocation,
);
if (!map) {
return UnmappedReason.MapLoadingFailed;
}

if (!map) return [];
const sourceMapUiLocation = await this._sourceMappedUiLocation(uiLocation, map);
if (!isUiLocation(sourceMapUiLocation)) return [];
if (this._doSourceMappedStepping) {
return this._originalPositionForMap(uiLocation, map);
}

const r = await this.getSourceMapUiLocations(sourceMapUiLocation);
r.push(sourceMapUiLocation);
return r;
if (isWasmSymbols(map)) {
const l = await map.disassembledPositionFor(
new Base1Position(uiLocation.lineNumber, uiLocation.columnNumber),
);
const mapped = l && uiLocation.source.sourceMap.sourceByUrl.get(l.url);
if (mapped) {
return mapped
? {
columnNumber: l.position.base1.lineNumber,
lineNumber: l.position.base1.lineNumber,
source: mapped,
}
: UnmappedReason.MapPositionMissing;
}
}

return UnmappedReason.MapDisabled;
}

private async _sourceMappedUiLocation(
/**
* Looks up the given location in the sourcemap.
*/
private async _originalPositionForMap(
uiLocation: IUiLocation,
map: SourceMap | IWasmSymbols,
): Promise<IUiLocation | UnmappedReason> {
Expand Down Expand Up @@ -529,7 +563,7 @@ export class SourceContainer {
sourceMap: SourceMap | IWasmSymbols,
uiLocation: LineColumn,
): Promise<{ url: string; position: IPosition } | undefined> {
if ('decompiledUrl' in sourceMap) {
if (isWasmSymbols(sourceMap)) {
return await sourceMap.originalPositionFor(
new Base1Position(uiLocation.lineNumber, uiLocation.columnNumber),
);
Expand Down
5 changes: 5 additions & 0 deletions src/build/generate-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ const baseConfigurationAttributes: ConfigurationAttributes<IBaseConfiguration> =
default: [],
description: refString('base.cascadeTerminateToConfigurations.label'),
},
enableDWARF: {
type: 'boolean',
default: true,
markdownDescription: refString('base.enableDWARF.label'),
},
};

/**
Expand Down
8 changes: 8 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ export interface IBaseConfiguration extends IMandatedConfiguration {
*/
cascadeTerminateToConfigurations: string[];

/**
* Toggles whether the debugger will try to read DWARF debug symbols from
* WebAssembly, which can be resource intensive. Requires the
* `ms-vscode.wasm-dwarf-debugging` extension to function.
*/
enableDWARF: boolean;

/**
* The value of the ${workspaceFolder} variable
*/
Expand Down Expand Up @@ -814,6 +821,7 @@ export const baseDefaults: IBaseConfiguration = {
sourceMapPathOverrides: defaultSourceMapPathOverrides('${workspaceFolder}'),
enableContentValidation: true,
cascadeTerminateToConfigurations: [],
enableDWARF: true,
// Should always be determined upstream;
__workspaceFolder: '',
__remoteFilePrefix: undefined,
Expand Down

0 comments on commit 613b9ea

Please sign in to comment.