How This Example Works
Nothing in the scopes proposal is JavaScript-specific. Source maps for WebAssembly use a single generated line (0) and the module byte offset as the column, and binding expressions are evaluated with Debugger.evaluateOnCallFrame on the wasm frame. There, V8 exposes a debug proxy with the wasm state: locals $var0… as { type, value }, the linear memory memories[0] (or $memory, named after its export), functions such as $_Z11adjustPixeliii, and the value stack stack[0]…
image-filter.wasm is a hand-assembled module whose authored source is image-filter.cpp. The bindings describe what an optimizing C++ compiler did: clampLevel and applyContrast are inlined into adjustPixel, with clampLevel inlined at two call sites. One wasm local ($var3) holds four different C++ values over time. The loop index i exists only as a pointer difference, and Image* is decoded from linear memory into an object with a live Uint8Array of pixels. The int64_t checksum shows up as a BigInt, and an Emscripten legalstub$adjustImage wrapper is hidden. The JS glue (bundle.js) is not source-mapped.
This is the experimental example: it only works if DevTools applies scopes to wasm call frames, so expect the most rough edges here.
What to Try & Verify in DevTools
Try:Click "Run & Pause in Debugger". DevTools pauses at debugger; in the JS glue (bundle.js), which implements emscripten_debugger(). Press Step out (Shift+F11) once.
- Authored location: Paused on line 49 of
image-filter.cpp(if (i == 2) emscripten_debugger();), not in the wasm disassembly. - Call Stack:
adjustImage(image-filter.cpp) →callAdjustImage→window.runExample08(bundle.js). The Emscripten wrapperlegalstub$adjustImagebetween them is not shown (hidden range). - No raw names: No
$var…locals and noExpression(value stack) scope from raw wasm debugging.
Try:Look at the Scope pane for the adjustImage frame, and expand image and image.pixels.
- Block scopes:
pixel: 2066(an address) andadjusted: 255; in the enclosingforblock,i: 2(computed as$var4 - $var3because the compiler turnediinto a pointer). - Function scope:
brightness: 30,contrastPct: 150,pixelCount: 4,checksum: 457n(anint64_tshows up as aBigInt). - Struct from linear memory:
imageis{width: 2, height: 2, pixels: Uint8Array(4) [41, 161, 255, 250]}. The first 3 pixels are already adjusted. - Module scope:
kMaxLevel: 255(constant),g_imagesProcessed: 0andg_lastAverage: 0(read from linear memory; higher on later runs),clampLevel/applyContrast/averageLevel<unavailable>(always inlined),adjustPixel/adjustImageare functions.
Try:Set a conditional breakpoint on line 19 (if (level > kMaxLevel) ... in clampLevel) with condition level > kMaxLevel, then resume (F8). Resume once more after the first hit.
- 1st hit (call site 1): Call Stack
clampLevel→adjustPixel(line 31) →adjustImage(line 46).level: 280(250 + 30). - 2nd hit (call site 2):
clampLevel→applyContrast(line 26) →adjustPixel(line 32) →adjustImage.level: 318. - Mixed frames:
clampLevelandapplyContrastare virtual (inlined) frames, whileadjustPixelandadjustImageare two real wasm frames.
Try:At the 2nd hit, select the applyContrast frame and then the adjustPixel frame in the Call Stack.
applyContrast:level: 255,contrastPct: 150,centered: 127,scaled: 190. The wasm local$var3currently holdsscaled + 128, solevelandcenteredare reconstructed from$var0/$var1.adjustPixel:level: 250,brightness: 30,contrastPct: 150,brightened: 255(reconstructed),result: <unavailable>.- Value stack binding: Remove the breakpoints, set one on line 33 (
return result;) and resume.resultshould be255, read from the wasm value stack (stack[0]).
Try:Remove all breakpoints, click Run again, step out of the JS glue and use Step over (F10) until you reach line 46. Then use Step into (F11) repeatedly.
- Step into enters
adjustPixel(a real wasm call) and then the inlinedclampLevel/applyContrastframes, which appear and disappear in the Call Stack. - Step over on a line that calls an inlined helper (e.g.
int32_t brightened = ...) does not stop inside the helper. - Step out of an inlined frame lands back in the caller's line.
Try:Remove all breakpoints, resume, and click Trap: Divide by Zero. It runs adjustImage on an empty 0×0 image, so the inlined averageLevel divides by zero.
- Console:
RuntimeError: divide by zerowith stackaverageLevel(line 37) →adjustImage(line 51) →callAdjustImage→window.logStackExample08(bundle.js). - Hidden and raw frames:
legalstub$adjustImageis not shown. The page readout shows the rawerr.stackwithwasm-function[3]:0x…frames. - Pause on exceptions: Enable Pause on caught exceptions and click the button again. DevTools pauses on line 37 with
sum: 0nandcount: 0. InadjustImage,imageis{width: 0, height: 0, pixels: Uint8Array(0)}.
Other things to try
- Evaluate raw wasm names like
$var3,$memory,memories,localsorstackin the Console. Do they still work next to the authored names? - Hover
image,checksumorpixelinimage-filter.cppfor popovers, and check the inline value hints. - Right-click the Call Stack › Copy stack trace while paused in the inlined
clampLevel. - Set a logpoint on line 48 logging
i, adjusted, checksum. - Open
image-filter.wasmitself in the Sources panel (disassembly) and compare the byte offsets with tab 2. - Add
imageas a Watch expression and step through the loop to watchimage.pixelschange.
Not sure if it's a DevTools bug? Tab 4. Decoded Scopes shows what the source map actually encodes.
Try in the Console While Paused
While paused, select a frame and evaluate these in the Console (Esc opens the drawer) using the authored names:
image.pixels[i]
Number(checksum) / (i + 1)
adjustPixel(200, brightness, contrastPct)
{ level, centered, scaled }