Commit 271bb94a authored by Z Nguyen-Huu's avatar Z Nguyen-Huu Committed by Commit Bot

[wasm] Support stepping back to Javascript from Wasm

This scenario is where user is at the end of Wasm execution and do
some stepping. Hence, user should be back at Javascript frame. We
can detect that stepping as it exits Wasm Interpreter and prepare
debugging as a step-out-ish in Javascript.

Bug: chromium:823923, chromium:1019606, chromium:1025151
Change-Id: I29022af0d5e5dcf78d87e83193f6e16fec954e87
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1912985
Commit-Queue: Z Nguyen-Huu <duongn@microsoft.com>
Reviewed-by: 's avatarYang Guo <yangguo@chromium.org>
Reviewed-by: 's avatarClemens Backes <clemensb@chromium.org>
Reviewed-by: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#65122}
parent f7333fd2
......@@ -959,7 +959,6 @@ void Debug::PrepareStepOnThrow() {
}
}
void Debug::PrepareStep(StepAction step_action) {
HandleScope scope(isolate_);
......@@ -981,53 +980,62 @@ void Debug::PrepareStep(StepAction step_action) {
StandardFrame* frame = frames_it.frame();
// Handle stepping in wasm functions via the wasm interpreter.
if (frame->is_wasm()) {
// If the top frame is compiled, we cannot step.
if (frame->is_wasm_compiled()) return;
if (frame->is_wasm_interpreter_entry()) {
WasmInterpreterEntryFrame* wasm_frame =
WasmInterpreterEntryFrame::cast(frame);
wasm_frame->debug_info().PrepareStep(step_action);
return;
if (wasm_frame->NumberOfActiveFrames() > 0) {
wasm_frame->debug_info().PrepareStep(step_action);
return;
}
}
// If this is wasm, but there are no interpreted frames on top, all we can do
// is step out.
if (frame->is_wasm()) step_action = StepOut;
JavaScriptFrame* js_frame = JavaScriptFrame::cast(frame);
DCHECK(js_frame->function().IsJSFunction());
BreakLocation location = BreakLocation::Invalid();
Handle<SharedFunctionInfo> shared;
int current_frame_count = CurrentFrameCount();
// Get the debug info (create it if it does not exist).
auto summary = FrameSummary::GetTop(frame).AsJavaScript();
Handle<JSFunction> function(summary.function());
Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
if (!EnsureBreakInfo(shared)) return;
PrepareFunctionForDebugExecution(shared);
if (frame->is_java_script()) {
JavaScriptFrame* js_frame = JavaScriptFrame::cast(frame);
DCHECK(js_frame->function().IsJSFunction());
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
// Get the debug info (create it if it does not exist).
auto summary = FrameSummary::GetTop(frame).AsJavaScript();
Handle<JSFunction> function(summary.function());
shared = Handle<SharedFunctionInfo>(function->shared(), isolate_);
if (!EnsureBreakInfo(shared)) return;
PrepareFunctionForDebugExecution(shared);
BreakLocation location = BreakLocation::FromFrame(debug_info, js_frame);
Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_);
location = BreakLocation::FromFrame(debug_info, js_frame);
// Any step at a return is a step-out, and a step-out at a suspend behaves
// like a return.
if (location.IsReturn() || (location.IsSuspend() && step_action == StepOut)) {
// On StepOut we'll ignore our further calls to current function in
// PrepareStepIn callback.
if (last_step_action() == StepOut) {
thread_local_.ignore_step_into_function_ = *function;
// Any step at a return is a step-out, and a step-out at a suspend behaves
// like a return.
if (location.IsReturn() ||
(location.IsSuspend() && step_action == StepOut)) {
// On StepOut we'll ignore our further calls to current function in
// PrepareStepIn callback.
if (last_step_action() == StepOut) {
thread_local_.ignore_step_into_function_ = *function;
}
step_action = StepOut;
thread_local_.last_step_action_ = StepIn;
}
step_action = StepOut;
thread_local_.last_step_action_ = StepIn;
}
// We need to schedule DebugOnFunction call callback
UpdateHookOnFunctionCall();
// We need to schedule DebugOnFunction call callback
UpdateHookOnFunctionCall();
// A step-next in blackboxed function is a step-out.
if (step_action == StepNext && IsBlackboxed(shared)) step_action = StepOut;
// A step-next in blackboxed function is a step-out.
if (step_action == StepNext && IsBlackboxed(shared)) step_action = StepOut;
thread_local_.last_statement_position_ =
summary.abstract_code()->SourceStatementPosition(summary.code_offset());
int current_frame_count = CurrentFrameCount();
thread_local_.last_frame_count_ = current_frame_count;
// No longer perform the current async step.
clear_suspended_generator();
thread_local_.last_statement_position_ =
summary.abstract_code()->SourceStatementPosition(summary.code_offset());
thread_local_.last_frame_count_ = current_frame_count;
// No longer perform the current async step.
clear_suspended_generator();
}
switch (step_action) {
case StepNone:
......@@ -1036,7 +1044,8 @@ void Debug::PrepareStep(StepAction step_action) {
// Clear last position info. For stepping out it does not matter.
thread_local_.last_statement_position_ = kNoSourcePosition;
thread_local_.last_frame_count_ = -1;
if (!location.IsReturnOrSuspend() && !IsBlackboxed(shared)) {
if (!shared.is_null() && !location.IsReturnOrSuspend() &&
!IsBlackboxed(shared)) {
// At not return position we flood return positions with one shots and
// will repeat StepOut automatically at next break.
thread_local_.target_frame_count_ = current_frame_count;
......@@ -1048,8 +1057,10 @@ void Debug::PrepareStep(StepAction step_action) {
// and deoptimize every frame along the way.
bool in_current_frame = true;
for (; !frames_it.done(); frames_it.Advance()) {
// TODO(clemensb): Implement stepping out from JS to wasm.
if (frames_it.frame()->is_wasm()) continue;
if (frames_it.frame()->is_wasm()) {
in_current_frame = false;
continue;
}
JavaScriptFrame* frame = JavaScriptFrame::cast(frames_it.frame());
if (last_step_action() == StepIn) {
// Deoptimize frame to ensure calls are checked for step-in.
......
......@@ -61,6 +61,7 @@ enum IgnoreBreakMode {
class BreakLocation {
public:
static BreakLocation Invalid() { return BreakLocation(-1, NOT_DEBUG_BREAK); }
static BreakLocation FromFrame(Handle<DebugInfo> debug_info,
JavaScriptFrame* frame);
......
......@@ -1988,6 +1988,11 @@ void WasmInterpreterEntryFrame::Summarize(
Code WasmInterpreterEntryFrame::unchecked_code() const { return Code(); }
int WasmInterpreterEntryFrame::NumberOfActiveFrames() const {
Handle<WasmInstanceObject> instance(wasm_instance(), isolate());
return instance->debug_info().NumberOfActiveFrames(fp());
}
WasmInstanceObject WasmInterpreterEntryFrame::wasm_instance() const {
const int offset = WasmCompiledFrameConstants::kWasmInstanceOffset;
Object instance(Memory<Address>(fp() + offset));
......
......@@ -1002,6 +1002,7 @@ class WasmInterpreterEntryFrame final : public StandardFrame {
Code unchecked_code() const override;
// Accessors.
int NumberOfActiveFrames() const;
WasmDebugInfo debug_info() const;
WasmInstanceObject wasm_instance() const;
......
......@@ -121,6 +121,10 @@ class InterpreterHandle {
activations_.erase(frame_pointer);
}
bool HasActivation(Address frame_pointer) {
return activations_.count(frame_pointer);
}
std::pair<uint32_t, uint32_t> GetActivationFrameRange(
WasmInterpreter::Thread* thread, Address frame_pointer) {
DCHECK_EQ(1, activations_.count(frame_pointer));
......@@ -234,6 +238,15 @@ class InterpreterHandle {
}
FinishActivation(frame_pointer, activation_id);
// If we do stepping and it exits wasm interpreter then debugger need to
// prepare for it.
if (next_step_action_ != StepNone) {
// Enter the debugger.
DebugScope debug_scope(isolate_->debug());
isolate_->debug()->PrepareStep(StepOut);
}
ClearStepping();
return true;
......@@ -346,6 +359,18 @@ class InterpreterHandle {
return stack;
}
int NumberOfActiveFrames(Address frame_pointer) {
if (!HasActivation(frame_pointer)) return 0;
DCHECK_EQ(1, interpreter()->GetThreadCount());
WasmInterpreter::Thread* thread = interpreter()->GetThread(0);
std::pair<uint32_t, uint32_t> frame_range =
GetActivationFrameRange(thread, frame_pointer);
return frame_range.second - frame_range.first;
}
WasmInterpreter::FramePtr GetInterpretedFrame(Address frame_pointer,
int idx) {
DCHECK_EQ(1, interpreter()->GetThreadCount());
......@@ -623,6 +648,10 @@ std::vector<std::pair<uint32_t, int>> WasmDebugInfo::GetInterpretedStack(
return GetInterpreterHandle(*this)->GetInterpretedStack(frame_pointer);
}
int WasmDebugInfo::NumberOfActiveFrames(Address frame_pointer) {
return GetInterpreterHandle(*this)->NumberOfActiveFrames(frame_pointer);
}
wasm::WasmInterpreter::FramePtr WasmDebugInfo::GetInterpretedFrame(
Address frame_pointer, int idx) {
return GetInterpreterHandle(*this)->GetInterpretedFrame(frame_pointer, idx);
......
......@@ -868,6 +868,8 @@ class WasmDebugInfo : public Struct {
std::vector<std::pair<uint32_t, int>> GetInterpretedStack(
Address frame_pointer);
int NumberOfActiveFrames(Address frame_pointer);
V8_EXPORT_PRIVATE
std::unique_ptr<wasm::InterpretedFrame, wasm::InterpretedFrameDeleter>
GetInterpretedFrame(Address frame_pointer, int frame_index);
......
......@@ -34,7 +34,7 @@ function listener(event, exec_state, event_data, data) {
break_count++;
assertTrue(
event_data.sourceLineText().indexOf(`Line ${js_break_line}.`) > 0);
js_break_line += 2;
js_break_line++;
} else {
assertTrue(event_data.functionName() == 'sub');
wasm_break_count++;
......@@ -85,8 +85,8 @@ Debug.setListener(listener2);
f();
Debug.setListener(null);
assertEquals(break_count, 2);
assertEquals(js_break_line, 4);
assertEquals(break_count, 3);
assertEquals(js_break_line, 3);
assertEquals(wasm_break_count, 4);
assertEquals(break_count2, 2);
assertNull(exception);
......@@ -333,4 +333,12 @@ at (anonymous) (0:17):
- scope (global):
-- skipped globals
Paused:
instance.exports.main(4)#
Scope:
at (anonymous) (0:24):
- scope (global):
-- skipped globals
exports.main returned. Test finished.
Tests stepping to javascript from wasm
Installing code and global variable.
Calling instantiate function.
Waiting for wasm scripts to be parsed.
Ignoring script with url v8://test/callInstantiate
Ignoring script with url wasm://wasm/485e942e
Got wasm script: wasm://wasm/485e942e/485e942e-0
Setting breakpoint on line 2 of wasm function
{
columnNumber : 0
lineNumber : 2
scriptId : <scriptId>
}
Start run 1
paused
function test() {
#debugger;
instance.exports.main();
Debugger.resume
paused
nop
#end
Debugger.stepOut
paused
instance.exports.main();
var x = #1;
x++;
Debugger.resume
exports.main returned!
Finished run 1!
Start run 2
paused
function test() {
#debugger;
instance.exports.main();
Debugger.resume
paused
nop
#end
Debugger.stepOver
paused
instance.exports.main();
var x = #1;
x++;
Debugger.resume
exports.main returned!
Finished run 2!
Start run 3
paused
function test() {
#debugger;
instance.exports.main();
Debugger.resume
paused
nop
#end
Debugger.stepInto
paused
instance.exports.main();
var x = #1;
x++;
Debugger.resume
exports.main returned!
Finished run 3!
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
let {session, contextGroup, Protocol} = InspectorTest.start('Tests stepping to javascript from wasm');
session.setupScriptMap();
utils.load('test/mjsunit/wasm/wasm-module-builder.js');
let builder = new WasmModuleBuilder();
// wasm_A
builder.addFunction('wasm_A', kSig_v_v)
.addBody([
// clang-format off
kExprNop, // Line 1
// clang-format on
])
.exportAs('main');
let module_bytes = builder.toArray();
function instantiate(bytes) {
let buffer = new ArrayBuffer(bytes.length);
let view = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; ++i) {
view[i] = bytes[i] | 0;
}
let module = new WebAssembly.Module(buffer);
// Set global variable.
instance = new WebAssembly.Instance(module);
}
let evalWithUrl = (code, url) => Protocol.Runtime.evaluate(
{'expression': code + '\n//# sourceURL=v8://test/' + url});
Protocol.Debugger.onPaused(async message => {
InspectorTest.log("paused");
var frames = message.params.callFrames;
await session.logSourceLocation(frames[0].location);
let action = step_actions.shift() || 'resume';
InspectorTest.log('Debugger.' + action)
await Protocol.Debugger[action]();
})
let step_actions = [
// start of run 1
'resume', // move to breakpoint
// then get back to Javascript.
'stepOut',
'resume',
// end of run 1
// start of run 2
'resume', // move to breakpoint
// then get back to Javascript.
'stepOver',
'resume',
// end of run 2
// start of run 3
'resume', // move to breakpoint
// then get back to Javascript.
'stepInto',
'resume',
// end of run 3
];
contextGroup.addScript(`
function test() {
debugger;
instance.exports.main();
var x = 1;
x++;
}
//# sourceURL=test.js`);
(async function Test() {
await Protocol.Debugger.enable();
InspectorTest.log('Installing code and global variable.');
await evalWithUrl('var instance;\n' + instantiate.toString(), 'setup');
InspectorTest.log('Calling instantiate function.');
evalWithUrl(
'instantiate(' + JSON.stringify(module_bytes) + ')', 'callInstantiate');
const scriptId = await waitForWasmScript();
InspectorTest.log(
'Setting breakpoint on line 2 of wasm function');
let msg = await Protocol.Debugger.setBreakpoint(
{'location': {'scriptId': scriptId, 'lineNumber': 2}});
printFailure(msg);
InspectorTest.logMessage(msg.result.actualLocation);
for (var i=1; i<=3; i++) {
InspectorTest.log('Start run '+ i);
await Protocol.Runtime.evaluate({ expression: 'test()' });
InspectorTest.log('exports.main returned!');
InspectorTest.log('Finished run '+ i +'!\n');
}
InspectorTest.completeTest();
})();
function printFailure(message) {
if (!message.result) {
InspectorTest.logMessage(message);
}
return message;
}
async function waitForWasmScript() {
InspectorTest.log('Waiting for wasm scripts to be parsed.');
while (true) {
let msg = await Protocol.Debugger.onceScriptParsed();
let url = msg.params.url;
if (!url.startsWith('wasm://') || url.split('/').length != 5) {
InspectorTest.log('Ignoring script with url ' + url);
continue;
}
let scriptId = msg.params.scriptId;
InspectorTest.log('Got wasm script: ' + url);
return scriptId;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment