[wasm][debug] Support multi-threaded stepping

Instead of keeping a single {stepping_frame_} per native module, we now
keep one frame id per isolate. Hence, each isolate can step through a
different frame, independent of other isolates.
The on-stack-replacement of the stepping frame already works on a
per-isolate basis, since we only replace the return address of a single
frame, part of the isolate that requested stepping.

The new test (which also executes in a variant with two concurrent
isolates) revealed some more data races to fix.

R=thibaudm@chromium.org

Bug: v8:10359
Cq-Include-Trybots: luci.v8.try:v8_linux64_tsan_rel
Cq-Include-Trybots: luci.v8.try:v8_linux64_tsan_isolates_rel_ng
Change-Id: I0bb013737162bd09b9f4be9c08990bca7bf736ac
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2214838
Reviewed-by: Thibaud Michaud <thibaudm@chromium.org>
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#68045}
This commit is contained in:
Clemens Backes 2020-05-28 15:55:40 +02:00 committed by Commit Bot
parent 3daf5b3ba0
commit 9743479196
5 changed files with 145 additions and 35 deletions

View File

@ -450,7 +450,7 @@ RUNTIME_FUNCTION(Runtime_WasmDebugBreak) {
WasmFrame* frame = frame_finder.frame();
auto* debug_info = frame->native_module()->GetDebugInfo();
if (debug_info->IsStepping(frame)) {
debug_info->ClearStepping();
debug_info->ClearStepping(isolate);
isolate->debug()->ClearStepping();
isolate->debug()->OnDebugBreak(isolate->factory()->empty_fixed_array());
return undefined;
@ -461,7 +461,7 @@ RUNTIME_FUNCTION(Runtime_WasmDebugBreak) {
Handle<FixedArray> breakpoints;
if (WasmScript::CheckBreakPoints(isolate, script, position)
.ToHandle(&breakpoints)) {
debug_info->ClearStepping();
debug_info->ClearStepping(isolate);
isolate->debug()->ClearStepping();
if (isolate->debug()->break_points_active()) {
// We hit one or several breakpoints. Notify the debug listeners.

View File

@ -485,10 +485,6 @@ class DebugInfoImpl {
WasmCode* RecompileLiftoffWithBreakpoints(
int func_index, Vector<int> offsets, Vector<int> extra_source_positions) {
// During compilation, we cannot hold the lock, since compilation takes the
// {NativeModule} lock, which could lead to deadlocks.
mutex_.AssertUnheld();
// Recompile the function with Liftoff, setting the new breakpoints.
// Not thread-safe. The caller is responsible for locking {mutex_}.
CompilationEnv env = native_module_->CreateCompilationEnv();
@ -514,16 +510,18 @@ class DebugInfoImpl {
native_module_->AddCompiledCode(std::move(result)));
DCHECK(new_code->is_inspectable());
bool added =
debug_side_tables_.emplace(new_code, std::move(debug_sidetable)).second;
DCHECK(added);
USE(added);
{
base::MutexGuard guard(&mutex_);
DCHECK_EQ(0, debug_side_tables_.count(new_code));
debug_side_tables_.emplace(new_code, std::move(debug_sidetable));
}
return new_code;
}
void SetBreakpoint(int func_index, int offset, Isolate* current_isolate) {
void SetBreakpoint(int func_index, int offset, Isolate* isolate) {
std::vector<int> breakpoints_copy;
StackFrameId stepping_frame = NO_ID;
{
// Hold the mutex while modifying the set of breakpoints, but release it
// before compiling the new code (see comment in
@ -544,26 +542,28 @@ class DebugInfoImpl {
}
breakpoints.insert(insertion_point, offset);
breakpoints_copy = breakpoints;
stepping_frame = per_isolate_data_[isolate].stepping_frame;
}
UpdateBreakpoints(func_index, VectorOf(breakpoints_copy), current_isolate);
UpdateBreakpoints(func_index, VectorOf(breakpoints_copy), isolate,
stepping_frame);
}
void UpdateBreakpoints(int func_index, Vector<int> breakpoints,
Isolate* current_isolate) {
Isolate* isolate, StackFrameId stepping_frame) {
// Generate additional source positions for current stack frame positions.
// These source positions are used to find return addresses in the new code.
std::vector<int> stack_frame_positions =
StackFramePositions(func_index, current_isolate);
StackFramePositions(func_index, isolate);
WasmCodeRefScope wasm_code_ref_scope;
WasmCode* new_code = RecompileLiftoffWithBreakpoints(
func_index, breakpoints, VectorOf(stack_frame_positions));
UpdateReturnAddresses(current_isolate, new_code);
UpdateReturnAddresses(isolate, new_code, stepping_frame);
}
void FloodWithBreakpoints(WasmFrame* frame, Isolate* current_isolate,
ReturnLocation return_location) {
void FloodWithBreakpoints(WasmFrame* frame, ReturnLocation return_location) {
// 0 is an invalid offset used to indicate flooding.
int offset = 0;
WasmCodeRefScope wasm_code_ref_scope;
@ -596,21 +596,30 @@ class DebugInfoImpl {
return_location = kAfterWasmCall;
}
FloodWithBreakpoints(frame, isolate, return_location);
stepping_frame_ = frame->id();
FloodWithBreakpoints(frame, return_location);
base::MutexGuard guard(&mutex_);
per_isolate_data_[isolate].stepping_frame = frame->id();
}
void ClearStepping() { stepping_frame_ = NO_ID; }
void ClearStepping(Isolate* isolate) {
base::MutexGuard guard(&mutex_);
auto it = per_isolate_data_.find(isolate);
if (it != per_isolate_data_.end()) it->second.stepping_frame = NO_ID;
}
bool IsStepping(WasmFrame* frame) {
Isolate* isolate = frame->wasm_instance().GetIsolate();
StepAction last_step_action = isolate->debug()->last_step_action();
return stepping_frame_ == frame->id() || last_step_action == StepIn;
if (isolate->debug()->last_step_action() == StepIn) return true;
base::MutexGuard guard(&mutex_);
auto it = per_isolate_data_.find(isolate);
return it != per_isolate_data_.end() &&
it->second.stepping_frame == frame->id();
}
void RemoveBreakpoint(int func_index, int position,
Isolate* current_isolate) {
void RemoveBreakpoint(int func_index, int position, Isolate* isolate) {
std::vector<int> breakpoints_copy;
StackFrameId stepping_frame = NO_ID;
{
base::MutexGuard guard(&mutex_);
const auto& function = native_module_->module()->functions[func_index];
@ -624,9 +633,12 @@ class DebugInfoImpl {
if (*insertion_point != offset) return;
breakpoints.erase(insertion_point);
breakpoints_copy = breakpoints;
stepping_frame = per_isolate_data_[isolate].stepping_frame;
}
UpdateBreakpoints(func_index, VectorOf(breakpoints_copy), current_isolate);
UpdateBreakpoints(func_index, VectorOf(breakpoints_copy), isolate,
stepping_frame);
}
void RemoveDebugSideTables(Vector<WasmCode* const> codes) {
@ -642,6 +654,11 @@ class DebugInfoImpl {
return it == debug_side_tables_.end() ? nullptr : it->second.get();
}
void RemoveIsolate(Isolate* isolate) {
base::MutexGuard guard(&mutex_);
per_isolate_data_.erase(isolate);
}
private:
struct FrameInspectionScope {
FrameInspectionScope(DebugInfoImpl* debug_info, Address pc)
@ -691,10 +708,13 @@ class DebugInfoImpl {
GenerateLiftoffDebugSideTable(allocator, &env, func_body);
DebugSideTable* ret = debug_side_table.get();
// Install into cache and return.
// Check cache again, maybe another thread concurrently generated a debug
// side table already.
{
base::MutexGuard guard(&mutex_);
debug_side_tables_[code] = std::move(debug_side_table);
auto& slot = debug_side_tables_[code];
if (slot != nullptr) return slot.get();
slot = std::move(debug_side_table);
}
// Print the code together with the debug table, if requested.
@ -768,15 +788,15 @@ class DebugInfoImpl {
// After installing a Liftoff code object with a different set of breakpoints,
// update return addresses on the stack so that execution resumes in the new
// code. The frame layout itself should be independent of breakpoints.
// TODO(thibaudm): update other threads as well.
void UpdateReturnAddresses(Isolate* isolate, WasmCode* new_code) {
void UpdateReturnAddresses(Isolate* isolate, WasmCode* new_code,
StackFrameId stepping_frame) {
// The first return location is after the breakpoint, others are after wasm
// calls.
ReturnLocation return_location = kAfterBreakpoint;
for (StackTraceFrameIterator it(isolate); !it.done();
it.Advance(), return_location = kAfterWasmCall) {
// We still need the flooded function for stepping.
if (it.frame()->id() == stepping_frame_) continue;
if (it.frame()->id() == stepping_frame) continue;
if (!it.is_wasm()) continue;
WasmFrame* frame = WasmFrame::cast(it.frame());
if (frame->native_module() != new_code->native_module()) continue;
@ -815,6 +835,16 @@ class DebugInfoImpl {
return static_cast<size_t>(position) == code.end_offset() - 1;
}
// Isolate-specific data, for debugging modules that are shared by multiple
// isolates.
struct PerIsolateDebugData {
// Store the frame ID when stepping, to avoid overwriting that frame when
// setting or removing a breakpoint.
StackFrameId stepping_frame = NO_ID;
// TODO(clemensb): Also move breakpoint here.
};
NativeModule* const native_module_;
// {mutex_} protects all fields below.
@ -829,11 +859,11 @@ class DebugInfoImpl {
// Keeps track of the currently set breakpoints (by offset within that
// function).
// TODO(clemensb): Move this into {PerIsolateDebugData}.
std::unordered_map<int, std::vector<int>> breakpoints_per_function_;
// Store the frame ID when stepping, to avoid overwriting that frame when
// setting or removing a breakpoint.
StackFrameId stepping_frame_ = NO_ID;
// Isolate-specific data.
std::unordered_map<Isolate*, PerIsolateDebugData> per_isolate_data_;
DISALLOW_COPY_AND_ASSIGN(DebugInfoImpl);
};
@ -882,7 +912,9 @@ void DebugInfo::PrepareStep(Isolate* isolate, StackFrameId break_frame_id) {
impl_->PrepareStep(isolate, break_frame_id);
}
void DebugInfo::ClearStepping() { impl_->ClearStepping(); }
void DebugInfo::ClearStepping(Isolate* isolate) {
impl_->ClearStepping(isolate);
}
bool DebugInfo::IsStepping(WasmFrame* frame) {
return impl_->IsStepping(frame);
@ -902,6 +934,10 @@ DebugSideTable* DebugInfo::GetDebugSideTableIfExists(
return impl_->GetDebugSideTableIfExists(code);
}
void DebugInfo::RemoveIsolate(Isolate* isolate) {
return impl_->RemoveIsolate(isolate);
}
} // namespace wasm
Handle<WasmDebugInfo> WasmDebugInfo::New(Handle<WasmInstanceObject> instance) {

View File

@ -168,7 +168,7 @@ class V8_EXPORT_PRIVATE DebugInfo {
void PrepareStep(Isolate*, StackFrameId);
void ClearStepping();
void ClearStepping(Isolate*);
bool IsStepping(WasmFrame*);
@ -180,6 +180,8 @@ class V8_EXPORT_PRIVATE DebugInfo {
// already been created. This will never trigger generation of the table.
DebugSideTable* GetDebugSideTableIfExists(const WasmCode*) const;
void RemoveIsolate(Isolate*);
private:
std::unique_ptr<DebugInfoImpl> impl_;
};

View File

@ -22,6 +22,7 @@
#include "src/wasm/module-decoder.h"
#include "src/wasm/module-instantiate.h"
#include "src/wasm/streaming-decoder.h"
#include "src/wasm/wasm-debug.h"
#include "src/wasm/wasm-limits.h"
#include "src/wasm/wasm-objects-inl.h"
@ -924,6 +925,9 @@ void WasmEngine::RemoveIsolate(Isolate* isolate) {
current_gc_info_->dead_code.erase(code);
}
}
if (native_module->HasDebugInfo()) {
native_module->GetDebugInfo()->RemoveIsolate(isolate);
}
}
if (current_gc_info_) {
if (RemoveIsolateFromCurrentGC(isolate)) PotentiallyFinishCurrentGC();

View File

@ -0,0 +1,68 @@
// Copyright 2020 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.
// Test stepping from JS through Wasm.
// A similar test exists as an inspector test already, but inspector tests are
// not run concurrently in multiple isolates (see `run-tests.py --isolates`).
load('test/mjsunit/wasm/wasm-module-builder.js');
const builder = new WasmModuleBuilder();
const imp_fun = builder.addImport('imp', 'ort', kSig_i_v);
const sub_fun = builder.addFunction('sub', kSig_i_ii).addBody([
kExprLocalGet, 0, // local.get i0
kExprLocalGet, 1, // local.get i1
kExprI32Sub // i32.sub i0 i1
]);
builder.addFunction('main', kSig_i_v)
.addBody([
kExprCallFunction, imp_fun, // call import
kExprI32Const, 3, // i32.const 3
kExprCallFunction, sub_fun.index // call 'sub'
])
.exportFunc();
// Compute the line number of the 'imported' function (to avoid hard-coding it).
const import_line_nr = parseInt((new Error()).stack.match(/:([0-9]+):/)[1]) + 1;
function imported() {
debugger;
return 7;
}
const instance = builder.instantiate({imp: {ort: imported}});
Debug = debug.Debug;
const expected_breaks = [
`imported:${import_line_nr + 1}:2`, // debugger;
`imported:${import_line_nr + 2}:2`, // return 7;
`imported:${import_line_nr + 2}:11`, // return 7;
'sub:1:58', 'sub:1:60', 'sub:1:62', 'sub:1:63', 'main:1:72'
];
let error;
function onBreak(event, exec_state, data) {
try {
if (event != Debug.DebugEvent.Break) return;
if (error) return;
if (data.sourceLineText().indexOf('Main returned.') >= 0) {
assertEquals(0, expected_breaks.length, 'hit all expected breaks');
return;
}
const pos =
[data.functionName(), data.sourceLine(), data.sourceColumn()].join(':');
const loc = [pos, data.sourceLineText()].join(':');
print(`Break at ${loc}`);
assertTrue(expected_breaks.length > 0, 'expecting more breaks');
const expected_pos = expected_breaks.shift();
assertEquals(expected_pos, pos);
exec_state.prepareStep(Debug.StepAction.StepIn);
} catch (e) {
if (!error) error = e;
}
}
Debug.setListener(onBreak);
print('Running main.');
const result = instance.exports.main();
print('Main returned.');
if (error) throw error;
assertEquals(4, result);