v8/test/cctest/wasm/test-liftoff-inspection.cc
Clemens Backes e0433f7dd0 [wasm][debug] Store "for debugging" flag on compilation unit
Before the "debug" flag was stored on the {CompilationEnv}. But each
background compilation task only gets the {CompilationEnv} once when
starting compilation, so by the time it picks up the "Liftoff for
debugging" compilation jobs, it might still compile them without the
debug flag being set. This leads to flakes in the "debug-step-into-wasm"
test, because we won't stop in the function prologue when stepping in
(because the function prologue does not check the "hook on function
call" flag if debug mode was not enabled).

This CL does not increase the size of a compilation unit, since both the
tier and the debug flag only need a single byte each.

As a nice side effect, this change allows us to remove the lock in
{CreateCompilationEnv}, because no modifyable flag is read any more.

R=thibaudm@chromium.org

Bug: v8:10410
Change-Id: Ic296ea0c4dd1d4dedde119f0536e87e5d301b5a1
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2144116
Reviewed-by: Thibaud Michaud <thibaudm@chromium.org>
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#67115}
2020-04-14 13:58:59 +00:00

442 lines
16 KiB
C++

// 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.
#include "src/wasm/baseline/liftoff-compiler.h"
#include "src/wasm/wasm-debug.h"
#include "test/cctest/cctest.h"
#include "test/cctest/wasm/wasm-run-utils.h"
#include "test/common/wasm/wasm-macro-gen.h"
namespace v8 {
namespace internal {
namespace wasm {
namespace {
class LiftoffCompileEnvironment {
public:
LiftoffCompileEnvironment()
: isolate_(CcTest::InitIsolateOnce()),
handle_scope_(isolate_),
zone_(isolate_->allocator(), ZONE_NAME),
module_builder_(&zone_, nullptr, ExecutionTier::kLiftoff,
kRuntimeExceptionSupport, kNoLowerSimd) {
// Add a table of length 1, for indirect calls.
module_builder_.AddIndirectFunctionTable(nullptr, 1);
}
struct TestFunction {
OwnedVector<uint8_t> body_bytes;
WasmFunction* function;
FunctionBody body;
};
void CheckDeterministicCompilation(
std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types,
std::initializer_list<uint8_t> raw_function_bytes) {
auto test_func = AddFunction(return_types, param_types, raw_function_bytes);
// Now compile the function with Liftoff two times.
CompilationEnv env = module_builder_.CreateCompilationEnv();
WasmFeatures detected1;
WasmFeatures detected2;
WasmCompilationResult result1 =
ExecuteLiftoffCompilation(isolate_->allocator(), &env, test_func.body,
test_func.function->func_index, kNoDebugging,
isolate_->counters(), &detected1);
WasmCompilationResult result2 =
ExecuteLiftoffCompilation(isolate_->allocator(), &env, test_func.body,
test_func.function->func_index, kNoDebugging,
isolate_->counters(), &detected2);
CHECK(result1.succeeded());
CHECK(result2.succeeded());
// Check that the generated code matches.
auto code1 =
VectorOf(result1.code_desc.buffer, result1.code_desc.instr_size);
auto code2 =
VectorOf(result2.code_desc.buffer, result2.code_desc.instr_size);
CHECK_EQ(code1, code2);
CHECK_EQ(detected1, detected2);
}
std::unique_ptr<DebugSideTable> GenerateDebugSideTable(
std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types,
std::initializer_list<uint8_t> raw_function_bytes,
std::vector<int> breakpoints = {}) {
auto test_func = AddFunction(return_types, param_types, raw_function_bytes);
CompilationEnv env = module_builder_.CreateCompilationEnv();
WasmFeatures detected;
std::unique_ptr<DebugSideTable> debug_side_table_via_compilation;
ExecuteLiftoffCompilation(CcTest::i_isolate()->allocator(), &env,
test_func.body, 0, kForDebugging, nullptr,
&detected, VectorOf(breakpoints),
&debug_side_table_via_compilation);
// If there are no breakpoint, then {ExecuteLiftoffCompilation} should
// provide the same debug side table.
if (breakpoints.empty()) {
std::unique_ptr<DebugSideTable> debug_side_table =
GenerateLiftoffDebugSideTable(CcTest::i_isolate()->allocator(), &env,
test_func.body);
CheckTableEquals(*debug_side_table, *debug_side_table_via_compilation);
}
return debug_side_table_via_compilation;
}
private:
static void CheckTableEquals(const DebugSideTable& a,
const DebugSideTable& b) {
CHECK_EQ(a.num_locals(), b.num_locals());
CHECK(std::equal(a.entries().begin(), a.entries().end(),
b.entries().begin(), b.entries().end(),
&CheckEntryEquals));
}
static bool CheckEntryEquals(const DebugSideTable::Entry& a,
const DebugSideTable::Entry& b) {
CHECK_EQ(a.pc_offset(), b.pc_offset());
CHECK(std::equal(a.values().begin(), a.values().end(), b.values().begin(),
b.values().end(), &CheckValueEquals));
return true;
}
static bool CheckValueEquals(const DebugSideTable::Entry::Value& a,
const DebugSideTable::Entry::Value& b) {
CHECK_EQ(a.type, b.type);
CHECK_EQ(a.kind, b.kind);
switch (a.kind) {
case DebugSideTable::Entry::kConstant:
CHECK_EQ(a.i32_const, b.i32_const);
break;
case DebugSideTable::Entry::kRegister:
CHECK_EQ(a.reg_code, b.reg_code);
break;
case DebugSideTable::Entry::kStack:
CHECK_EQ(a.stack_offset, b.stack_offset);
break;
}
return true;
}
OwnedVector<uint8_t> GenerateFunctionBody(
std::initializer_list<uint8_t> raw_function_bytes) {
// Build the function bytes by prepending the locals decl and appending an
// "end" opcode.
OwnedVector<uint8_t> function_bytes =
OwnedVector<uint8_t>::New(raw_function_bytes.size() + 2);
function_bytes[0] = WASM_NO_LOCALS;
std::copy(raw_function_bytes.begin(), raw_function_bytes.end(),
&function_bytes[1]);
function_bytes[raw_function_bytes.size() + 1] = WASM_END;
return function_bytes;
}
FunctionSig* AddSig(std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types) {
ValueType* storage =
zone_.NewArray<ValueType>(return_types.size() + param_types.size());
std::copy(return_types.begin(), return_types.end(), storage);
std::copy(param_types.begin(), param_types.end(),
storage + return_types.size());
FunctionSig* sig = new (&zone_)
FunctionSig{return_types.size(), param_types.size(), storage};
module_builder_.AddSignature(sig);
return sig;
}
TestFunction AddFunction(std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types,
std::initializer_list<uint8_t> raw_function_bytes) {
OwnedVector<uint8_t> function_bytes =
GenerateFunctionBody(raw_function_bytes);
FunctionSig* sig = AddSig(return_types, param_types);
int func_index =
module_builder_.AddFunction(sig, "f", TestingModuleBuilder::kWasm);
WasmFunction* function = module_builder_.GetFunctionAt(func_index);
function->code = {module_builder_.AddBytes(function_bytes.as_vector()),
static_cast<uint32_t>(function_bytes.size())};
FunctionBody body{function->sig, 0, function_bytes.begin(),
function_bytes.end()};
return {std::move(function_bytes), function, body};
}
Isolate* isolate_;
HandleScope handle_scope_;
Zone zone_;
TestingModuleBuilder module_builder_;
};
struct DebugSideTableEntry {
std::vector<DebugSideTable::Entry::Value> values;
// Construct via vector or implicitly via initializer list.
explicit DebugSideTableEntry(std::vector<DebugSideTable::Entry::Value> values)
: values(std::move(values)) {}
DebugSideTableEntry(
std::initializer_list<DebugSideTable::Entry::Value> values)
: values(values) {}
bool operator==(const DebugSideTableEntry& other) const {
if (values.size() != other.values.size()) return false;
for (size_t i = 0; i < values.size(); ++i) {
if (values[i].type != other.values[i].type) return false;
if (values[i].kind != other.values[i].kind) return false;
// Stack offsets and register codes are platform dependent, so only check
// constants here.
if (values[i].kind == DebugSideTable::Entry::kConstant &&
values[i].i32_const != other.values[i].i32_const) {
return false;
}
}
return true;
}
};
// Debug builds will print the vector of DebugSideTableEntry.
#ifdef DEBUG
std::ostream& operator<<(std::ostream& out, const DebugSideTableEntry& entry) {
out << "{";
const char* comma = "";
for (auto& v : entry.values) {
out << comma << v.type.type_name() << " ";
switch (v.kind) {
case DebugSideTable::Entry::kConstant:
out << "const:" << v.i32_const;
break;
case DebugSideTable::Entry::kRegister:
out << "reg";
break;
case DebugSideTable::Entry::kStack:
out << "stack";
break;
}
comma = ", ";
}
return out << "}";
}
std::ostream& operator<<(std::ostream& out,
const std::vector<DebugSideTableEntry>& entries) {
return out << PrintCollection(entries);
}
#endif // DEBUG
// Named constructors to make the tests more readable.
DebugSideTable::Entry::Value Constant(ValueType type, int32_t constant) {
DebugSideTable::Entry::Value value;
value.type = type;
value.kind = DebugSideTable::Entry::kConstant;
value.i32_const = constant;
return value;
}
DebugSideTable::Entry::Value Register(ValueType type) {
DebugSideTable::Entry::Value value;
value.type = type;
value.kind = DebugSideTable::Entry::kRegister;
return value;
}
DebugSideTable::Entry::Value Stack(ValueType type) {
DebugSideTable::Entry::Value value;
value.type = type;
value.kind = DebugSideTable::Entry::kStack;
return value;
}
void CheckDebugSideTable(std::vector<DebugSideTableEntry> expected_entries,
const wasm::DebugSideTable* debug_side_table) {
std::vector<DebugSideTableEntry> entries;
for (auto& entry : debug_side_table->entries()) {
auto values = entry.values();
entries.push_back(
DebugSideTableEntry{std::vector<DebugSideTable::Entry::Value>{
values.begin(), values.end()}});
}
CHECK_EQ(expected_entries, entries);
}
} // namespace
TEST(Liftoff_deterministic_simple) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
}
TEST(Liftoff_deterministic_call) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
}
TEST(Liftoff_deterministic_indirect_call) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_INDIRECT(0, WASM_GET_LOCAL(0), WASM_I32V_1(47)),
WASM_GET_LOCAL(0))});
}
TEST(Liftoff_deterministic_loop) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_LOOP(WASM_BR_IF(0, WASM_GET_LOCAL(0))), WASM_GET_LOCAL(0)});
}
TEST(Liftoff_deterministic_trap) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_DIVS(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
}
TEST(Liftoff_debug_side_table_simple) {
LiftoffCompileEnvironment env;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
CheckDebugSideTable(
{
// function entry, locals in registers.
{Register(kWasmI32), Register(kWasmI32)},
// OOL stack check, locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_call) {
LiftoffCompileEnvironment env;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// call, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_call_const) {
LiftoffCompileEnvironment env;
constexpr int kConst = 13;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_SET_LOCAL(0, WASM_I32V_1(kConst)),
WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// call, local is kConst.
{Constant(kWasmI32, kConst)},
// OOL stack check, local spilled.
{Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_indirect_call) {
LiftoffCompileEnvironment env;
constexpr int kConst = 47;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_INDIRECT(0, WASM_I32V_1(47), WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// indirect call, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL trap (invalid index), local spilled, stack has {kConst}.
{Stack(kWasmI32), Constant(kWasmI32, kConst)},
// OOL trap (sig mismatch), local spilled, stack has {kConst}.
{Stack(kWasmI32), Constant(kWasmI32, kConst)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_loop) {
LiftoffCompileEnvironment env;
constexpr int kConst = 42;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_I32V_1(kConst), WASM_LOOP(WASM_BR_IF(0, WASM_GET_LOCAL(0)))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL loop stack check, local spilled, stack has {kConst}.
{Stack(kWasmI32), Constant(kWasmI32, kConst)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_trap) {
LiftoffCompileEnvironment env;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_DIVS(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
CheckDebugSideTable(
{
// function entry, locals in registers.
{Register(kWasmI32), Register(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
// OOL trap (div by zero), locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
// OOL trap (result unrepresentable), locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_breakpoint_simple) {
LiftoffCompileEnvironment env;
// Set two breakpoints. At both locations, values are live in registers.
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))},
{
1, // break at beginning of function (first local.get)
5 // break at i32.add
});
CheckDebugSideTable(
{
// First break point, locals in registers.
{Register(kWasmI32), Register(kWasmI32)},
// Second break point, locals and two stack values in registers.
{Register(kWasmI32), Register(kWasmI32), Register(kWasmI32),
Register(kWasmI32)},
// OOL stack check, locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
},
debug_side_table.get());
}
} // namespace wasm
} // namespace internal
} // namespace v8