Skip to content

Commit

Permalink
fix: unable to hit most inflight breakpoints while debugging (#6217)
Browse files Browse the repository at this point in the history
Fixes #5988
Fixes #2546
Fixes #6036
Fixes #5986

#5988 was a symptom of a deeper issue. In `wing it` or any other command, breakpoints would only work sometimes and depended on the order of inflights in a file and how many there were. The underlying issue is that the sourcemaps are too slow and may not finish loading in time to help the debugger attach breakpoints. At first I made a change in this PR to fix a problem with how esbuild bundles our sourcemaps. On it's own this change made many scenarios work that previously didn't.

That wasn't enough though, so I went through many other options. As part of this, I actually added debugger settings to vscode.

In the end I needed to add a hack where we simply have a small sleep before running inflight code.

TODO:
- [x] Check Windows sourcemaps aren't borked
- [x] Ensure minimal benchmark regression (might skip this process if debugger is not needed)
- [x] A few tests for the remapping logic
- [x] Test locally with a winglib
- [ ] Try to figure out better options than a sleep

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
MarkMcCulloh authored Apr 14, 2024
1 parent c6fccec commit ba06ad3
Show file tree
Hide file tree
Showing 12 changed files with 361 additions and 21 deletions.
45 changes: 44 additions & 1 deletion apps/vscode-wing/.projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,49 @@ const contributes: VSCodeExtensionContributions = {
},
},
],
debuggers: [
{
type: "wing",
label: "Wing Debug",
program: "",
configurationAttributes: {
launch: {
entrypoint: {
type: "string",
description: "The entrypoint to run",
default: "${file}",
},
arguments: {
type: "string",
description: "Wing CLI arguments",
default: "test",
},
},
},
initialConfigurations: [
{
label: "Wing Debug: Launch",
description: "Launch a Wing program",
body: {
type: "wing",
request: "launch",
name: "Launch",
},
},
],
configurationSnippets: [
{
label: "Wing Debug: Launch",
description: "Launch a Wing program",
body: {
type: "wing",
request: "launch",
name: "Launch",
},
},
],
},
],
grammars: [
{
language: "wing",
Expand Down Expand Up @@ -167,7 +210,7 @@ project.addFields({
vscode: `^${VSCODE_BASE_VERSION}`,
},
categories: ["Programming Languages"],
activationEvents: ["onLanguage:wing"],
activationEvents: ["onLanguage:wing", "onDebug"],
contributes,
});

Expand Down
46 changes: 45 additions & 1 deletion apps/vscode-wing/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 66 additions & 1 deletion apps/vscode-wing/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
languages,
workspace,
window,
debug,
DebugConfiguration,
} from "vscode";
import { getWingBin, updateStatusBar } from "./bin-helper";
import { CFG_WING, CFG_WING_BIN, COMMAND_OPEN_CONSOLE } from "./constants";
Expand All @@ -17,7 +19,6 @@ let languageServerManager: LanguageServerManager | undefined;
export async function deactivate() {
wingBinWatcher?.close();
await languageServerManager?.stop();
await wingConsoleContext?.stop();
}

