Skip to content

Commit

Permalink
feat: support basic webassembly debugging (#1791)
Browse files Browse the repository at this point in the history
* feat: support basic webassembly debugging

Supports viewing, stepping through, and setting breakpoints in
webassembly (decompiled as WAT) in the editor. Includes a
basic tmLanguage for WAT.

![](https://memes.peet.io/img/23-08-b7617299-9f8d-41c9-8fe0-ada8a3c57966.png)

Unfortunately we eagerly have to decompile WASM in order to get line
mappings to show e.g. in breakpoints. Location mapping is currently
mostly synchronous and I didn't want to make everything async
for webassembly. However, we don't keep the WAT source in memory,
instead request it again if it's needed. I opted to do this to reduce
memory usage for user applications that just happen to contain WASM
where they aren't always interested in debugging it.

For #1789
Fixes #1715 on the way

* retain wasm-set breakpoints between reloads
  • Loading branch information
connor4312 committed Aug 27, 2023
1 parent 07fd954 commit 72c0276
Show file tree
Hide file tree
Showing 15 changed files with 548 additions and 76 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: allow basic webassembly debugging ([vscode#102181](https://github.com/microsoft/vscode/issues/102181))
- feat: add `Symbol.for("debug.description")` as a way to generate object descriptions ([vscode#102181](https://github.com/microsoft/vscode/issues/102181))
- feat: adopt supportTerminateDebuggee for browsers and node ([#1733](https://github.com/microsoft/vscode-js-debug/issues/1733))
- fix: child processes from extension host not getting spawned during debug
Expand Down
29 changes: 18 additions & 11 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ gulp.task('compile:static', () =>
'README.md',
'package.nls.json',
'src/**/*.sh',
'src/ui/basic-wat.tmLanguage.json',
'.vscodeignore',
],
{
Expand Down Expand Up @@ -340,19 +341,25 @@ gulp.task(
);

/** Prepares the package and then hoists it to the root directory. Destructive. */
gulp.task('package:hoist', gulp.series('package:prepare', async () => {
const srcFiles = await fs.promises.readdir(buildDir);
const ignoredFiles = new Set(await fs.promises.readdir(__dirname));
gulp.task(
'package:hoist',
gulp.series('package:prepare', async () => {
const srcFiles = await fs.promises.readdir(buildDir);
const ignoredFiles = new Set(await fs.promises.readdir(__dirname));

ignoredFiles.delete('l10n-extract'); // special case: made in the pipeline
ignoredFiles.delete('l10n-extract'); // special case: made in the pipeline

for (const file of srcFiles) {
ignoredFiles.delete(file);
await fs.promises.rm(path.join(__dirname, file), { force: true, recursive: true });
await fs.promises.rename(path.join(buildDir, file), path.join(__dirname, file));
}
await fs.promises.appendFile(path.join(__dirname, '.vscodeignore'), [...ignoredFiles].join('\n'));
}));
for (const file of srcFiles) {
ignoredFiles.delete(file);
await fs.promises.rm(path.join(__dirname, file), { force: true, recursive: true });
await fs.promises.rename(path.join(buildDir, file), path.join(__dirname, file));
}
await fs.promises.appendFile(
path.join(__dirname, '.vscodeignore'),
[...ignoredFiles].join('\n'),
);
}),
);

gulp.task('package', gulp.series('package:prepare', 'package:createVSIX'));

Expand Down
53 changes: 25 additions & 28 deletions src/adapter/breakpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ import { PatternEntryBreakpoint } from './breakpoints/patternEntrypointBreakpoin
import { UserDefinedBreakpoint } from './breakpoints/userDefinedBreakpoint';
import { DiagnosticToolSuggester } from './diagnosticToolSuggester';
import {
base0To1,
base1To0,
ISourceWithMap,
isSourceWithMap,
IUiLocation,
Source,
SourceContainer,
base0To1,
base1To0,
isSourceWithMap,
rawToUiOffset,
uiToRawOffset,
} from './sources';
import { ScriptWithSourceMapHandler, Thread } from './threads';

Expand Down Expand Up @@ -153,6 +151,17 @@ export class BreakpointManager {

_breakpointsPredictor?.onLongParse(() => dap.longPrediction({}));

sourceContainer.onScript(script => {
script.source.then(source => {
const thread = this._thread;
if (thread) {
this._byRef
.get(source.sourceReference)
?.forEach(bp => bp.updateForNewLocations(thread, script));
}
});
});

sourceContainer.onSourceMappedSteppingChange(() => {
if (this._thread) {
for (const bp of this._byDapId.values()) {
Expand Down Expand Up @@ -182,10 +191,10 @@ export class BreakpointManager {
const path = source.absolutePath;
const byPath = path ? this._byPath.get(path) : undefined;
for (const breakpoint of byPath || [])
todo.push(breakpoint.updateForSourceMap(this._thread, script));
todo.push(breakpoint.updateForNewLocations(this._thread, script));
const byRef = this._byRef.get(source.sourceReference);
for (const breakpoint of byRef || [])
todo.push(breakpoint.updateForSourceMap(this._thread, script));
todo.push(breakpoint.updateForNewLocations(this._thread, script));

if (source.sourceMap) {
queue.push(source.sourceMap.sourceByUrl.values());
Expand Down Expand Up @@ -362,8 +371,8 @@ export class BreakpointManager {
.cdp()
.Debugger.getPossibleBreakpoints({
restrictToFunction: false,
start: { scriptId, ...uiToRawOffset(base1To0(start), lsrc.runtimeScriptOffset) },
end: { scriptId, ...uiToRawOffset(base1To0(end), lsrc.runtimeScriptOffset) },
start: { scriptId, ...lsrc.offsetSourceToScript(base1To0(start)) },
end: { scriptId, ...lsrc.offsetSourceToScript(base1To0(end)) },
})
.then(r => {
if (!r) {
Expand All @@ -378,7 +387,7 @@ export class BreakpointManager {
const { lineNumber, columnNumber = 0 } = breakLocation;
const uiLocations = this._sourceContainer.currentSiblingUiLocations({
source: lsrc,
...rawToUiOffset(base0To1({ lineNumber, columnNumber }), lsrc.runtimeScriptOffset),
...lsrc.offsetScriptToSource(base0To1({ lineNumber, columnNumber })),
});

result.push({ breakLocation, uiLocations });
Expand Down Expand Up @@ -510,19 +519,7 @@ export class BreakpointManager {

const wasEntryBpSet = await this._sourceMapHandlerInstalled?.entryBpSet;
params.source.path = urlUtils.platformPathToPreferredCase(params.source.path);

// If we see we want to set breakpoints in file by source reference ID but
// it doesn't exist, they were probably from a previous section. The
// references for scripts just auto-increment per session and are entirely
// ephemeral. Remove the reference so that we fall back to a path if possible.
const containedSource = this._sourceContainer.source(params.source);
if (
params.source.sourceReference /* not (undefined or 0=on disk) */ &&
params.source.path &&
!containedSource
) {
params.source.sourceReference = undefined;
}

// Wait until the breakpoint predictor finishes to be sure that we
// can place correctly in breakpoint.set(), if:
Expand Down Expand Up @@ -589,17 +586,17 @@ export class BreakpointManager {
};

const getCurrent = () =>
params.source.path
? this._byPath.get(params.source.path)
: params.source.sourceReference
params.source.sourceReference
? this._byRef.get(params.source.sourceReference)
: params.source.path
? this._byPath.get(params.source.path)
: undefined;

const result = mergeInto(getCurrent() ?? []);
if (params.source.path) {
this._byPath.set(params.source.path, result.list);
} else if (params.source.sourceReference) {
if (params.source.sourceReference) {
this._byRef.set(params.source.sourceReference, result.list);
} else if (params.source.path) {
this._byPath.set(params.source.path, result.list);
} else {
return { breakpoints: [] };
}
Expand Down
33 changes: 23 additions & 10 deletions src/adapter/breakpoints/breakpointBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
IUiLocation,
Source,
SourceFromMap,
uiToRawOffset,
WasmSource,
} from '../sources';
import { Script, Thread } from '../threads';

Expand Down Expand Up @@ -194,7 +194,10 @@ export abstract class Breakpoint {
// a source map source. To make them work, we always set by url to not miss compiled.
// Additionally, if we have two sources with the same url, but different path (or no path),
// this will make breakpoint work in all of them.
this._setByPath(thread, uiToRawOffset(this.originalPosition, source?.runtimeScriptOffset)),
this._setByPath(
thread,
source?.offsetSourceToScript(this.originalPosition) || this.originalPosition,
),
);
}

Expand All @@ -210,7 +213,7 @@ export abstract class Breakpoint {

await Promise.all(
uiLocations.map(uiLocation =>
this._setByUiLocation(thread, uiToRawOffset(uiLocation, source.runtimeScriptOffset)),
this._setByUiLocation(thread, source.offsetSourceToScript(uiLocation)),
),
);
}
Expand Down Expand Up @@ -287,21 +290,33 @@ export abstract class Breakpoint {
await Promise.all(promises);
}

public async updateForSourceMap(thread: Thread, script: Script) {
/**
* Updates breakpoint placements in the debugee in responce to a new script
* getting parsed. This is useful in two cases:
*
* 1. Where the source was sourcemapped, in which case a new sourcemap tells
* us scripts to set BPs in.
* 2. Where a source was set by script ID, which happens for sourceReferenced
* sources.
*/
public async updateForNewLocations(thread: Thread, script: Script) {
const source = this._manager._sourceContainer.source(this.source);
if (!source) {
return [];
}

if (source instanceof WasmSource) {
await source.offsetsAssembled;
}

// Find all locations for this breakpoint in the new script.
const scriptSource = await script.source;
const uiLocations = this._manager._sourceContainer.currentSiblingUiLocations(
{
lineNumber: this.originalPosition.lineNumber,
columnNumber: this.originalPosition.columnNumber,
source,
},
scriptSource,
await script.source,
);

if (!uiLocations.length) {
Expand All @@ -310,9 +325,7 @@ export abstract class Breakpoint {

const promises: Promise<void>[] = [];
for (const uiLocation of uiLocations) {
promises.push(
this._setByScriptId(thread, script, uiToRawOffset(uiLocation, source.runtimeScriptOffset)),
);
promises.push(this._setByScriptId(thread, script, source.offsetSourceToScript(uiLocation)));
}

// If we get a source map that references this script exact URL, then
Expand All @@ -323,7 +336,7 @@ export abstract class Breakpoint {
continue;
}

if (!this.breakpointIsForSource(bp.args, scriptSource)) {
if (!this.breakpointIsForSource(bp.args, source)) {
continue;
}

Expand Down
Loading

0 comments on commit 72c0276

Please sign in to comment.