export async function activate(context: ExtensionContext) {
Expand All @@ -29,6 +30,70 @@ export async function activate(context: ExtensionContext) {

languageServerManager = new LanguageServerManager();

debug.registerDebugConfigurationProvider("wing", {
async resolveDebugConfiguration(_, _config: DebugConfiguration) {
Loggers.default.appendLine(
`Resolving debug configuration... ${JSON.stringify(_config)}`
);
const editor = window.activeTextEditor;

const currentFilename = editor?.document.fileName;
let chosenFile;
if (
currentFilename?.endsWith("main.w") ||
currentFilename?.endsWith(".test.w")
) {
chosenFile = currentFilename;
} else {
let uriOptions = await workspace.findFiles(
`**/*.{main,test}.w`,
"**/{node_modules,target}/**"
);
uriOptions.concat(
await workspace.findFiles(`**/main.w`, "**/{node_modules,target}/**")
);

const entrypoint = await window.showQuickPick(
uriOptions.map((f) => f.fsPath),
{
placeHolder: "Choose entrypoint to debug",
}
);

if (!entrypoint) {
return;
}

chosenFile = entrypoint;
}

const command = await window.showInputBox({
title: `Debugging ${chosenFile}`,
prompt: "Wing CLI arguments",
value: "test",
});

if (!command) {
return;
}

const currentWingBin = await getWingBin();

// Use builtin node debugger
return {
name: `Debug ${chosenFile}`,
request: "launch",
type: "node",
args: [currentWingBin, command, chosenFile],
runtimeSourcemapPausePatterns: [
"${workspaceFolder}/**/target/**/*.cjs",
],
autoAttachChildProcesses: true,
pauseForSourceMap: true,
};
},
});

const wingBinChanged = async () => {
Loggers.default.appendLine(`Setting up wing bin...`);
const currentWingBin = await getWingBin();
Expand Down
6 changes: 6 additions & 0 deletions apps/vscode-wing/src/project/vscode_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export interface VSCodeDebugger {
readonly label?: string;
readonly type: string;
readonly runtime?: string;
readonly program?: string;
readonly request?: string;
readonly variables?: string;
readonly configurationAttributes?: any;
readonly initialConfigurations?: any[];
readonly configurationSnippets?: any[];
}

export interface VSCodeGrammar {
Expand Down
10 changes: 3 additions & 7 deletions docs/docs/06-tools/03-debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ Internally Wing uses JavaScript to execute preflight and inflight code, so stand

### Local/Simulator Debugging

To start, open your .w file in VS Code and set a breakpoint by clicking in the gutter to the left of the line number. Breakpoints can also be set in extern files. There are several ways to start the debugger, but let's use the "JavaScript Debug Terminal".
Open the command palette and type "Debug: Open JavaScript Debug Terminal". This works for any wing commands like `wing test` and `wing it`, although keep in mind that `wing compile` will only debug preflight code.

### Limitations

- ([Issue](https://github.com/winglang/wing/issues/5988)) When using the Wing Console (`wing it`) and attempting to debug inflight code in a `test` or Function, the first execution of the test will not hit a breakpoint and will need to be run again
- ([Issue](https://github.com/winglang/wing/issues/5986)) inflight code by default has a timeout that continues during debugging, so if execution is paused for too long the program is terminate
To start, open your .w file in VS Code and set breakpoints by clicking in the gutter to the left of the line number. Breakpoints can also be set in extern files.
Once set, press F5 or use the "Run and Debug" button in the sidebar to start the debugger. This will use the current file if it's an entrypoint or it will prompt you to select one. Different CLI arguments can be provided as well.
By default, `wing test` will be run with an attached debugger.

#### Non-VSCode Support

Expand Down
4 changes: 4 additions & 0 deletions libs/wingsdk/.projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions libs/wingsdk/.projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const project = new cdk.JsiiProject({
// enhanced diagnostics
"stacktracey",
"ulid",
"vlq",
// tunnels
"@winglang/wingtunnels@workspace:^",
"glob",
Expand Down
2 changes: 2 additions & 0 deletions libs/wingsdk/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 99 additions & 9 deletions libs/wingsdk/src/shared/bundling.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as crypto from "crypto";
import { mkdirSync, realpathSync, writeFileSync } from "fs";
import { posix, resolve } from "path";
import { decode, encode } from "vlq";
import { normalPath } from "./misc";

const SDK_PATH = normalPath(resolve(__dirname, "..", ".."));
Expand Down Expand Up @@ -74,15 +75,7 @@ export function createBundle(
const sourcemapData = JSON.parse(
new TextDecoder().decode(esbuild.outputFiles[0].contents)
);
if (sourcemapData.sourceRoot) {
sourcemapData.sourceRoot = normalPath(sourcemapData.sourceRoot);
}

for (const [idx, source] of Object.entries(
sourcemapData.sources as string[]
)) {
sourcemapData.sources[idx] = normalPath(source);
}
fixSourcemaps(sourcemapData);

writeFileSync(outfile, bundleOutput.contents);
writeFileSync(outfileMap, JSON.stringify(sourcemapData));
Expand All @@ -101,3 +94,100 @@ export function createBundle(
sourcemapPath: outfileMap,
};
}

export interface SourceMap {
sourceRoot?: string;
sources: string[];
sourcesContent: string[];
mappings: string;
}

/**
* Takes a bundled sourcemap and does the following fixes:
* - Normalizes paths in sources and sourceRoot
* - Removes duplicate sources and sourcesContent
* - Updates mappings to reflect the new source indices
*
* The duplicate sources come from esbuild's strange handling of multiple files being bundled that point to the same source (e.g. inflights that point to one .w file)
* See https://github.com/evanw/esbuild/issues/933
*/
export function fixSourcemaps(sourcemapData: SourceMap): void {
// normalize sourceRoot
if (sourcemapData.sourceRoot) {
sourcemapData.sourceRoot = normalPath(sourcemapData.sourceRoot);
}

// normalize sources and remove duplicates
const sourceSet: string[] = [];
const newSourceContents: string[] = [];
const sourceIndexMap: Record<number, number> = {};
let hasSourceDupes = false;
sourcemapData.sources.forEach((source, idx) => {
const newPath = normalPath(source);
sourcemapData.sources[idx] = newPath;

const existingIndex = sourceSet.indexOf(newPath);
if (existingIndex === -1) {
sourceSet.push(newPath);
newSourceContents.push(sourcemapData.sourcesContent[idx]);
sourceIndexMap[idx] = sourceSet.length - 1;
} else {
hasSourceDupes = true;
sourceIndexMap[idx] = existingIndex;
}
});

sourcemapData.sources = sourceSet;
sourcemapData.sourcesContent = newSourceContents;

// fast path: No source duplicates so no need to update mappings
if (!hasSourceDupes) {
return;
}

// update mappings
let newMapping = "";
let characterIndex = 0;
let lastFile = 0;
let lastTrueFile = 0;
while (characterIndex < sourcemapData.mappings.length) {
const char = sourcemapData.mappings[characterIndex];
// `;` and `,` are separators between the segments of interest
if (char === ";" || char === ",") {
newMapping += char;
characterIndex++;
continue;
}

// get next slice of segment data
let segment = "";
let nextChar = char;
while (nextChar !== undefined && nextChar !== "," && nextChar !== ";") {
segment += nextChar;
nextChar = sourcemapData.mappings[++characterIndex];
}
const decoded = decode(segment);
if (decoded.length === 1) {
newMapping += segment;
continue;
}

const sourceRelative = decoded[1];
const originalSource = lastTrueFile + sourceRelative;
const newSourceIndex = sourceIndexMap[originalSource];
lastTrueFile = originalSource;

const newRelativeValue = newSourceIndex - lastFile;
lastFile = newSourceIndex;

if (newRelativeValue === decoded[1]) {
// no change was made, avoid re-encoding
newMapping += segment;
} else {
decoded[1] = newRelativeValue;
newMapping += encode(decoded);
}
}

sourcemapData.mappings = newMapping;
}
Loading

0 comments on commit ba06ad3

Please sign in to comment.