| // Copyright 2017 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/wasm-code-manager.h" |
| |
| #include <algorithm> |
| #include <iomanip> |
| #include <numeric> |
| |
| #include "src/base/atomicops.h" |
| #include "src/base/build_config.h" |
| #include "src/base/iterator.h" |
| #include "src/base/macros.h" |
| #include "src/base/platform/platform.h" |
| #include "src/base/platform/wrappers.h" |
| #include "src/base/small-vector.h" |
| #include "src/base/string-format.h" |
| #include "src/base/vector.h" |
| #include "src/codegen/assembler-inl.h" |
| #include "src/codegen/macro-assembler-inl.h" |
| #include "src/codegen/macro-assembler.h" |
| #include "src/common/code-memory-access.h" |
| #include "src/common/globals.h" |
| #include "src/deoptimizer/deoptimizer.h" |
| #include "src/diagnostics/disassembler.h" |
| #include "src/logging/counters.h" |
| #include "src/logging/log.h" |
| #include "src/objects/objects-inl.h" |
| #include "src/snapshot/embedded/embedded-data-inl.h" |
| #include "src/utils/ostreams.h" |
| #include "src/wasm/code-space-access.h" |
| #include "src/wasm/compilation-environment.h" |
| #include "src/wasm/function-compiler.h" |
| #include "src/wasm/jump-table-assembler.h" |
| #include "src/wasm/module-compiler.h" |
| #include "src/wasm/names-provider.h" |
| #include "src/wasm/pgo.h" |
| #include "src/wasm/std-object-sizes.h" |
| #include "src/wasm/wasm-builtin-list.h" |
| #include "src/wasm/wasm-debug.h" |
| #include "src/wasm/wasm-deopt-data.h" |
| #include "src/wasm/wasm-engine.h" |
| #include "src/wasm/wasm-import-wrapper-cache.h" |
| #include "src/wasm/wasm-module-sourcemap.h" |
| #include "src/wasm/wasm-module.h" |
| #include "src/wasm/wasm-objects-inl.h" |
| #include "src/wasm/wasm-objects.h" |
| #include "src/wasm/well-known-imports.h" |
| |
| #if defined(V8_OS_WIN64) |
| #include "src/diagnostics/unwinding-info-win64.h" |
| #endif // V8_OS_WIN64 |
| |
| #define TRACE_HEAP(...) \ |
| do { \ |
| if (v8_flags.trace_wasm_native_heap) PrintF(__VA_ARGS__); \ |
| } while (false) |
| |
| namespace v8 { |
| namespace internal { |
| namespace wasm { |
| |
| using trap_handler::ProtectedInstructionData; |
| |
| // Check that {WasmCode} objects are sufficiently small. We create many of them, |
| // often for rather small functions. |
| // Increase the limit if needed, but first check if the size increase is |
| // justified. |
| #ifndef V8_GC_MOLE |
| static_assert(sizeof(WasmCode) <= 96); |
| #endif |
| |
| base::AddressRegion DisjointAllocationPool::Merge( |
| base::AddressRegion new_region) { |
| // Find the possible insertion position by identifying the first region whose |
| // start address is not less than that of {new_region}. Since there cannot be |
| // any overlap between regions, this also means that the start of {above} is |
| // bigger or equal than the *end* of {new_region}. |
| auto above = regions_.lower_bound(new_region); |
| DCHECK(above == regions_.end() || above->begin() >= new_region.end()); |
| |
| // Check whether to merge with {above}. |
| if (above != regions_.end() && new_region.end() == above->begin()) { |
| base::AddressRegion merged_region{new_region.begin(), |
| new_region.size() + above->size()}; |
| DCHECK_EQ(merged_region.end(), above->end()); |
| // Check whether to also merge with the region below. |
| if (above != regions_.begin()) { |
| auto below = above; |
| --below; |
| if (below->end() == new_region.begin()) { |
| merged_region = {below->begin(), below->size() + merged_region.size()}; |
| regions_.erase(below); |
| } |
| } |
| auto insert_pos = regions_.erase(above); |
| regions_.insert(insert_pos, merged_region); |
| return merged_region; |
| } |
| |
| // No element below, and not adjavent to {above}: insert and done. |
| if (above == regions_.begin()) { |
| regions_.insert(above, new_region); |
| return new_region; |
| } |
| |
| auto below = above; |
| --below; |
| // Consistency check: |
| DCHECK(above == regions_.end() || below->end() < above->begin()); |
| |
| // Adjacent to {below}: merge and done. |
| if (below->end() == new_region.begin()) { |
| base::AddressRegion merged_region{below->begin(), |
| below->size() + new_region.size()}; |
| DCHECK_EQ(merged_region.end(), new_region.end()); |
| regions_.erase(below); |
| regions_.insert(above, merged_region); |
| return merged_region; |
| } |
| |
| // Not adjacent to any existing region: insert between {below} and {above}. |
| DCHECK_LT(below->end(), new_region.begin()); |
| regions_.insert(above, new_region); |
| return new_region; |
| } |
| |
| base::AddressRegion DisjointAllocationPool::Allocate(size_t size) { |
| return AllocateInRegion(size, |
| {kNullAddress, std::numeric_limits<size_t>::max()}); |
| } |
| |
| base::AddressRegion DisjointAllocationPool::AllocateInRegion( |
| size_t size, base::AddressRegion region) { |
| // Get an iterator to the first contained region whose start address is not |
| // smaller than the start address of {region}. Start the search from the |
| // region one before that (the last one whose start address is smaller). |
| auto it = regions_.lower_bound(region); |
| if (it != regions_.begin()) --it; |
| |
| for (auto end = regions_.end(); it != end; ++it) { |
| base::AddressRegion overlap = it->GetOverlap(region); |
| if (size > overlap.size()) continue; |
| base::AddressRegion ret{overlap.begin(), size}; |
| base::AddressRegion old = *it; |
| auto insert_pos = regions_.erase(it); |
| if (size == old.size()) { |
| // We use the full region --> nothing to add back. |
| } else if (ret.begin() == old.begin()) { |
| // We return a region at the start --> shrink old region from front. |
| regions_.insert(insert_pos, {old.begin() + size, old.size() - size}); |
| } else if (ret.end() == old.end()) { |
| // We return a region at the end --> shrink remaining region. |
| regions_.insert(insert_pos, {old.begin(), old.size() - size}); |
| } else { |
| // We return something in the middle --> split the remaining region |
| // (insert the region with smaller address first). |
| regions_.insert(insert_pos, {old.begin(), ret.begin() - old.begin()}); |
| regions_.insert(insert_pos, {ret.end(), old.end() - ret.end()}); |
| } |
| return ret; |
| } |
| return {}; |
| } |
| |
| Address WasmCode::constant_pool() const { |
| if (V8_EMBEDDED_CONSTANT_POOL_BOOL) { |
| if (constant_pool_offset_ < code_comments_offset_) { |
| return instruction_start() + constant_pool_offset_; |
| } |
| } |
| return kNullAddress; |
| } |
| |
| Address WasmCode::handler_table() const { |
| return instruction_start() + handler_table_offset_; |
| } |
| |
| int WasmCode::handler_table_size() const { |
| DCHECK_GE(constant_pool_offset_, handler_table_offset_); |
| return static_cast<int>(constant_pool_offset_ - handler_table_offset_); |
| } |
| |
| Address WasmCode::code_comments() const { |
| return instruction_start() + code_comments_offset_; |
| } |
| |
| int WasmCode::code_comments_size() const { |
| DCHECK_GE(unpadded_binary_size_, code_comments_offset_); |
| return static_cast<int>(unpadded_binary_size_ - code_comments_offset_); |
| } |
| |
| std::unique_ptr<const uint8_t[]> WasmCode::ConcatenateBytes( |
| std::initializer_list<base::Vector<const uint8_t>> vectors) { |
| size_t total_size = 0; |
| for (auto& vec : vectors) total_size += vec.size(); |
| // Use default-initialization (== no initialization). |
| std::unique_ptr<uint8_t[]> result{new uint8_t[total_size]}; |
| uint8_t* ptr = result.get(); |
| for (auto& vec : vectors) { |
| if (vec.empty()) continue; // Avoid nullptr in {memcpy}. |
| memcpy(ptr, vec.begin(), vec.size()); |
| ptr += vec.size(); |
| } |
| return result; |
| } |
| |
| void WasmCode::RegisterTrapHandlerData() { |
| DCHECK(!has_trap_handler_index()); |
| if (kind() != WasmCode::kWasmFunction) return; |
| if (protected_instructions_size_ == 0) return; |
| |
| Address base = instruction_start(); |
| |
| size_t size = instructions().size(); |
| auto protected_instruction_data = this->protected_instructions(); |
| const int index = |
| RegisterHandlerData(base, size, protected_instruction_data.size(), |
| protected_instruction_data.begin()); |
| |
| // TODO(eholk): if index is negative, fail. |
| CHECK_LE(0, index); |
| set_trap_handler_index(index); |
| DCHECK(has_trap_handler_index()); |
| } |
| |
| bool WasmCode::ShouldBeLogged(Isolate* isolate) { |
| // The return value is cached in {WasmEngine::IsolateData::log_codes}. Ensure |
| // to call {WasmEngine::EnableCodeLogging} if this return value would change |
| // for any isolate. Otherwise we might lose code events. |
| return isolate->IsLoggingCodeCreation(); |
| } |
| |
| std::string WasmCode::DebugName() const { |
| switch (kind()) { |
| case kWasmToCapiWrapper: |
| return "wasm-to-c"; |
| case kJumpTable: |
| return "jump-table"; |
| case kWasmToJsWrapper: |
| return "wasm-to-js"; |
| case kWasmFunction: |
| // Gets handled below |
| break; |
| } |
| |
| ModuleWireBytes wire_bytes(native_module()->wire_bytes()); |
| const WasmModule* module = native_module()->module(); |
| WireBytesRef name_ref = |
| module->lazily_generated_names.LookupFunctionName(wire_bytes, index()); |
| WasmName name = wire_bytes.GetNameOrNull(name_ref); |
| std::string name_buffer; |
| if (name.empty()) { |
| name_buffer.resize(32); |
| name_buffer.resize( |
| SNPrintF(base::VectorOf(&name_buffer.front(), name_buffer.size()), |
| "wasm-function[%d]", index())); |
| } else { |
| name_buffer.append(name.begin(), name.end()); |
| } |
| return name_buffer; |
| } |
| |
| void WasmCode::LogCode(Isolate* isolate, const char* source_url, |
| int script_id) const { |
| DCHECK(ShouldBeLogged(isolate)); |
| if (IsAnonymous() && kind() != WasmCode::Kind::kWasmToJsWrapper) return; |
| |
| ModuleWireBytes wire_bytes(native_module_->wire_bytes()); |
| const WasmModule* module = native_module_->module(); |
| std::string fn_name = DebugName(); |
| WasmName name = base::VectorOf(fn_name); |
| |
| const WasmDebugSymbols& debug_symbols = module->debug_symbols; |
| auto load_wasm_source_map = isolate->wasm_load_source_map_callback(); |
| auto source_map = native_module_->GetWasmSourceMap(); |
| if (!source_map && debug_symbols.type == WasmDebugSymbols::Type::SourceMap && |
| !debug_symbols.external_url.is_empty() && load_wasm_source_map) { |
| WasmName external_url = |
| wire_bytes.GetNameOrNull(debug_symbols.external_url); |
| std::string external_url_string(external_url.data(), external_url.size()); |
| HandleScope scope(isolate); |
| v8::Isolate* v8_isolate = reinterpret_cast<v8::Isolate*>(isolate); |
| Local<v8::String> source_map_str = |
| load_wasm_source_map(v8_isolate, external_url_string.c_str()); |
| native_module_->SetWasmSourceMap( |
| std::make_unique<WasmModuleSourceMap>(v8_isolate, source_map_str)); |
| } |
| |
| // Record source positions before adding code, otherwise when code is added, |
| // there are no source positions to associate with the added code. |
| if (!source_positions().empty()) { |
| LOG_CODE_EVENT(isolate, WasmCodeLinePosInfoRecordEvent(instruction_start(), |
| source_positions())); |
| } |
| |
| int code_offset = 0; |
| if (!IsAnonymous()) { |
| code_offset = module->functions[index_].code.offset(); |
| } |
| PROFILE(isolate, CodeCreateEvent(LogEventListener::CodeTag::kFunction, this, |
| name, source_url, code_offset, script_id)); |
| } |
| |
| namespace { |
| bool ProtectedInstructionDataCompare(const ProtectedInstructionData& left, |
| const ProtectedInstructionData& right) { |
| return left.instr_offset < right.instr_offset; |
| } |
| } // namespace |
| |
| bool WasmCode::IsProtectedInstruction(Address pc) { |
| base::Vector<const trap_handler::ProtectedInstructionData> instructions = |
| protected_instructions(); |
| ProtectedInstructionData offset{ |
| static_cast<uint32_t>(pc - instruction_start())}; |
| return std::binary_search(instructions.begin(), instructions.end(), offset, |
| ProtectedInstructionDataCompare); |
| } |
| |
| void WasmCode::Validate() const { |
| // The packing strategy for {tagged_parameter_slots} only works if both the |
| // max number of parameters and their max combined stack slot usage fits into |
| // their respective half of the result value. |
| static_assert(wasm::kV8MaxWasmFunctionParams < |
| std::numeric_limits<uint16_t>::max()); |
| static constexpr int kMaxSlotsPerParam = 4; // S128 on 32-bit platforms. |
| static_assert(wasm::kV8MaxWasmFunctionParams * kMaxSlotsPerParam < |
| std::numeric_limits<uint16_t>::max()); |
| |
| #ifdef DEBUG |
| // Scope for foreign WasmCode pointers. |
| WasmCodeRefScope code_ref_scope; |
| // We expect certain relocation info modes to never appear in {WasmCode} |
| // objects or to be restricted to a small set of valid values. Hence the |
| // iteration below does not use a mask, but visits all relocation data. |
| for (RelocIterator it(instructions(), reloc_info(), constant_pool()); |
| !it.done(); it.next()) { |
| RelocInfo::Mode mode = it.rinfo()->rmode(); |
| switch (mode) { |
| case RelocInfo::WASM_CALL: { |
| Address target = it.rinfo()->wasm_call_address(); |
| WasmCode* code = native_module_->Lookup(target); |
| CHECK_NOT_NULL(code); |
| CHECK_EQ(WasmCode::kJumpTable, code->kind()); |
| CHECK(code->contains(target)); |
| break; |
| } |
| case RelocInfo::WASM_STUB_CALL: { |
| Address target = it.rinfo()->wasm_stub_call_address(); |
| WasmCode* code = native_module_->Lookup(target); |
| CHECK_NOT_NULL(code); |
| CHECK_EQ(WasmCode::kJumpTable, code->kind()); |
| CHECK(code->contains(target)); |
| break; |
| } |
| case RelocInfo::INTERNAL_REFERENCE: |
| case RelocInfo::INTERNAL_REFERENCE_ENCODED: { |
| Address target = it.rinfo()->target_internal_reference(); |
| CHECK(contains(target)); |
| break; |
| } |
| case RelocInfo::EXTERNAL_REFERENCE: |
| case RelocInfo::CONST_POOL: |
| case RelocInfo::VENEER_POOL: |
| // These are OK to appear. |
| break; |
| default: |
| FATAL("Unexpected mode: %d", mode); |
| } |
| } |
| #endif |
| } |
| |
| void WasmCode::MaybePrint() const { |
| // Determines whether flags want this code to be printed. |
| bool function_index_matches = |
| (!IsAnonymous() && |
| v8_flags.print_wasm_code_function_index == static_cast<int>(index())); |
| if (v8_flags.print_code || |
| (kind() == kWasmFunction |
| ? (v8_flags.print_wasm_code || function_index_matches) |
| : v8_flags.print_wasm_stub_code.value())) { |
| std::string name = DebugName(); |
| Print(name.c_str()); |
| } |
| } |
| |
| void WasmCode::Print(const char* name) const { |
| StdoutStream os; |
| os << "--- WebAssembly code ---\n"; |
| Disassemble(name, os); |
| if (native_module_->HasDebugInfo()) { |
| if (auto* debug_side_table = |
| native_module_->GetDebugInfo()->GetDebugSideTableIfExists(this)) { |
| debug_side_table->Print(os); |
| } |
| } |
| os << "--- End code ---\n"; |
| } |
| |
| void WasmCode::Disassemble(const char* name, std::ostream& os, |
| Address current_pc) const { |
| if (name) os << "name: " << name << "\n"; |
| if (!IsAnonymous()) os << "index: " << index() << "\n"; |
| os << "kind: " << GetWasmCodeKindAsString(kind()) << "\n"; |
| if (kind() == kWasmFunction) { |
| DCHECK(is_liftoff() || tier() == ExecutionTier::kTurbofan); |
| const char* compiler = |
| is_liftoff() ? (for_debugging() ? "Liftoff (debug)" : "Liftoff") |
| : "TurboFan"; |
| os << "compiler: " << compiler << "\n"; |
| } |
| size_t padding = instructions().size() - unpadded_binary_size_; |
| os << "Body (size = " << instructions().size() << " = " |
| << unpadded_binary_size_ << " + " << padding << " padding)\n"; |
| |
| int instruction_size = unpadded_binary_size_; |
| if (constant_pool_offset_ < instruction_size) { |
| instruction_size = constant_pool_offset_; |
| } |
| if (safepoint_table_offset_ && safepoint_table_offset_ < instruction_size) { |
| instruction_size = safepoint_table_offset_; |
| } |
| if (handler_table_offset_ < instruction_size) { |
| instruction_size = handler_table_offset_; |
| } |
| DCHECK_LT(0, instruction_size); |
| |
| #ifdef ENABLE_DISASSEMBLER |
| os << "Instructions (size = " << instruction_size << ")\n"; |
| Disassembler::Decode(nullptr, os, instructions().begin(), |
| instructions().begin() + instruction_size, |
| CodeReference(this), current_pc); |
| os << "\n"; |
| |
| if (handler_table_size() > 0) { |
| HandlerTable table(this); |
| os << "Exception Handler Table (size = " << table.NumberOfReturnEntries() |
| << "):\n"; |
| table.HandlerTableReturnPrint(os); |
| os << "\n"; |
| } |
| |
| if (protected_instructions_size_ > 0) { |
| os << "Protected instructions:\n pc offset\n"; |
| for (auto& data : protected_instructions()) { |
| os << std::setw(10) << std::hex << data.instr_offset << std::setw(10) |
| << "\n"; |
| } |
| os << "\n"; |
| } |
| |
| if (!source_positions().empty()) { |
| os << "Source positions:\n pc offset position\n"; |
| for (SourcePositionTableIterator it(source_positions()); !it.done(); |
| it.Advance()) { |
| os << std::setw(10) << std::hex << it.code_offset() << std::dec |
| << std::setw(10) << it.source_position().ScriptOffset() |
| << (it.is_statement() ? " statement" : "") << "\n"; |
| } |
| os << "\n"; |
| } |
| |
| if (deopt_data_size_ > 0) { |
| // TODO(mliedtke): It'd be more readable to format this as a table. |
| WasmDeoptView view(deopt_data()); |
| const WasmDeoptData data = view.GetDeoptData(); |
| os << "Deopt exits (entries = " << data.entry_count |
| << ", byte size = " << deopt_data_size_ << ")\n"; |
| uint32_t deopt_offset = data.deopt_exit_start_offset; |
| for (uint32_t i = 0; i < data.entry_count; ++i) { |
| WasmDeoptEntry entry = view.GetDeoptEntry(i); |
| os << std::hex << deopt_offset << std::dec |
| << ": function offset = " << entry.bytecode_offset |
| << ", translation = " << entry.translation_index << '\n'; |
| deopt_offset += Deoptimizer::kEagerDeoptExitSize; |
| } |
| os << '\n'; |
| } |
| |
| if (safepoint_table_offset_ > 0) { |
| SafepointTable table(this); |
| table.Print(os); |
| os << "\n"; |
| } |
| |
| os << "RelocInfo (size = " << reloc_info().size() << ")\n"; |
| for (RelocIterator it(instructions(), reloc_info(), constant_pool()); |
| !it.done(); it.next()) { |
| it.rinfo()->Print(nullptr, os); |
| } |
| os << "\n"; |
| #else // !ENABLE_DISASSEMBLER |
| os << "Instructions (size = " << instruction_size << ", " |
| << static_cast<void*>(instructions().begin()) << "-" |
| << static_cast<void*>(instructions().begin() + instruction_size) << ")\n"; |
| #endif // !ENABLE_DISASSEMBLER |
| } |
| |
| const char* GetWasmCodeKindAsString(WasmCode::Kind kind) { |
| switch (kind) { |
| case WasmCode::kWasmFunction: |
| return "wasm function"; |
| case WasmCode::kWasmToCapiWrapper: |
| return "wasm-to-capi"; |
| case WasmCode::kWasmToJsWrapper: |
| return "wasm-to-js"; |
| case WasmCode::kJumpTable: |
| return "jump table"; |
| } |
| return "unknown kind"; |
| } |
| |
| WasmCode::~WasmCode() { |
| if (has_trap_handler_index()) { |
| trap_handler::ReleaseHandlerData(trap_handler_index()); |
| } |
| } |
| |
| V8_WARN_UNUSED_RESULT bool WasmCode::DecRefOnPotentiallyDeadCode() { |
| if (GetWasmEngine()->AddPotentiallyDeadCode(this)) { |
| // The code just became potentially dead. The ref count we wanted to |
| // decrement is now transferred to the set of potentially dead code, and |
| // will be decremented when the next GC is run. |
| return false; |
| } |
| // If we reach here, the code was already potentially dead. Decrement the ref |
| // count, and return true if it drops to zero. |
| return DecRefOnDeadCode(); |
| } |
| |
| // static |
| void WasmCode::DecrementRefCount(base::Vector<WasmCode* const> code_vec) { |
| // Decrement the ref counter of all given code objects. Keep the ones whose |
| // ref count drops to zero. |
| WasmEngine::DeadCodeMap dead_code; |
| for (WasmCode* code : code_vec) { |
| if (!code->DecRef()) continue; // Remaining references. |
| dead_code[code->native_module()].push_back(code); |
| } |
| |
| if (dead_code.empty()) return; |
| |
| GetWasmEngine()->FreeDeadCode(dead_code); |
| } |
| |
| SourcePosition WasmCode::GetSourcePositionBefore(int code_offset) { |
| SourcePosition position; |
| for (SourcePositionTableIterator iterator(source_positions()); |
| !iterator.done() && iterator.code_offset() < code_offset; |
| iterator.Advance()) { |
| position = iterator.source_position(); |
| } |
| return position; |
| } |
| |
| int WasmCode::GetSourceOffsetBefore(int code_offset) { |
| return GetSourcePositionBefore(code_offset).ScriptOffset(); |
| } |
| |
| std::tuple<int, bool, SourcePosition> WasmCode::GetInliningPosition( |
| int inlining_id) const { |
| const size_t elem_size = sizeof(int) + sizeof(bool) + sizeof(SourcePosition); |
| const uint8_t* start = inlining_positions().begin() + elem_size * inlining_id; |
| DCHECK_LE(start, inlining_positions().end()); |
| std::tuple<int, bool, SourcePosition> result; |
| std::memcpy(&std::get<0>(result), start, sizeof std::get<0>(result)); |
| std::memcpy(&std::get<1>(result), start + sizeof std::get<0>(result), |
| sizeof std::get<1>(result)); |
| std::memcpy(&std::get<2>(result), |
| start + sizeof std::get<0>(result) + sizeof std::get<1>(result), |
| sizeof std::get<2>(result)); |
| return result; |
| } |
| |
| size_t WasmCode::EstimateCurrentMemoryConsumption() const { |
| UPDATE_WHEN_CLASS_CHANGES(WasmCode, 96); |
| size_t result = sizeof(WasmCode); |
| // For meta_data_. |
| result += protected_instructions_size_ + reloc_info_size_ + |
| source_positions_size_ + inlining_positions_size_ + |
| deopt_data_size_; |
| return result; |
| } |
| |
| WasmCodeAllocator::WasmCodeAllocator(std::shared_ptr<Counters> async_counters) |
| : async_counters_(std::move(async_counters)) { |
| owned_code_space_.reserve(4); |
| } |
| |
| WasmCodeAllocator::~WasmCodeAllocator() { |
| GetWasmCodeManager()->FreeNativeModule(base::VectorOf(owned_code_space_), |
| committed_code_space()); |
| } |
| |
| void WasmCodeAllocator::Init(VirtualMemory code_space) { |
| DCHECK(owned_code_space_.empty()); |
| DCHECK(free_code_space_.IsEmpty()); |
| free_code_space_.Merge(code_space.region()); |
| owned_code_space_.emplace_back(std::move(code_space)); |
| async_counters_->wasm_module_num_code_spaces()->AddSample(1); |
| } |
| |
| namespace { |
| // On Windows, we cannot commit a region that straddles different reservations |
| // of virtual memory. Because we bump-allocate, and because, if we need more |
| // memory, we append that memory at the end of the owned_code_space_ list, we |
| // traverse that list in reverse order to find the reservation(s) that guide how |
| // to chunk the region to commit. |
| #if V8_OS_WIN |
| constexpr bool kNeedsToSplitRangeByReservations = true; |
| #else |
| constexpr bool kNeedsToSplitRangeByReservations = false; |
| #endif |
| |
| base::SmallVector<base::AddressRegion, 1> SplitRangeByReservationsIfNeeded( |
| base::AddressRegion range, |
| const std::vector<VirtualMemory>& owned_code_space) { |
| if (!kNeedsToSplitRangeByReservations) return {range}; |
| |
| base::SmallVector<base::AddressRegion, 1> split_ranges; |
| size_t missing_begin = range.begin(); |
| size_t missing_end = range.end(); |
| for (auto& vmem : base::Reversed(owned_code_space)) { |
| Address overlap_begin = std::max(missing_begin, vmem.address()); |
| Address overlap_end = std::min(missing_end, vmem.end()); |
| if (overlap_begin >= overlap_end) continue; |
| split_ranges.emplace_back(overlap_begin, overlap_end - overlap_begin); |
| // Opportunistically reduce the missing range. This might terminate the loop |
| // early. |
| if (missing_begin == overlap_begin) missing_begin = overlap_end; |
| if (missing_end == overlap_end) missing_end = overlap_begin; |
| if (missing_begin >= missing_end) break; |
| } |
| #ifdef ENABLE_SLOW_DCHECKS |
| // The returned vector should cover the full range. |
| size_t total_split_size = 0; |
| for (auto split : split_ranges) total_split_size += split.size(); |
| DCHECK_EQ(range.size(), total_split_size); |
| #endif |
| return split_ranges; |
| } |
| |
| int NumWasmFunctionsInFarJumpTable(uint32_t num_declared_functions) { |
| return NativeModule::kNeedsFarJumpsBetweenCodeSpaces |
| ? static_cast<int>(num_declared_functions) |
| : 0; |
| } |
| |
| // Returns an overapproximation of the code size overhead per new code space |
| // created by the jump tables. |
| size_t OverheadPerCodeSpace(uint32_t num_declared_functions) { |
| // Overhead for the jump table. |
| size_t overhead = RoundUp<kCodeAlignment>( |
| JumpTableAssembler::SizeForNumberOfSlots(num_declared_functions)); |
| |
| #if defined(V8_OS_WIN64) |
| // On Win64, we need to reserve some pages at the beginning of an executable |
| // space. See {AddCodeSpace}. |
| overhead += Heap::GetCodeRangeReservedAreaSize(); |
| #endif // V8_OS_WIN64 |
| |
| // Overhead for the far jump table. |
| overhead += |
| RoundUp<kCodeAlignment>(JumpTableAssembler::SizeForNumberOfFarJumpSlots( |
| BuiltinLookup::BuiltinCount(), |
| NumWasmFunctionsInFarJumpTable(num_declared_functions))); |
| |
| return overhead; |
| } |
| |
| // Returns an estimate how much code space should be reserved. This can be |
| // smaller than the passed-in {code_size_estimate}, see comments in the code. |
| size_t ReservationSize(size_t code_size_estimate, int num_declared_functions, |
| size_t total_reserved) { |
| size_t overhead = OverheadPerCodeSpace(num_declared_functions); |
| |
| // Reserve the maximum of |
| // a) needed size + overhead (this is the minimum needed) |
| // b) 2 * overhead (to not waste too much space by overhead) |
| // c) 1/4 of current total reservation size (to grow exponentially) |
| // For the minimum size we only take the overhead into account and not the |
| // code space estimate, for two reasons: |
| // - The code space estimate is only an estimate; we might actually need less |
| // space later. |
| // - When called at module construction time we pass the estimate for all |
| // code in the module; this can still be split up into multiple spaces |
| // later. |
| size_t minimum_size = 2 * overhead; |
| size_t suggested_size = |
| std::max(std::max(RoundUp<kCodeAlignment>(code_size_estimate) + overhead, |
| minimum_size), |
| total_reserved / 4); |
| |
| const size_t max_code_space_size = |
| size_t{v8_flags.wasm_max_code_space_size_mb} * MB; |
| if (V8_UNLIKELY(minimum_size > max_code_space_size)) { |
| auto oom_detail = base::FormattedString{} |
| << "required reservation minimum (" << minimum_size |
| << ") is bigger than supported maximum (" |
| << max_code_space_size << ")"; |
| V8::FatalProcessOutOfMemory(nullptr, |
| "Exceeding maximum wasm code space size", |
| oom_detail.PrintToArray().data()); |
| UNREACHABLE(); |
| } |
| |
| // Limit by the maximum code space size. |
| size_t reserve_size = std::min(max_code_space_size, suggested_size); |
| |
| return reserve_size; |
| } |
| |
| // Sentinel value to be used for {AllocateForCodeInRegion} for specifying no |
| // restriction on the region to allocate in. |
| constexpr base::AddressRegion kUnrestrictedRegion{ |
| kNullAddress, std::numeric_limits<size_t>::max()}; |
| |
| } // namespace |
| |
| base::Vector<uint8_t> WasmCodeAllocator::AllocateForCode( |
| NativeModule* native_module, size_t size) { |
| return AllocateForCodeInRegion(native_module, size, kUnrestrictedRegion); |
| } |
| |
| base::Vector<uint8_t> WasmCodeAllocator::AllocateForCodeInRegion( |
| NativeModule* native_module, size_t size, base::AddressRegion region) { |
| DCHECK_LT(0, size); |
| auto* code_manager = GetWasmCodeManager(); |
| size = RoundUp<kCodeAlignment>(size); |
| base::AddressRegion code_space = |
| free_code_space_.AllocateInRegion(size, region); |
| if (V8_UNLIKELY(code_space.is_empty())) { |
| // Only allocations without a specific region are allowed to fail. Otherwise |
| // the region must have been allocated big enough to hold all initial |
| // allocations (jump tables etc). |
| CHECK_EQ(kUnrestrictedRegion, region); |
| |
| size_t total_reserved = 0; |
| for (auto& vmem : owned_code_space_) total_reserved += vmem.size(); |
| size_t reserve_size = ReservationSize( |
| size, native_module->module()->num_declared_functions, total_reserved); |
| if (reserve_size < size) { |
| auto oom_detail = base::FormattedString{} |
| << "cannot reserve space for " << size |
| << "bytes of code (maximum reservation size is " |
| << reserve_size << ")"; |
| V8::FatalProcessOutOfMemory(nullptr, "Grow wasm code space", |
| oom_detail.PrintToArray().data()); |
| } |
| VirtualMemory new_mem = code_manager->TryAllocate(reserve_size); |
| if (!new_mem.IsReserved()) { |
| auto oom_detail = base::FormattedString{} |
| << "cannot allocate more code space (" << reserve_size |
| << " bytes, currently " << total_reserved << ")"; |
| V8::FatalProcessOutOfMemory(nullptr, "Grow wasm code space", |
| oom_detail.PrintToArray().data()); |
| UNREACHABLE(); |
| } |
| |
| base::AddressRegion new_region = new_mem.region(); |
| code_manager->AssignRange(new_region, native_module); |
| free_code_space_.Merge(new_region); |
| owned_code_space_.emplace_back(std::move(new_mem)); |
| native_module->AddCodeSpaceLocked(new_region); |
| |
| code_space = free_code_space_.Allocate(size); |
| CHECK(!code_space.is_empty()); |
| |
| async_counters_->wasm_module_num_code_spaces()->AddSample( |
| static_cast<int>(owned_code_space_.size())); |
| } |
| const Address commit_page_size = CommitPageSize(); |
| Address commit_start = RoundUp(code_space.begin(), commit_page_size); |
| |
| Address commit_end = RoundUp(code_space.end(), commit_page_size); |
| // {commit_start} will be either code_space.start or the start of the next |
| // page. {commit_end} will be the start of the page after the one in which |
| // the allocation ends. |
| // We start from an aligned start, and we know we allocated vmem in |
| // page multiples. |
| // We just need to commit what's not committed. The page in which we |
| // start is already committed (or we start at the beginning of a page). |
| // The end needs to be committed all through the end of the page. |
| if (commit_start < commit_end) { |
| for (base::AddressRegion split_range : SplitRangeByReservationsIfNeeded( |
| {commit_start, commit_end - commit_start}, owned_code_space_)) { |
| code_manager->Commit(split_range); |
| } |
| committed_code_space_.fetch_add(commit_end - commit_start); |
| // Committed code cannot grow bigger than maximum code space size. |
| DCHECK_LE(committed_code_space_.load(), |
| v8_flags.wasm_max_committed_code_mb * MB); |
| } |
| DCHECK(IsAligned(code_space.begin(), kCodeAlignment)); |
| generated_code_size_.fetch_add(code_space.size(), std::memory_order_relaxed); |
| |
| TRACE_HEAP("Code alloc for %p: 0x%" PRIxPTR ",+%zu\n", this, |
| code_space.begin(), size); |
| return {reinterpret_cast<uint8_t*>(code_space.begin()), code_space.size()}; |
| } |
| |
| void WasmCodeAllocator::FreeCode(base::Vector<WasmCode* const> codes) { |
| // Zap code area and collect freed code regions. |
| DisjointAllocationPool freed_regions; |
| size_t code_size = 0; |
| for (WasmCode* code : codes) { |
| code_size += code->instructions().size(); |
| freed_regions.Merge(base::AddressRegion{code->instruction_start(), |
| code->instructions().size()}); |
| ThreadIsolation::UnregisterWasmAllocation(code->instruction_start(), |
| code->instructions().size()); |
| } |
| freed_code_size_.fetch_add(code_size); |
| |
| // Merge {freed_regions} into {freed_code_space_} and put all ranges of full |
| // pages to decommit into {regions_to_decommit} (decommitting is expensive, |
| // so try to merge regions before decommitting). |
| DisjointAllocationPool regions_to_decommit; |
| size_t commit_page_size = CommitPageSize(); |
| for (auto region : freed_regions.regions()) { |
| auto merged_region = freed_code_space_.Merge(region); |
| Address discard_start = |
| std::max(RoundUp(merged_region.begin(), commit_page_size), |
| RoundDown(region.begin(), commit_page_size)); |
| Address discard_end = |
| std::min(RoundDown(merged_region.end(), commit_page_size), |
| RoundUp(region.end(), commit_page_size)); |
| if (discard_start >= discard_end) continue; |
| regions_to_decommit.Merge({discard_start, discard_end - discard_start}); |
| } |
| |
| auto* code_manager = GetWasmCodeManager(); |
| for (auto region : regions_to_decommit.regions()) { |
| [[maybe_unused]] size_t old_committed = |
| committed_code_space_.fetch_sub(region.size()); |
| DCHECK_GE(old_committed, region.size()); |
| for (base::AddressRegion split_range : |
| SplitRangeByReservationsIfNeeded(region, owned_code_space_)) { |
| code_manager->Decommit(split_range); |
| } |
| } |
| } |
| |
| size_t WasmCodeAllocator::GetNumCodeSpaces() const { |
| return owned_code_space_.size(); |
| } |
| |
| NativeModule::NativeModule(WasmFeatures enabled, |
| CompileTimeImports compile_imports, |
| DynamicTiering dynamic_tiering, |
| VirtualMemory code_space, |
| std::shared_ptr<const WasmModule> module, |
| std::shared_ptr<Counters> async_counters, |
| std::shared_ptr<NativeModule>* shared_this) |
| : engine_scope_( |
| GetWasmEngine()->GetBarrierForBackgroundCompile()->TryLock()), |
| code_allocator_(async_counters), |
| enabled_features_(enabled), |
| compile_imports_(compile_imports), |
| module_(std::move(module)), |
| fast_api_targets_( |
| new std::atomic<Address>[module_->num_imported_functions]()), |
| fast_api_return_is_bool_( |
| new std::atomic<bool>[module_->num_imported_functions]()) { |
| DCHECK(engine_scope_); |
| // We receive a pointer to an empty {std::shared_ptr}, and install ourselve |
| // there. |
| DCHECK_NOT_NULL(shared_this); |
| DCHECK_NULL(*shared_this); |
| shared_this->reset(this); |
| compilation_state_ = CompilationState::New( |
| *shared_this, std::move(async_counters), dynamic_tiering); |
| compilation_state_->InitCompileJob(); |
| DCHECK_NOT_NULL(module_); |
| if (module_->num_declared_functions > 0) { |
| code_table_ = |
| std::make_unique<WasmCode*[]>(module_->num_declared_functions); |
| tiering_budgets_ = |
| std::make_unique<uint32_t[]>(module_->num_declared_functions); |
| |
| std::fill_n(tiering_budgets_.get(), module_->num_declared_functions, |
| v8_flags.wasm_tiering_budget); |
| } |
| // Even though there cannot be another thread using this object (since we are |
| // just constructing it), we need to hold the mutex to fulfill the |
| // precondition of {WasmCodeAllocator::Init}, which calls |
| // {NativeModule::AddCodeSpaceLocked}. |
| base::RecursiveMutexGuard guard{&allocation_mutex_}; |
| auto initial_region = code_space.region(); |
| code_allocator_.Init(std::move(code_space)); |
| AddCodeSpaceLocked(initial_region); |
| } |
| |
| void NativeModule::ReserveCodeTableForTesting(uint32_t max_functions) { |
| WasmCodeRefScope code_ref_scope; |
| CHECK_LE(module_->num_declared_functions, max_functions); |
| auto new_table = std::make_unique<WasmCode*[]>(max_functions); |
| if (module_->num_declared_functions > 0) { |
| memcpy(new_table.get(), code_table_.get(), |
| module_->num_declared_functions * sizeof(WasmCode*)); |
| } |
| code_table_ = std::move(new_table); |
| |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| CHECK_EQ(1, code_space_data_.size()); |
| base::AddressRegion single_code_space_region = code_space_data_[0].region; |
| // Re-allocate the near and far jump tables. |
| main_jump_table_ = CreateEmptyJumpTableInRegionLocked( |
| JumpTableAssembler::SizeForNumberOfSlots(max_functions), |
| single_code_space_region, JumpTableType::kJumpTable); |
| CHECK( |
| single_code_space_region.contains(main_jump_table_->instruction_start())); |
| main_far_jump_table_ = CreateEmptyJumpTableInRegionLocked( |
| JumpTableAssembler::SizeForNumberOfFarJumpSlots( |
| BuiltinLookup::BuiltinCount(), |
| NumWasmFunctionsInFarJumpTable(max_functions)), |
| single_code_space_region, JumpTableType::kFarJumpTable); |
| CHECK(single_code_space_region.contains( |
| main_far_jump_table_->instruction_start())); |
| code_space_data_[0].jump_table = main_jump_table_; |
| InitializeJumpTableForLazyCompilation(max_functions); |
| } |
| |
| void NativeModule::LogWasmCodes(Isolate* isolate, Tagged<Script> script) { |
| DisallowGarbageCollection no_gc; |
| if (!WasmCode::ShouldBeLogged(isolate)) return; |
| |
| TRACE_EVENT1("v8.wasm", "wasm.LogWasmCodes", "functions", |
| module_->num_declared_functions); |
| |
| Tagged<Object> url_obj = script->name(); |
| DCHECK(IsString(url_obj) || IsUndefined(url_obj)); |
| std::unique_ptr<char[]> source_url = |
| IsString(url_obj) ? String::cast(url_obj)->ToCString() |
| : std::unique_ptr<char[]>(new char[1]{'\0'}); |
| |
| // Log all owned code, not just the current entries in the code table. This |
| // will also include import wrappers. |
| WasmCodeRefScope code_ref_scope; |
| for (auto& code : SnapshotAllOwnedCode()) { |
| code->LogCode(isolate, source_url.get(), script->id()); |
| } |
| } |
| |
| WasmCode* NativeModule::AddCodeForTesting(Handle<Code> code) { |
| const size_t relocation_size = code->relocation_size(); |
| base::OwnedVector<uint8_t> reloc_info; |
| if (relocation_size > 0) { |
| reloc_info = base::OwnedVector<uint8_t>::Of( |
| base::Vector<uint8_t>{code->relocation_start(), relocation_size}); |
| } |
| Handle<TrustedByteArray> source_pos_table( |
| code->source_position_table(), code->instruction_stream()->GetIsolate()); |
| int source_pos_len = source_pos_table->length(); |
| auto source_pos = base::OwnedVector<uint8_t>::NewForOverwrite(source_pos_len); |
| if (source_pos_len > 0) { |
| MemCopy(source_pos.begin(), source_pos_table->begin(), source_pos_len); |
| } |
| |
| static_assert(InstructionStream::kOnHeapBodyIsContiguous); |
| base::Vector<const uint8_t> instructions( |
| reinterpret_cast<uint8_t*>(code->body_start()), |
| static_cast<size_t>(code->body_size())); |
| const int stack_slots = code->stack_slots(); |
| |
| // Metadata offsets in InstructionStream objects are relative to the start of |
| // the metadata section, whereas WasmCode expects offsets relative to |
| // instruction_start. |
| const int base_offset = code->instruction_size(); |
| // TODO(jgruber,v8:8758): Remove this translation. It exists only because |
| // InstructionStream objects contains real offsets but WasmCode expects an |
| // offset of 0 to mean 'empty'. |
| const int safepoint_table_offset = |
| code->has_safepoint_table() ? base_offset + code->safepoint_table_offset() |
| : 0; |
| const int handler_table_offset = base_offset + code->handler_table_offset(); |
| const int constant_pool_offset = base_offset + code->constant_pool_offset(); |
| const int code_comments_offset = base_offset + code->code_comments_offset(); |
| |
| base::RecursiveMutexGuard guard{&allocation_mutex_}; |
| base::Vector<uint8_t> dst_code_bytes = |
| code_allocator_.AllocateForCode(this, instructions.size()); |
| { |
| WritableJitAllocation jit_allocation = |
| ThreadIsolation::RegisterJitAllocation( |
| reinterpret_cast<Address>(dst_code_bytes.begin()), |
| dst_code_bytes.size(), |
| ThreadIsolation::JitAllocationType::kWasmCode); |
| jit_allocation.CopyCode(0, instructions.begin(), instructions.size()); |
| |
| // Apply the relocation delta by iterating over the RelocInfo. |
| intptr_t delta = reinterpret_cast<Address>(dst_code_bytes.begin()) - |
| code->instruction_start(); |
| int mode_mask = |
| RelocInfo::kApplyMask | RelocInfo::ModeMask(RelocInfo::WASM_STUB_CALL); |
| auto jump_tables_ref = |
| FindJumpTablesForRegionLocked(base::AddressRegionOf(dst_code_bytes)); |
| Address dst_code_addr = reinterpret_cast<Address>(dst_code_bytes.begin()); |
| Address constant_pool_start = dst_code_addr + constant_pool_offset; |
| RelocIterator orig_it(*code, mode_mask); |
| for (WritableRelocIterator it(jit_allocation, dst_code_bytes, |
| reloc_info.as_vector(), constant_pool_start, |
| mode_mask); |
| !it.done(); it.next(), orig_it.next()) { |
| RelocInfo::Mode mode = it.rinfo()->rmode(); |
| if (RelocInfo::IsWasmStubCall(mode)) { |
| uint32_t stub_call_tag = orig_it.rinfo()->wasm_call_tag(); |
| DCHECK_LT(stub_call_tag, |
| static_cast<uint32_t>(Builtin::kFirstBytecodeHandler)); |
| Builtin builtin = static_cast<Builtin>(stub_call_tag); |
| Address entry = GetJumpTableEntryForBuiltin(builtin, jump_tables_ref); |
| it.rinfo()->set_wasm_stub_call_address(entry, SKIP_ICACHE_FLUSH); |
| } else { |
| it.rinfo()->apply(delta); |
| } |
| } |
| } |
| |
| // Flush the i-cache after relocation. |
| FlushInstructionCache(dst_code_bytes.begin(), dst_code_bytes.size()); |
| |
| std::unique_ptr<WasmCode> new_code{ |
| new WasmCode{this, // native_module |
| kAnonymousFuncIndex, // index |
| dst_code_bytes, // instructions |
| stack_slots, // stack_slots |
| 0, // tagged_parameter_slots |
| safepoint_table_offset, // safepoint_table_offset |
| handler_table_offset, // handler_table_offset |
| constant_pool_offset, // constant_pool_offset |
| code_comments_offset, // code_comments_offset |
| instructions.length(), // unpadded_binary_size |
| {}, // protected_instructions |
| reloc_info.as_vector(), // reloc_info |
| source_pos.as_vector(), // source positions |
| {}, // inlining positions |
| {}, // deopt data |
| WasmCode::kWasmFunction, // kind |
| ExecutionTier::kNone, // tier |
| kNotForDebugging}}; // for_debugging |
| new_code->MaybePrint(); |
| new_code->Validate(); |
| |
| return PublishCodeLocked(std::move(new_code)); |
| } |
| |
| void NativeModule::InitializeJumpTableForLazyCompilation( |
| uint32_t num_wasm_functions) { |
| if (!num_wasm_functions) return; |
| allocation_mutex_.AssertHeld(); |
| |
| DCHECK_NULL(lazy_compile_table_); |
| lazy_compile_table_ = CreateEmptyJumpTableLocked( |
| JumpTableAssembler::SizeForNumberOfLazyFunctions(num_wasm_functions), |
| JumpTableType::kLazyCompileTable); |
| |
| CHECK_EQ(1, code_space_data_.size()); |
| const CodeSpaceData& code_space_data = code_space_data_[0]; |
| DCHECK_NOT_NULL(code_space_data.jump_table); |
| DCHECK_NOT_NULL(code_space_data.far_jump_table); |
| |
| Address compile_lazy_address = |
| code_space_data.far_jump_table->instruction_start() + |
| JumpTableAssembler::FarJumpSlotIndexToOffset( |
| BuiltinLookup::JumptableIndexForBuiltin(Builtin::kWasmCompileLazy)); |
| |
| JumpTableAssembler::GenerateLazyCompileTable( |
| lazy_compile_table_->instruction_start(), num_wasm_functions, |
| module_->num_imported_functions, compile_lazy_address); |
| |
| JumpTableAssembler::InitializeJumpsToLazyCompileTable( |
| code_space_data.jump_table->instruction_start(), num_wasm_functions, |
| lazy_compile_table_->instruction_start()); |
| } |
| |
| void NativeModule::UseLazyStubLocked(uint32_t func_index) { |
| allocation_mutex_.AssertHeld(); |
| DCHECK_LE(module_->num_imported_functions, func_index); |
| DCHECK_LT(func_index, |
| module_->num_imported_functions + module_->num_declared_functions); |
| // Avoid opening a new write scope per function. The caller should hold the |
| // scope instead. |
| |
| DCHECK_NOT_NULL(lazy_compile_table_); |
| |
| // Add jump table entry for jump to the lazy compile stub. |
| uint32_t slot_index = declared_function_index(module(), func_index); |
| DCHECK_NULL(code_table_[slot_index]); |
| Address lazy_compile_target = |
| lazy_compile_table_->instruction_start() + |
| JumpTableAssembler::LazyCompileSlotIndexToOffset(slot_index); |
| PatchJumpTablesLocked(slot_index, lazy_compile_target); |
| } |
| |
| std::unique_ptr<WasmCode> NativeModule::AddCode( |
| int index, const CodeDesc& desc, int stack_slots, |
| uint32_t tagged_parameter_slots, |
| base::Vector<const uint8_t> protected_instructions_data, |
| base::Vector<const uint8_t> source_position_table, |
| base::Vector<const uint8_t> inlining_positions, |
| base::Vector<const uint8_t> deopt_data, WasmCode::Kind kind, |
| ExecutionTier tier, ForDebugging for_debugging) { |
| base::Vector<uint8_t> code_space; |
| NativeModule::JumpTablesRef jump_table_ref; |
| { |
| base::RecursiveMutexGuard guard{&allocation_mutex_}; |
| code_space = code_allocator_.AllocateForCode(this, desc.instr_size); |
| jump_table_ref = |
| FindJumpTablesForRegionLocked(base::AddressRegionOf(code_space)); |
| } |
| // Only Liftoff code can have the {frame_has_feedback_slot} bit set. |
| DCHECK_NE(tier, ExecutionTier::kLiftoff); |
| bool frame_has_feedback_slot = false; |
| ThreadIsolation::RegisterJitAllocation( |
| reinterpret_cast<Address>(code_space.begin()), code_space.size(), |
| ThreadIsolation::JitAllocationType::kWasmCode); |
| return AddCodeWithCodeSpace( |
| index, desc, stack_slots, tagged_parameter_slots, |
| protected_instructions_data, source_position_table, inlining_positions, |
| deopt_data, kind, tier, for_debugging, frame_has_feedback_slot, |
| code_space, jump_table_ref); |
| } |
| |
| std::unique_ptr<WasmCode> NativeModule::AddCodeWithCodeSpace( |
| int index, const CodeDesc& desc, int stack_slots, |
| uint32_t tagged_parameter_slots, |
| base::Vector<const uint8_t> protected_instructions_data, |
| base::Vector<const uint8_t> source_position_table, |
| base::Vector<const uint8_t> inlining_positions, |
| base::Vector<const uint8_t> deopt_data, WasmCode::Kind kind, |
| ExecutionTier tier, ForDebugging for_debugging, |
| bool frame_has_feedback_slot, base::Vector<uint8_t> dst_code_bytes, |
| const JumpTablesRef& jump_tables) { |
| base::Vector<uint8_t> reloc_info{ |
| desc.buffer + desc.buffer_size - desc.reloc_size, |
| static_cast<size_t>(desc.reloc_size)}; |
| UpdateCodeSize(desc.instr_size, tier, for_debugging); |
| |
| // TODO(jgruber,v8:8758): Remove this translation. It exists only because |
| // CodeDesc contains real offsets but WasmCode expects an offset of 0 to mean |
| // 'empty'. |
| const int safepoint_table_offset = |
| desc.safepoint_table_size == 0 ? 0 : desc.safepoint_table_offset; |
| const int handler_table_offset = desc.handler_table_offset; |
| const int constant_pool_offset = desc.constant_pool_offset; |
| const int code_comments_offset = desc.code_comments_offset; |
| const int instr_size = desc.instr_size; |
| |
| { |
| WritableJitAllocation jit_allocation = ThreadIsolation::LookupJitAllocation( |
| reinterpret_cast<Address>(dst_code_bytes.begin()), |
| dst_code_bytes.size(), ThreadIsolation::JitAllocationType::kWasmCode); |
| jit_allocation.CopyCode(0, desc.buffer, desc.instr_size); |
| |
| // Apply the relocation delta by iterating over the RelocInfo. |
| intptr_t delta = dst_code_bytes.begin() - desc.buffer; |
| int mode_mask = RelocInfo::kApplyMask | |
| RelocInfo::ModeMask(RelocInfo::WASM_CALL) | |
| RelocInfo::ModeMask(RelocInfo::WASM_STUB_CALL); |
| Address code_start = reinterpret_cast<Address>(dst_code_bytes.begin()); |
| Address constant_pool_start = code_start + constant_pool_offset; |
| |
| for (WritableRelocIterator it(jit_allocation, dst_code_bytes, reloc_info, |
| constant_pool_start, mode_mask); |
| !it.done(); it.next()) { |
| RelocInfo::Mode mode = it.rinfo()->rmode(); |
| if (RelocInfo::IsWasmCall(mode)) { |
| uint32_t call_tag = it.rinfo()->wasm_call_tag(); |
| Address target = GetNearCallTargetForFunction(call_tag, jump_tables); |
| it.rinfo()->set_wasm_call_address(target, SKIP_ICACHE_FLUSH); |
| } else if (RelocInfo::IsWasmStubCall(mode)) { |
| uint32_t stub_call_tag = it.rinfo()->wasm_call_tag(); |
| DCHECK_LT(stub_call_tag, |
| static_cast<uint32_t>(Builtin::kFirstBytecodeHandler)); |
| Builtin builtin = static_cast<Builtin>(stub_call_tag); |
| Address entry = GetJumpTableEntryForBuiltin(builtin, jump_tables); |
| it.rinfo()->set_wasm_stub_call_address(entry, SKIP_ICACHE_FLUSH); |
| } else { |
| it.rinfo()->apply(delta); |
| } |
| } |
| } |
| |
| // Flush the i-cache after relocation. |
| FlushInstructionCache(dst_code_bytes.begin(), dst_code_bytes.size()); |
| |
| // Liftoff code will not be relocated or serialized, thus do not store any |
| // relocation information. |
| if (tier == ExecutionTier::kLiftoff) reloc_info = {}; |
| |
| std::unique_ptr<WasmCode> code{new WasmCode{ |
| this, index, dst_code_bytes, stack_slots, tagged_parameter_slots, |
| safepoint_table_offset, handler_table_offset, constant_pool_offset, |
| code_comments_offset, instr_size, protected_instructions_data, reloc_info, |
| source_position_table, inlining_positions, deopt_data, kind, tier, |
| for_debugging, frame_has_feedback_slot}}; |
| |
| code->MaybePrint(); |
| code->Validate(); |
| |
| return code; |
| } |
| |
| WasmCode* NativeModule::PublishCode(std::unique_ptr<WasmCode> code, |
| AssumptionsJournal* assumptions) { |
| TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.wasm.detailed"), |
| "wasm.PublishCode"); |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| if (assumptions != nullptr) { |
| // Acquiring the lock is expensive, so callers should only pass non-empty |
| // assumptions journals. |
| DCHECK(!assumptions->empty()); |
| // Only Turbofan makes assumptions. |
| DCHECK_EQ(ExecutionTier::kTurbofan, code->tier()); |
| WellKnownImportsList& current = module_->type_feedback.well_known_imports; |
| base::MutexGuard wki_lock(current.mutex()); |
| for (auto [import_index, status] : assumptions->import_statuses()) { |
| if (current.get(import_index) != status) { |
| compilation_state_->AllowAnotherTopTierJob(code->index()); |
| return nullptr; |
| } |
| } |
| } |
| return PublishCodeLocked(std::move(code)); |
| } |
| |
| std::vector<WasmCode*> NativeModule::PublishCode( |
| base::Vector<std::unique_ptr<WasmCode>> codes) { |
| TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("v8.wasm.detailed"), |
| "wasm.PublishCode", "number", codes.size()); |
| std::vector<WasmCode*> published_code; |
| published_code.reserve(codes.size()); |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| // The published code is put into the top-most surrounding {WasmCodeRefScope}. |
| for (auto& code : codes) { |
| published_code.push_back(PublishCodeLocked(std::move(code))); |
| } |
| return published_code; |
| } |
| |
| WasmCode::Kind GetCodeKind(const WasmCompilationResult& result) { |
| switch (result.kind) { |
| case WasmCompilationResult::kWasmToJsWrapper: |
| return WasmCode::Kind::kWasmToJsWrapper; |
| case WasmCompilationResult::kFunction: |
| return WasmCode::Kind::kWasmFunction; |
| default: |
| UNREACHABLE(); |
| } |
| } |
| |
| WasmCode* NativeModule::PublishCodeLocked( |
| std::unique_ptr<WasmCode> owned_code) { |
| allocation_mutex_.AssertHeld(); |
| |
| WasmCode* code = owned_code.get(); |
| new_owned_code_.emplace_back(std::move(owned_code)); |
| |
| // Add the code to the surrounding code ref scope, so the returned pointer is |
| // guaranteed to be valid. |
| WasmCodeRefScope::AddRef(code); |
| |
| if (code->index() < static_cast<int>(module_->num_imported_functions)) { |
| return code; |
| } |
| |
| DCHECK_LT(code->index(), num_functions()); |
| |
| code->RegisterTrapHandlerData(); |
| |
| // Put the code in the debugging cache, if needed. |
| if (V8_UNLIKELY(cached_code_)) InsertToCodeCache(code); |
| |
| // Assume an order of execution tiers that represents the quality of their |
| // generated code. |
| static_assert(ExecutionTier::kNone < ExecutionTier::kLiftoff && |
| ExecutionTier::kLiftoff < ExecutionTier::kTurbofan, |
| "Assume an order on execution tiers"); |
| |
| uint32_t slot_idx = declared_function_index(module(), code->index()); |
| WasmCode* prior_code = code_table_[slot_idx]; |
| // If we are tiered down, install all debugging code (except for stepping |
| // code, which is only used for a single frame and never installed in the |
| // code table of jump table). Otherwise, install code if it was compiled |
| // with a higher tier. |
| static_assert( |
| kForDebugging > kNotForDebugging && kWithBreakpoints > kForDebugging, |
| "for_debugging is ordered"); |
| |
| if (should_update_code_table(code, prior_code)) { |
| code_table_[slot_idx] = code; |
| if (prior_code) { |
| WasmCodeRefScope::AddRef(prior_code); |
| // The code is added to the current {WasmCodeRefScope}, hence the ref |
| // count cannot drop to zero here. |
| prior_code->DecRefOnLiveCode(); |
| } |
| |
| PatchJumpTablesLocked(slot_idx, code->instruction_start()); |
| } else { |
| // The code tables does not hold a reference to the code, hence decrement |
| // the initial ref count of 1. The code was added to the |
| // {WasmCodeRefScope} though, so it cannot die here. |
| code->DecRefOnLiveCode(); |
| } |
| |
| return code; |
| } |
| |
| bool NativeModule::should_update_code_table(WasmCode* new_code, |
| WasmCode* prior_code) const { |
| if (new_code->for_debugging() == kForStepping) { |
| // Never install stepping code. |
| return false; |
| } |
| if (debug_state_ == kDebugging) { |
| if (new_code->for_debugging() == kNotForDebugging) { |
| // In debug state, only install debug code. |
| return false; |
| } |
| if (prior_code && prior_code->for_debugging() > new_code->for_debugging()) { |
| // In debug state, install breakpoints over normal debug code. |
| return false; |
| } |
| } |
| // In kNoDebugging: |
| // Install if the tier is higher than before or we replace debugging code with |
| // non-debugging code. |
| if (prior_code && !prior_code->for_debugging() && |
| prior_code->tier() > new_code->tier()) { |
| return false; |
| } |
| return true; |
| } |
| |
| void NativeModule::ReinstallDebugCode(WasmCode* code) { |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| |
| DCHECK_EQ(this, code->native_module()); |
| DCHECK_EQ(kWithBreakpoints, code->for_debugging()); |
| DCHECK(!code->IsAnonymous()); |
| DCHECK_LE(module_->num_imported_functions, code->index()); |
| DCHECK_LT(code->index(), num_functions()); |
| |
| // If the module is tiered up by now, do not reinstall debug code. |
| if (debug_state_ != kDebugging) return; |
| |
| uint32_t slot_idx = declared_function_index(module(), code->index()); |
| if (WasmCode* prior_code = code_table_[slot_idx]) { |
| WasmCodeRefScope::AddRef(prior_code); |
| // The code is added to the current {WasmCodeRefScope}, hence the ref |
| // count cannot drop to zero here. |
| prior_code->DecRefOnLiveCode(); |
| } |
| code_table_[slot_idx] = code; |
| code->IncRef(); |
| |
| PatchJumpTablesLocked(slot_idx, code->instruction_start()); |
| } |
| |
| std::pair<base::Vector<uint8_t>, NativeModule::JumpTablesRef> |
| NativeModule::AllocateForDeserializedCode(size_t total_code_size) { |
| base::RecursiveMutexGuard guard{&allocation_mutex_}; |
| base::Vector<uint8_t> code_space = |
| code_allocator_.AllocateForCode(this, total_code_size); |
| auto jump_tables = |
| FindJumpTablesForRegionLocked(base::AddressRegionOf(code_space)); |
| return {code_space, jump_tables}; |
| } |
| |
| std::unique_ptr<WasmCode> NativeModule::AddDeserializedCode( |
| int index, base::Vector<uint8_t> instructions, int stack_slots, |
| uint32_t tagged_parameter_slots, int safepoint_table_offset, |
| int handler_table_offset, int constant_pool_offset, |
| int code_comments_offset, int unpadded_binary_size, |
| base::Vector<const uint8_t> protected_instructions_data, |
| base::Vector<const uint8_t> reloc_info, |
| base::Vector<const uint8_t> source_position_table, |
| base::Vector<const uint8_t> inlining_positions, |
| base::Vector<const uint8_t> deopt_data, WasmCode::Kind kind, |
| ExecutionTier tier) { |
| UpdateCodeSize(instructions.size(), tier, kNotForDebugging); |
| |
| return std::unique_ptr<WasmCode>{new WasmCode{ |
| this, index, instructions, stack_slots, tagged_parameter_slots, |
| safepoint_table_offset, handler_table_offset, constant_pool_offset, |
| code_comments_offset, unpadded_binary_size, protected_instructions_data, |
| reloc_info, source_position_table, inlining_positions, deopt_data, kind, |
| tier, kNotForDebugging}}; |
| } |
| |
| std::pair<std::vector<WasmCode*>, std::vector<WellKnownImport>> |
| NativeModule::SnapshotCodeTable() const { |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| WasmCode** start = code_table_.get(); |
| WasmCode** end = start + module_->num_declared_functions; |
| for (WasmCode* code : base::VectorOf(start, end - start)) { |
| if (code) WasmCodeRefScope::AddRef(code); |
| } |
| std::vector<WellKnownImport> import_statuses(module_->num_imported_functions); |
| for (uint32_t i = 0; i < module_->num_imported_functions; i++) { |
| import_statuses[i] = module_->type_feedback.well_known_imports.get(i); |
| } |
| return {std::vector<WasmCode*>{start, end}, std::move(import_statuses)}; |
| } |
| |
| std::vector<WasmCode*> NativeModule::SnapshotAllOwnedCode() const { |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| if (!new_owned_code_.empty()) TransferNewOwnedCodeLocked(); |
| |
| std::vector<WasmCode*> all_code(owned_code_.size()); |
| std::transform(owned_code_.begin(), owned_code_.end(), all_code.begin(), |
| [](auto& entry) { return entry.second.get(); }); |
| std::for_each(all_code.begin(), all_code.end(), WasmCodeRefScope::AddRef); |
| return all_code; |
| } |
| |
| WasmCode* NativeModule::GetCode(uint32_t index) const { |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| WasmCode* code = code_table_[declared_function_index(module(), index)]; |
| if (code) WasmCodeRefScope::AddRef(code); |
| return code; |
| } |
| |
| bool NativeModule::HasCode(uint32_t index) const { |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| return code_table_[declared_function_index(module(), index)] != nullptr; |
| } |
| |
| bool NativeModule::HasCodeWithTier(uint32_t index, ExecutionTier tier) const { |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| return code_table_[declared_function_index(module(), index)] != nullptr && |
| code_table_[declared_function_index(module(), index)]->tier() == tier; |
| } |
| |
| void NativeModule::SetWasmSourceMap( |
| std::unique_ptr<WasmModuleSourceMap> source_map) { |
| source_map_ = std::move(source_map); |
| } |
| |
| WasmModuleSourceMap* NativeModule::GetWasmSourceMap() const { |
| return source_map_.get(); |
| } |
| |
| WasmCode* NativeModule::CreateEmptyJumpTableLocked(int jump_table_size, |
| JumpTableType type) { |
| return CreateEmptyJumpTableInRegionLocked(jump_table_size, |
| kUnrestrictedRegion, type); |
| } |
| |
| namespace { |
| |
| ThreadIsolation::JitAllocationType ToAllocationType( |
| v8::internal::wasm::NativeModule::JumpTableType type) { |
| switch (type) { |
| case NativeModule::JumpTableType::kJumpTable: |
| return ThreadIsolation::JitAllocationType::kWasmJumpTable; |
| case NativeModule::JumpTableType::kFarJumpTable: |
| return ThreadIsolation::JitAllocationType::kWasmFarJumpTable; |
| case NativeModule::JumpTableType::kLazyCompileTable: |
| return ThreadIsolation::JitAllocationType::kWasmLazyCompileTable; |
| } |
| } |
| |
| } // namespace |
| |
| WasmCode* NativeModule::CreateEmptyJumpTableInRegionLocked( |
| int jump_table_size, base::AddressRegion region, JumpTableType type) { |
| allocation_mutex_.AssertHeld(); |
| // Only call this if we really need a jump table. |
| DCHECK_LT(0, jump_table_size); |
| base::Vector<uint8_t> code_space = |
| code_allocator_.AllocateForCodeInRegion(this, jump_table_size, region); |
| DCHECK(!code_space.empty()); |
| UpdateCodeSize(jump_table_size, ExecutionTier::kNone, kNotForDebugging); |
| { |
| WritableJitAllocation jit_allocation = |
| ThreadIsolation::RegisterJitAllocation( |
| reinterpret_cast<Address>(code_space.begin()), code_space.size(), |
| ToAllocationType(type)); |
| jit_allocation.ClearBytes(0, code_space.size()); |
| } |
| std::unique_ptr<WasmCode> code{ |
| new WasmCode{this, // native_module |
| kAnonymousFuncIndex, // index |
| code_space, // instructions |
| 0, // stack_slots |
| 0, // tagged_parameter_slots |
| 0, // safepoint_table_offset |
| jump_table_size, // handler_table_offset |
| jump_table_size, // constant_pool_offset |
| jump_table_size, // code_comments_offset |
| jump_table_size, // unpadded_binary_size |
| {}, // protected_instructions |
| {}, // reloc_info |
| {}, // source_pos |
| {}, // inlining pos |
| {}, // deopt data |
| WasmCode::kJumpTable, // kind |
| ExecutionTier::kNone, // tier |
| kNotForDebugging}}; // for_debugging |
| return PublishCodeLocked(std::move(code)); |
| } |
| |
| void NativeModule::UpdateCodeSize(size_t size, ExecutionTier tier, |
| ForDebugging for_debugging) { |
| if (for_debugging != kNotForDebugging) return; |
| // Count jump tables (ExecutionTier::kNone) for both Liftoff and TurboFan as |
| // this is shared code. |
| if (tier != ExecutionTier::kTurbofan) liftoff_code_size_.fetch_add(size); |
| if (tier != ExecutionTier::kLiftoff) turbofan_code_size_.fetch_add(size); |
| } |
| |
| void NativeModule::PatchJumpTablesLocked(uint32_t slot_index, Address target) { |
| allocation_mutex_.AssertHeld(); |
| |
| for (auto& code_space_data : code_space_data_) { |
| // TODO(sroettger): need to unlock both jump tables together |
| DCHECK_IMPLIES(code_space_data.jump_table, code_space_data.far_jump_table); |
| if (!code_space_data.jump_table) continue; |
| WritableJumpTablePair writable_jump_tables = |
| ThreadIsolation::LookupJumpTableAllocations( |
| code_space_data.jump_table->instruction_start(), |
| code_space_data.jump_table->instructions_size_, |
| code_space_data.far_jump_table->instruction_start(), |
| code_space_data.far_jump_table->instructions_size_); |
| PatchJumpTableLocked(code_space_data, slot_index, target); |
| } |
| } |
| |
| void NativeModule::PatchJumpTableLocked(const CodeSpaceData& code_space_data, |
| uint32_t slot_index, Address target) { |
| allocation_mutex_.AssertHeld(); |
| |
| DCHECK_NOT_NULL(code_space_data.jump_table); |
| DCHECK_NOT_NULL(code_space_data.far_jump_table); |
| |
| DCHECK_LT(slot_index, module_->num_declared_functions); |
| Address jump_table_slot = |
| code_space_data.jump_table->instruction_start() + |
| JumpTableAssembler::JumpSlotIndexToOffset(slot_index); |
| uint32_t far_jump_table_offset = JumpTableAssembler::FarJumpSlotIndexToOffset( |
| BuiltinLookup::BuiltinCount() + slot_index); |
| // Only pass the far jump table start if the far jump table actually has a |
| // slot for this function index (i.e. does not only contain runtime stubs). |
| bool has_far_jump_slot = |
| far_jump_table_offset < |
| code_space_data.far_jump_table->instructions().size(); |
| Address far_jump_table_start = |
| code_space_data.far_jump_table->instruction_start(); |
| Address far_jump_table_slot = |
| has_far_jump_slot ? far_jump_table_start + far_jump_table_offset |
| : kNullAddress; |
| JumpTableAssembler::PatchJumpTableSlot(jump_table_slot, far_jump_table_slot, |
| target); |
| } |
| |
| void NativeModule::AddCodeSpaceLocked(base::AddressRegion region) { |
| allocation_mutex_.AssertHeld(); |
| |
| // Each code space must be at least twice as large as the overhead per code |
| // space. Otherwise, we are wasting too much memory. |
| DCHECK_GE(region.size(), |
| 2 * OverheadPerCodeSpace(module()->num_declared_functions)); |
| |
| #if defined(V8_OS_WIN64) |
| // On some platforms, specifically Win64, we need to reserve some pages at |
| // the beginning of an executable space. |
| // See src/heap/spaces.cc, MemoryAllocator::InitializeCodePageAllocator() and |
| // https://cs.chromium.org/chromium/src/components/crash/content/app/crashpad_win.cc?rcl=fd680447881449fba2edcf0589320e7253719212&l=204 |
| // for details. |
| if (WasmCodeManager::CanRegisterUnwindInfoForNonABICompliantCodeRange()) { |
| size_t size = Heap::GetCodeRangeReservedAreaSize(); |
| DCHECK_LT(0, size); |
| base::Vector<uint8_t> padding = |
| code_allocator_.AllocateForCodeInRegion(this, size, region); |
| CHECK_EQ(reinterpret_cast<Address>(padding.begin()), region.begin()); |
| win64_unwindinfo::RegisterNonABICompliantCodeRange( |
| reinterpret_cast<void*>(region.begin()), region.size()); |
| } |
| #endif // V8_OS_WIN64 |
| |
| WasmCodeRefScope code_ref_scope; |
| WasmCode* jump_table = nullptr; |
| WasmCode* far_jump_table = nullptr; |
| const uint32_t num_wasm_functions = module_->num_declared_functions; |
| const bool is_first_code_space = code_space_data_.empty(); |
| // We always need a far jump table, because it contains the runtime stubs. |
| const bool needs_far_jump_table = |
| !FindJumpTablesForRegionLocked(region).is_valid(); |
| const bool needs_jump_table = num_wasm_functions > 0 && needs_far_jump_table; |
| |
| if (needs_jump_table) { |
| // Allocate additional jump tables just as big as the first one. |
| // This is in particular needed in cctests which add functions to the module |
| // after the jump tables are already created (see https://crbug.com/v8/14213 |
| // and {NativeModule::ReserveCodeTableForTesting}. |
| int jump_table_size = |
| is_first_code_space |
| ? JumpTableAssembler::SizeForNumberOfSlots(num_wasm_functions) |
| : main_jump_table_->instructions_size_; |
| jump_table = CreateEmptyJumpTableInRegionLocked(jump_table_size, region, |
| JumpTableType::kJumpTable); |
| CHECK(region.contains(jump_table->instruction_start())); |
| } |
| |
| if (needs_far_jump_table) { |
| int num_function_slots = NumWasmFunctionsInFarJumpTable(num_wasm_functions); |
| // See comment above for the size computation. |
| int far_jump_table_size = |
| is_first_code_space |
| ? JumpTableAssembler::SizeForNumberOfFarJumpSlots( |
| BuiltinLookup::BuiltinCount(), num_function_slots) |
| : main_far_jump_table_->instructions_size_; |
| far_jump_table = CreateEmptyJumpTableInRegionLocked( |
| far_jump_table_size, region, JumpTableType::kFarJumpTable); |
| CHECK(region.contains(far_jump_table->instruction_start())); |
| EmbeddedData embedded_data = EmbeddedData::FromBlob(); |
| static_assert(Builtins::kAllBuiltinsAreIsolateIndependent); |
| Address builtin_addresses[BuiltinLookup::BuiltinCount()]; |
| for (int i = 0; i < BuiltinLookup::BuiltinCount(); ++i) { |
| builtin_addresses[i] = embedded_data.InstructionStartOf( |
| BuiltinLookup::BuiltinForJumptableIndex(i)); |
| } |
| WritableJitAllocation jit_allocation = ThreadIsolation::LookupJitAllocation( |
| far_jump_table->instruction_start(), far_jump_table->instructions_size_, |
| ThreadIsolation::JitAllocationType::kWasmFarJumpTable); |
| JumpTableAssembler::GenerateFarJumpTable( |
| far_jump_table->instruction_start(), builtin_addresses, |
| BuiltinLookup::BuiltinCount(), num_function_slots); |
| } |
| |
| if (is_first_code_space) { |
| // This can be updated and accessed without locks, since the addition of the |
| // first code space happens during initialization of the {NativeModule}, |
| // where no concurrent accesses are possible. |
| main_jump_table_ = jump_table; |
| main_far_jump_table_ = far_jump_table; |
| } |
| |
| code_space_data_.push_back(CodeSpaceData{region, jump_table, far_jump_table}); |
| |
| if (is_first_code_space) { |
| InitializeJumpTableForLazyCompilation(num_wasm_functions); |
| } |
| |
| if (jump_table && !is_first_code_space) { |
| // Patch the new jump table(s) with existing functions. If this is the first |
| // code space, there cannot be any functions that have been compiled yet. |
| const CodeSpaceData& new_code_space_data = code_space_data_.back(); |
| // TODO(sroettger): need to create two write scopes? Or have a write scope |
| // for multiple allocations. |
| WritableJumpTablePair writable_jump_tables = |
| ThreadIsolation::LookupJumpTableAllocations( |
| new_code_space_data.jump_table->instruction_start(), |
| |
| new_code_space_data.jump_table->instructions_size_, |
| new_code_space_data.far_jump_table->instruction_start(), |
| |
| new_code_space_data.far_jump_table->instructions_size_); |
| for (uint32_t slot_index = 0; slot_index < num_wasm_functions; |
| ++slot_index) { |
| if (code_table_[slot_index]) { |
| PatchJumpTableLocked(new_code_space_data, slot_index, |
| code_table_[slot_index]->instruction_start()); |
| } else if (lazy_compile_table_) { |
| Address lazy_compile_target = |
| lazy_compile_table_->instruction_start() + |
| JumpTableAssembler::LazyCompileSlotIndexToOffset(slot_index); |
| PatchJumpTableLocked(new_code_space_data, slot_index, |
| lazy_compile_target); |
| } |
| } |
| } |
| } |
| |
| namespace { |
| class NativeModuleWireBytesStorage final : public WireBytesStorage { |
| public: |
| explicit NativeModuleWireBytesStorage( |
| std::shared_ptr<base::OwnedVector<const uint8_t>> wire_bytes) |
| : wire_bytes_(std::move(wire_bytes)) {} |
| |
| base::Vector<const uint8_t> GetCode(WireBytesRef ref) const final { |
| return std::atomic_load(&wire_bytes_) |
| ->as_vector() |
| .SubVector(ref.offset(), ref.end_offset()); |
| } |
| |
| base::Optional<ModuleWireBytes> GetModuleBytes() const final { |
| return base::Optional<ModuleWireBytes>( |
| std::atomic_load(&wire_bytes_)->as_vector()); |
| } |
| |
| private: |
| const std::shared_ptr<base::OwnedVector<const uint8_t>> wire_bytes_; |
| }; |
| } // namespace |
| |
| void NativeModule::SetWireBytes(base::OwnedVector<const uint8_t> wire_bytes) { |
| auto shared_wire_bytes = |
| std::make_shared<base::OwnedVector<const uint8_t>>(std::move(wire_bytes)); |
| std::atomic_store(&wire_bytes_, shared_wire_bytes); |
| if (!shared_wire_bytes->empty()) { |
| compilation_state_->SetWireBytesStorage( |
| std::make_shared<NativeModuleWireBytesStorage>( |
| std::move(shared_wire_bytes))); |
| } |
| } |
| |
| void NativeModule::AddLazyCompilationTimeSample(int64_t sample_in_micro_sec) { |
| num_lazy_compilations_.fetch_add(1, std::memory_order_relaxed); |
| sum_lazy_compilation_time_in_micro_sec_.fetch_add(sample_in_micro_sec, |
| std::memory_order_relaxed); |
| int64_t max = |
| max_lazy_compilation_time_in_micro_sec_.load(std::memory_order_relaxed); |
| while (sample_in_micro_sec > max && |
| !max_lazy_compilation_time_in_micro_sec_.compare_exchange_weak( |
| max, sample_in_micro_sec, std::memory_order_relaxed, |
| std::memory_order_relaxed)) { |
| // Repeat until we set the new maximum sucessfully. |
| } |
| } |
| |
| void NativeModule::TransferNewOwnedCodeLocked() const { |
| allocation_mutex_.AssertHeld(); |
| DCHECK(!new_owned_code_.empty()); |
| // Sort the {new_owned_code_} vector reversed, such that the position of the |
| // previously inserted element can be used as a hint for the next element. If |
| // elements in {new_owned_code_} are adjacent, this will guarantee |
| // constant-time insertion into the map. |
| std::sort(new_owned_code_.begin(), new_owned_code_.end(), |
| [](const std::unique_ptr<WasmCode>& a, |
| const std::unique_ptr<WasmCode>& b) { |
| return a->instruction_start() > b->instruction_start(); |
| }); |
| auto insertion_hint = owned_code_.end(); |
| for (auto& code : new_owned_code_) { |
| DCHECK_EQ(0, owned_code_.count(code->instruction_start())); |
| // Check plausibility of the insertion hint. |
| DCHECK(insertion_hint == owned_code_.end() || |
| insertion_hint->first > code->instruction_start()); |
| insertion_hint = owned_code_.emplace_hint( |
| insertion_hint, code->instruction_start(), std::move(code)); |
| } |
| new_owned_code_.clear(); |
| } |
| |
| void NativeModule::InsertToCodeCache(WasmCode* code) { |
| allocation_mutex_.AssertHeld(); |
| DCHECK_NOT_NULL(cached_code_); |
| if (code->IsAnonymous()) return; |
| // Only cache Liftoff debugging code or TurboFan code (no breakpoints or |
| // stepping). |
| if (code->tier() == ExecutionTier::kLiftoff && |
| code->for_debugging() != kForDebugging) { |
| return; |
| } |
| auto key = std::make_pair(code->tier(), code->index()); |
| if (cached_code_->insert(std::make_pair(key, code)).second) { |
| code->IncRef(); |
| } |
| } |
| |
| WasmCode* NativeModule::Lookup(Address pc) const { |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| if (!new_owned_code_.empty()) TransferNewOwnedCodeLocked(); |
| auto iter = owned_code_.upper_bound(pc); |
| if (iter == owned_code_.begin()) return nullptr; |
| --iter; |
| WasmCode* candidate = iter->second.get(); |
| DCHECK_EQ(candidate->instruction_start(), iter->first); |
| if (!candidate->contains(pc)) return nullptr; |
| WasmCodeRefScope::AddRef(candidate); |
| return candidate; |
| } |
| |
| NativeModule::JumpTablesRef NativeModule::FindJumpTablesForRegionLocked( |
| base::AddressRegion code_region) const { |
| allocation_mutex_.AssertHeld(); |
| auto jump_table_usable = [code_region](const WasmCode* jump_table) { |
| // We only ever need to check for suitable jump tables if |
| // {kNeedsFarJumpsBetweenCodeSpaces} is true. |
| if constexpr (!kNeedsFarJumpsBetweenCodeSpaces) UNREACHABLE(); |
| Address table_start = jump_table->instruction_start(); |
| Address table_end = table_start + jump_table->instructions().size(); |
| // Compute the maximum distance from anywhere in the code region to anywhere |
| // in the jump table, avoiding any underflow. |
| size_t max_distance = std::max( |
| code_region.end() > table_start ? code_region.end() - table_start : 0, |
| table_end > code_region.begin() ? table_end - code_region.begin() : 0); |
| // kDefaultMaxWasmCodeSpaceSizeMb is <= the maximum near call distance on |
| // the current platform. |
| // We can allow a max_distance that is equal to |
| // kDefaultMaxWasmCodeSpaceSizeMb, because every call or jump will target an |
| // address *within* the region, but never exactly the end of the region. So |
| // all occuring offsets are actually smaller than max_distance. |
| return max_distance <= kDefaultMaxWasmCodeSpaceSizeMb * MB; |
| }; |
| |
| for (auto& code_space_data : code_space_data_) { |
| DCHECK_IMPLIES(code_space_data.jump_table, code_space_data.far_jump_table); |
| if (!code_space_data.far_jump_table) continue; |
| // Only return these jump tables if they are reachable from the whole |
| // {code_region}. |
| if (kNeedsFarJumpsBetweenCodeSpaces && |
| (!jump_table_usable(code_space_data.far_jump_table) || |
| (code_space_data.jump_table && |
| !jump_table_usable(code_space_data.jump_table)))) { |
| continue; |
| } |
| return {code_space_data.jump_table |
| ? code_space_data.jump_table->instruction_start() |
| : kNullAddress, |
| code_space_data.far_jump_table->instruction_start()}; |
| } |
| return {}; |
| } |
| |
| Address NativeModule::GetNearCallTargetForFunction( |
| uint32_t func_index, const JumpTablesRef& jump_tables) const { |
| DCHECK(jump_tables.is_valid()); |
| uint32_t slot_offset = JumpTableOffset(module(), func_index); |
| return jump_tables.jump_table_start + slot_offset; |
| } |
| |
| Address NativeModule::GetJumpTableEntryForBuiltin( |
| Builtin builtin, const JumpTablesRef& jump_tables) const { |
| DCHECK(jump_tables.is_valid()); |
| int index = BuiltinLookup::JumptableIndexForBuiltin(builtin); |
| |
| auto offset = JumpTableAssembler::FarJumpSlotIndexToOffset(index); |
| return jump_tables.far_jump_table_start + offset; |
| } |
| |
| uint32_t NativeModule::GetFunctionIndexFromJumpTableSlot( |
| Address slot_address) const { |
| WasmCodeRefScope code_refs; |
| WasmCode* code = Lookup(slot_address); |
| DCHECK_NOT_NULL(code); |
| DCHECK_EQ(WasmCode::kJumpTable, code->kind()); |
| uint32_t slot_offset = |
| static_cast<uint32_t>(slot_address - code->instruction_start()); |
| uint32_t slot_idx = JumpTableAssembler::SlotOffsetToIndex(slot_offset); |
| DCHECK_LT(slot_idx, module_->num_declared_functions); |
| DCHECK_EQ(slot_address, |
| code->instruction_start() + |
| JumpTableAssembler::JumpSlotIndexToOffset(slot_idx)); |
| return module_->num_imported_functions + slot_idx; |
| } |
| |
| Builtin NativeModule::GetBuiltinInJumptableSlot(Address target) const { |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| |
| for (auto& code_space_data : code_space_data_) { |
| if (code_space_data.far_jump_table != nullptr && |
| code_space_data.far_jump_table->contains(target)) { |
| uint32_t offset = static_cast<uint32_t>( |
| target - code_space_data.far_jump_table->instruction_start()); |
| uint32_t index = JumpTableAssembler::FarJumpSlotOffsetToIndex(offset); |
| if (index >= BuiltinLookup::BuiltinCount()) continue; |
| if (JumpTableAssembler::FarJumpSlotIndexToOffset(index) != offset) { |
| continue; |
| } |
| return BuiltinLookup::BuiltinForJumptableIndex(index); |
| } |
| } |
| |
| // Invalid address. |
| return Builtin::kNoBuiltinId; |
| } |
| |
| NativeModule::~NativeModule() { |
| TRACE_HEAP("Deleting native module: %p\n", this); |
| // Cancel all background compilation before resetting any field of the |
| // NativeModule or freeing anything. |
| compilation_state_->CancelCompilation(); |
| |
| // Clear the import wrapper cache before releasing the {WasmCode} objects in |
| // {owned_code_}. The {WasmImportWrapperCache} still needs to decrement |
| // reference counts on the {WasmCode} objects. |
| import_wrapper_cache_.clear(); |
| |
| GetWasmEngine()->FreeNativeModule(this); |
| |
| // If experimental PGO support is enabled, serialize the PGO data now. |
| if (V8_UNLIKELY(v8_flags.experimental_wasm_pgo_to_file)) { |
| DumpProfileToFile(module_.get(), wire_bytes(), tiering_budgets_.get()); |
| } |
| } |
| |
| WasmCodeManager::WasmCodeManager() |
| : max_committed_code_space_(v8_flags.wasm_max_committed_code_mb * MB), |
| critical_committed_code_space_(max_committed_code_space_ / 2), |
| next_code_space_hint_(reinterpret_cast<Address>( |
| GetPlatformPageAllocator()->GetRandomMmapAddr())) { |
| // Check that --wasm-max-code-space-size-mb is not set bigger than the default |
| // value. Otherwise we run into DCHECKs or other crashes later. |
| CHECK_GE(kDefaultMaxWasmCodeSpaceSizeMb, |
| v8_flags.wasm_max_code_space_size_mb); |
| } |
| |
| WasmCodeManager::~WasmCodeManager() { |
| // No more committed code space. |
| DCHECK_EQ(0, total_committed_code_space_.load()); |
| } |
| |
| #if defined(V8_OS_WIN64) |
| // static |
| bool WasmCodeManager::CanRegisterUnwindInfoForNonABICompliantCodeRange() { |
| return win64_unwindinfo::CanRegisterUnwindInfoForNonABICompliantCodeRange() && |
| v8_flags.win64_unwinding_info; |
| } |
| #endif // V8_OS_WIN64 |
| |
| void WasmCodeManager::Commit(base::AddressRegion region) { |
| DCHECK(IsAligned(region.begin(), CommitPageSize())); |
| DCHECK(IsAligned(region.size(), CommitPageSize())); |
| // Reserve the size. Use CAS loop to avoid overflow on |
| // {total_committed_code_space_}. |
| size_t old_value = total_committed_code_space_.load(); |
| while (true) { |
| DCHECK_GE(max_committed_code_space_, old_value); |
| if (region.size() > max_committed_code_space_ - old_value) { |
| auto oom_detail = base::FormattedString{} |
| << "trying to commit " << region.size() |
| << ", already committed " << old_value; |
| V8::FatalProcessOutOfMemory(nullptr, |
| "Exceeding maximum wasm committed code space", |
| oom_detail.PrintToArray().data()); |
| UNREACHABLE(); |
| } |
| if (total_committed_code_space_.compare_exchange_weak( |
| old_value, old_value + region.size())) { |
| break; |
| } |
| } |
| |
| TRACE_HEAP("Setting rwx permissions for 0x%" PRIxPTR ":0x%" PRIxPTR "\n", |
| region.begin(), region.end()); |
| bool success = GetPlatformPageAllocator()->RecommitPages( |
| reinterpret_cast<void*>(region.begin()), region.size(), |
| PageAllocator::kReadWriteExecute); |
| |
| if (V8_UNLIKELY(!success)) { |
| auto oom_detail = base::FormattedString{} << "region size: " |
| << region.size(); |
| V8::FatalProcessOutOfMemory(nullptr, "Commit wasm code space", |
| oom_detail.PrintToArray().data()); |
| UNREACHABLE(); |
| } |
| } |
| |
| void WasmCodeManager::Decommit(base::AddressRegion region) { |
| PageAllocator* allocator = GetPlatformPageAllocator(); |
| DCHECK(IsAligned(region.begin(), allocator->CommitPageSize())); |
| DCHECK(IsAligned(region.size(), allocator->CommitPageSize())); |
| [[maybe_unused]] size_t old_committed = |
| total_committed_code_space_.fetch_sub(region.size()); |
| DCHECK_LE(region.size(), old_committed); |
| TRACE_HEAP("Decommitting system pages 0x%" PRIxPTR ":0x%" PRIxPTR "\n", |
| region.begin(), region.end()); |
| if (V8_UNLIKELY(!allocator->DecommitPages( |
| reinterpret_cast<void*>(region.begin()), region.size()))) { |
| // Decommit can fail in near-OOM situations. |
| auto oom_detail = base::FormattedString{} << "region size: " |
| << region.size(); |
| V8::FatalProcessOutOfMemory(nullptr, "Decommit Wasm code space", |
| oom_detail.PrintToArray().data()); |
| } |
| } |
| |
| void WasmCodeManager::AssignRange(base::AddressRegion region, |
| NativeModule* native_module) { |
| base::MutexGuard lock(&native_modules_mutex_); |
| lookup_map_.insert(std::make_pair( |
| region.begin(), std::make_pair(region.end(), native_module))); |
| } |
| |
| VirtualMemory WasmCodeManager::TryAllocate(size_t size) { |
| v8::PageAllocator* page_allocator = GetPlatformPageAllocator(); |
| DCHECK_GT(size, 0); |
| size_t allocate_page_size = page_allocator->AllocatePageSize(); |
| size = RoundUp(size, allocate_page_size); |
| Address hint = |
| next_code_space_hint_.fetch_add(size, std::memory_order_relaxed); |
| |
| // When we start exposing Wasm in jitless mode, then the jitless flag |
| // will have to determine whether we set kMapAsJittable or not. |
| DCHECK(!v8_flags.jitless); |
| VirtualMemory mem(page_allocator, size, reinterpret_cast<void*>(hint), |
| allocate_page_size, |
| PageAllocator::Permission::kNoAccessWillJitLater); |
| if (!mem.IsReserved()) { |
| // Try resetting {next_code_space_hint_}, which might fail if another thread |
| // bumped it in the meantime. |
| Address bumped_hint = hint + size; |
| next_code_space_hint_.compare_exchange_weak(bumped_hint, hint, |
| std::memory_order_relaxed); |
| return {}; |
| } |
| TRACE_HEAP("VMem alloc: 0x%" PRIxPTR ":0x%" PRIxPTR " (%zu)\n", mem.address(), |
| mem.end(), mem.size()); |
| |
| if (mem.address() != hint) { |
| // If the hint was ignored, just store the end of the new vmem area |
| // unconditionally, potentially racing with other concurrent allocations (it |
| // does not really matter which end pointer we keep in that case). |
| next_code_space_hint_.store(mem.end(), std::memory_order_relaxed); |
| } |
| |
| // Don't pre-commit the code cage on Windows since it uses memory and it's not |
| // required for recommit. |
| #if !defined(V8_OS_WIN) |
| if (MemoryProtectionKeysEnabled()) { |
| #if V8_HAS_PKU_JIT_WRITE_PROTECT |
| if (ThreadIsolation::Enabled()) { |
| CHECK(ThreadIsolation::MakeExecutable(mem.address(), mem.size())); |
| } else { |
| CHECK(base::MemoryProtectionKey::SetPermissionsAndKey( |
| mem.region(), PageAllocator::kReadWriteExecute, |
| RwxMemoryWriteScope::memory_protection_key())); |
| } |
| #else |
| UNREACHABLE(); |
| #endif |
| } else { |
| CHECK(SetPermissions(GetPlatformPageAllocator(), mem.address(), mem.size(), |
| PageAllocator::kReadWriteExecute)); |
| } |
| page_allocator->DiscardSystemPages(reinterpret_cast<void*>(mem.address()), |
| mem.size()); |
| #endif // !defined(V8_OS_WIN) |
| |
| ThreadIsolation::RegisterJitPage(mem.address(), mem.size()); |
| |
| return mem; |
| } |
| |
| namespace { |
| // The numbers here are rough estimates, used to calculate the size of the |
| // initial code reservation and for estimating the amount of external memory |
| // reported to the GC. |
| // They do not need to be accurate. Choosing them too small will result in |
| // separate code spaces being allocated (compile time and runtime overhead), |
| // choosing them too large results in over-reservation (virtual address space |
| // only). |
| // In doubt, choose the numbers slightly too large on 64-bit systems (where |
| // {kNeedsFarJumpsBetweenCodeSpaces} is {true}). Over-reservation is less |
| // critical in a 64-bit address space, but separate code spaces cause overhead. |
| // On 32-bit systems (where {kNeedsFarJumpsBetweenCodeSpaces} is {false}), the |
| // opposite is true: Multiple code spaces are cheaper, and address space is |
| // scarce, hence choose numbers slightly too small. |
| // |
| // Numbers can be determined by running benchmarks with |
| // --trace-wasm-compilation-times, and piping the output through |
| // tools/wasm/code-size-factors.py. |
| #if V8_TARGET_ARCH_X64 |
| constexpr size_t kTurbofanFunctionOverhead = 24; |
| constexpr size_t kTurbofanCodeSizeMultiplier = 3; |
| constexpr size_t kLiftoffFunctionOverhead = 56; |
| constexpr size_t kLiftoffCodeSizeMultiplier = 4; |
| constexpr size_t kImportSize = 640; |
| #elif V8_TARGET_ARCH_IA32 |
| constexpr size_t kTurbofanFunctionOverhead = 20; |
| constexpr size_t kTurbofanCodeSizeMultiplier = 3; |
| constexpr size_t kLiftoffFunctionOverhead = 48; |
| constexpr size_t kLiftoffCodeSizeMultiplier = 3; |
| constexpr size_t kImportSize = 600; |
| #elif V8_TARGET_ARCH_ARM |
| constexpr size_t kTurbofanFunctionOverhead = 44; |
| constexpr size_t kTurbofanCodeSizeMultiplier = 3; |
| constexpr size_t kLiftoffFunctionOverhead = 96; |
| constexpr size_t kLiftoffCodeSizeMultiplier = 5; |
| constexpr size_t kImportSize = 550; |
| #elif V8_TARGET_ARCH_ARM64 |
| constexpr size_t kTurbofanFunctionOverhead = 40; |
| constexpr size_t kTurbofanCodeSizeMultiplier = 3; |
| constexpr size_t kLiftoffFunctionOverhead = 68; |
| constexpr size_t kLiftoffCodeSizeMultiplier = 4; |
| constexpr size_t kImportSize = 750; |
| #else |
| // Other platforms should add their own estimates for best performance. Numbers |
| // below are the maximum of other architectures. |
| constexpr size_t kTurbofanFunctionOverhead = 44; |
| constexpr size_t kTurbofanCodeSizeMultiplier = 4; |
| constexpr size_t kLiftoffFunctionOverhead = 96; |
| constexpr size_t kLiftoffCodeSizeMultiplier = 5; |
| constexpr size_t kImportSize = 750; |
| #endif |
| } // namespace |
| |
| // static |
| size_t WasmCodeManager::EstimateLiftoffCodeSize(int body_size) { |
| return kLiftoffFunctionOverhead + kCodeAlignment / 2 + |
| body_size * kLiftoffCodeSizeMultiplier; |
| } |
| |
| // static |
| size_t WasmCodeManager::EstimateNativeModuleCodeSize( |
| const WasmModule* module, bool include_liftoff, |
| DynamicTiering dynamic_tiering) { |
| int num_functions = static_cast<int>(module->num_declared_functions); |
| int num_imported_functions = static_cast<int>(module->num_imported_functions); |
| int code_section_length = 0; |
| if (num_functions > 0) { |
| DCHECK_EQ(module->functions.size(), num_imported_functions + num_functions); |
| auto* first_fn = &module->functions[module->num_imported_functions]; |
| auto* last_fn = &module->functions.back(); |
| code_section_length = |
| static_cast<int>(last_fn->code.end_offset() - first_fn->code.offset()); |
| } |
| return EstimateNativeModuleCodeSize(num_functions, num_imported_functions, |
| code_section_length, include_liftoff, |
| dynamic_tiering); |
| } |
| |
| // static |
| size_t WasmCodeManager::EstimateNativeModuleCodeSize( |
| int num_functions, int num_imported_functions, int code_section_length, |
| bool include_liftoff, DynamicTiering dynamic_tiering) { |
| // The size for the jump table and far jump table is added later, per code |
| // space (see {OverheadPerCodeSpace}). We still need to add the overhead for |
| // the lazy compile table once, though. There are configurations where we do |
| // not need it (non-asm.js, no dynamic tiering and no lazy compilation), but |
| // we ignore this here as most of the time we will need it. |
| const size_t lazy_compile_table_size = |
| JumpTableAssembler::SizeForNumberOfLazyFunctions(num_functions); |
| |
| const size_t size_of_imports = kImportSize * num_imported_functions; |
| |
| const size_t overhead_per_function_turbofan = |
| kTurbofanFunctionOverhead + kCodeAlignment / 2; |
| size_t size_of_turbofan = overhead_per_function_turbofan * num_functions + |
| kTurbofanCodeSizeMultiplier * code_section_length; |
| |
| const size_t overhead_per_function_liftoff = |
| kLiftoffFunctionOverhead + kCodeAlignment / 2; |
| const size_t size_of_liftoff = |
| include_liftoff ? overhead_per_function_liftoff * num_functions + |
| kLiftoffCodeSizeMultiplier * code_section_length |
| : 0; |
| |
| // With dynamic tiering we don't expect to compile more than 25% with |
| // TurboFan. If there is no liftoff though then all code will get generated |
| // by TurboFan. |
| if (include_liftoff && dynamic_tiering) size_of_turbofan /= 4; |
| |
| return lazy_compile_table_size + size_of_imports + size_of_liftoff + |
| size_of_turbofan; |
| } |
| |
| // static |
| size_t WasmCodeManager::EstimateNativeModuleMetaDataSize( |
| const WasmModule* module) { |
| size_t wasm_module_estimate = module->EstimateStoredSize(); |
| |
| uint32_t num_wasm_functions = module->num_declared_functions; |
| |
| // TODO(wasm): Include wire bytes size. |
| size_t native_module_estimate = |
| sizeof(NativeModule) + // NativeModule struct |
| (sizeof(WasmCode*) * num_wasm_functions) + // code table size |
| (sizeof(WasmCode) * num_wasm_functions); // code object size |
| |
| size_t jump_table_size = RoundUp<kCodeAlignment>( |
| JumpTableAssembler::SizeForNumberOfSlots(num_wasm_functions)); |
| size_t far_jump_table_size = |
| RoundUp<kCodeAlignment>(JumpTableAssembler::SizeForNumberOfFarJumpSlots( |
| BuiltinLookup::BuiltinCount(), |
| NumWasmFunctionsInFarJumpTable(num_wasm_functions))); |
| |
| return wasm_module_estimate + native_module_estimate + jump_table_size + |
| far_jump_table_size; |
| } |
| |
| // static |
| bool WasmCodeManager::HasMemoryProtectionKeySupport() { |
| #if V8_HAS_PKU_JIT_WRITE_PROTECT |
| return RwxMemoryWriteScope::IsSupported(); |
| #else |
| return false; |
| #endif // V8_HAS_PKU_JIT_WRITE_PROTECT |
| } |
| |
| // static |
| bool WasmCodeManager::MemoryProtectionKeysEnabled() { |
| return HasMemoryProtectionKeySupport(); |
| } |
| |
| // static |
| bool WasmCodeManager::MemoryProtectionKeyWritable() { |
| #if V8_HAS_PKU_JIT_WRITE_PROTECT |
| return RwxMemoryWriteScope::IsPKUWritable(); |
| #else |
| return false; |
| #endif // V8_HAS_PKU_JIT_WRITE_PROTECT |
| } |
| |
| std::shared_ptr<NativeModule> WasmCodeManager::NewNativeModule( |
| Isolate* isolate, WasmFeatures enabled, CompileTimeImports compile_imports, |
| size_t code_size_estimate, std::shared_ptr<const WasmModule> module) { |
| if (total_committed_code_space_.load() > |
| critical_committed_code_space_.load()) { |
| (reinterpret_cast<v8::Isolate*>(isolate)) |
| ->MemoryPressureNotification(MemoryPressureLevel::kCritical); |
| size_t committed = total_committed_code_space_.load(); |
| DCHECK_GE(max_committed_code_space_, committed); |
| critical_committed_code_space_.store( |
| committed + (max_committed_code_space_ - committed) / 2); |
| } |
| |
| size_t code_vmem_size = |
| ReservationSize(code_size_estimate, module->num_declared_functions, 0); |
| |
| // The '--wasm-max-initial-code-space-reservation' testing flag can be used to |
| // reduce the maximum size of the initial code space reservation (in MB). |
| if (v8_flags.wasm_max_initial_code_space_reservation > 0) { |
| size_t flag_max_bytes = |
| static_cast<size_t>(v8_flags.wasm_max_initial_code_space_reservation) * |
| MB; |
| if (flag_max_bytes < code_vmem_size) code_vmem_size = flag_max_bytes; |
| } |
| |
| // Try up to two times; getting rid of dead JSArrayBuffer allocations might |
| // require two GCs because the first GC maybe incremental and may have |
| // floating garbage. |
| static constexpr int kAllocationRetries = 2; |
| VirtualMemory code_space; |
| for (int retries = 0;; ++retries) { |
| code_space = TryAllocate(code_vmem_size); |
| if (code_space.IsReserved()) break; |
| if (retries == kAllocationRetries) { |
| auto oom_detail = base::FormattedString{} |
| << "NewNativeModule cannot allocate code space of " |
| << code_vmem_size << " bytes"; |
| V8::FatalProcessOutOfMemory(isolate, "Allocate initial wasm code space", |
| oom_detail.PrintToArray().data()); |
| UNREACHABLE(); |
| } |
| // Run one GC, then try the allocation again. |
| isolate->heap()->MemoryPressureNotification(MemoryPressureLevel::kCritical, |
| true); |
| } |
| |
| Address start = code_space.address(); |
| size_t size = code_space.size(); |
| Address end = code_space.end(); |
| std::shared_ptr<NativeModule> ret; |
| new NativeModule(enabled, compile_imports, |
| DynamicTiering{v8_flags.wasm_dynamic_tiering.value()}, |
| std::move(code_space), std::move(module), |
| isolate->async_counters(), &ret); |
| // The constructor initialized the shared_ptr. |
| DCHECK_NOT_NULL(ret); |
| TRACE_HEAP("New NativeModule %p: Mem: 0x%" PRIxPTR ",+%zu\n", ret.get(), |
| start, size); |
| |
| base::MutexGuard lock(&native_modules_mutex_); |
| lookup_map_.insert(std::make_pair(start, std::make_pair(end, ret.get()))); |
| return ret; |
| } |
| |
| void NativeModule::SampleCodeSize(Counters* counters) const { |
| size_t code_size = code_allocator_.committed_code_space(); |
| int code_size_mb = static_cast<int>(code_size / MB); |
| counters->wasm_module_code_size_mb()->AddSample(code_size_mb); |
| int code_size_kb = static_cast<int>(code_size / KB); |
| counters->wasm_module_code_size_kb()->AddSample(code_size_kb); |
| // If this is a wasm module of >= 2MB, also sample the freed code size, |
| // absolute and relative. Code GC does not happen on asm.js |
| // modules, and small modules will never trigger GC anyway. |
| size_t generated_size = code_allocator_.generated_code_size(); |
| if (generated_size >= 2 * MB && module()->origin == kWasmOrigin) { |
| size_t freed_size = code_allocator_.freed_code_size(); |
| DCHECK_LE(freed_size, generated_size); |
| int freed_percent = static_cast<int>(100 * freed_size / generated_size); |
| counters->wasm_module_freed_code_size_percent()->AddSample(freed_percent); |
| } |
| } |
| |
| std::unique_ptr<WasmCode> NativeModule::AddCompiledCode( |
| const WasmCompilationResult& result) { |
| std::vector<std::unique_ptr<WasmCode>> code = AddCompiledCode({&result, 1}); |
| return std::move(code[0]); |
| } |
| |
| std::vector<std::unique_ptr<WasmCode>> NativeModule::AddCompiledCode( |
| base::Vector<const WasmCompilationResult> results) { |
| TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("v8.wasm.detailed"), |
| "wasm.AddCompiledCode", "num", results.size()); |
| DCHECK(!results.empty()); |
| std::vector<std::unique_ptr<WasmCode>> generated_code; |
| generated_code.reserve(results.size()); |
| |
| // First, allocate code space for all the results. |
| // Never add more than half of a code space at once. This leaves some space |
| // for jump tables and other overhead. We could use {OverheadPerCodeSpace}, |
| // but that's only an approximation, so we are conservative here and never use |
| // more than half a code space. |
| size_t max_code_batch_size = v8_flags.wasm_max_code_space_size_mb * MB / 2; |
| size_t total_code_space = 0; |
| for (auto& result : results) { |
| DCHECK(result.succeeded()); |
| size_t new_code_space = |
| RoundUp<kCodeAlignment>(result.code_desc.instr_size); |
| if (total_code_space + new_code_space > max_code_batch_size) { |
| // Split off the first part of the {results} vector and process it |
| // separately. This method then continues with the rest. |
| size_t split_point = &result - results.begin(); |
| CHECK_WITH_MSG( |
| split_point != 0, |
| "A single code object needs more than half of the code space size"); |
| auto first_results = AddCompiledCode(results.SubVector(0, split_point)); |
| generated_code.insert(generated_code.end(), |
| std::make_move_iterator(first_results.begin()), |
| std::make_move_iterator(first_results.end())); |
| // Continue processing the rest of the vector. This change to the |
| // {results} vector does not invalidate iterators (which are just |
| // pointers). In particular, the end pointer stays the same. |
| results += split_point; |
| total_code_space = 0; |
| } |
| total_code_space += new_code_space; |
| } |
| base::Vector<uint8_t> code_space; |
| NativeModule::JumpTablesRef jump_tables; |
| { |
| base::RecursiveMutexGuard guard{&allocation_mutex_}; |
| code_space = code_allocator_.AllocateForCode(this, total_code_space); |
| // Lookup the jump tables to use once, then use for all code objects. |
| jump_tables = |
| FindJumpTablesForRegionLocked(base::AddressRegionOf(code_space)); |
| } |
| // If we happen to have a {total_code_space} which is bigger than |
| // {kMaxCodeSpaceSize}, we would not find valid jump tables for the whole |
| // region. If this ever happens, we need to handle this case (by splitting the |
| // {results} vector in smaller chunks). |
| CHECK(jump_tables.is_valid()); |
| |
| std::vector<size_t> sizes; |
| for (const auto& result : results) { |
| sizes.emplace_back(RoundUp<kCodeAlignment>(result.code_desc.instr_size)); |
| } |
| ThreadIsolation::RegisterJitAllocations( |
| reinterpret_cast<Address>(code_space.begin()), sizes, |
| ThreadIsolation::JitAllocationType::kWasmCode); |
| |
| // Now copy the generated code into the code space and relocate it. |
| for (auto& result : results) { |
| DCHECK_EQ(result.code_desc.buffer, result.instr_buffer->start()); |
| size_t code_size = RoundUp<kCodeAlignment>(result.code_desc.instr_size); |
| base::Vector<uint8_t> this_code_space = code_space.SubVector(0, code_size); |
| code_space += code_size; |
| generated_code.emplace_back(AddCodeWithCodeSpace( |
| result.func_index, result.code_desc, result.frame_slot_count, |
| result.tagged_parameter_slots, |
| result.protected_instructions_data.as_vector(), |
| result.source_positions.as_vector(), |
| result.inlining_positions.as_vector(), result.deopt_data.as_vector(), |
| GetCodeKind(result), result.result_tier, result.for_debugging, |
| result.frame_has_feedback_slot, this_code_space, jump_tables)); |
| } |
| DCHECK_EQ(0, code_space.size()); |
| |
| // Check that we added the expected amount of code objects, even if we split |
| // the {results} vector. |
| DCHECK_EQ(generated_code.capacity(), generated_code.size()); |
| |
| return generated_code; |
| } |
| |
| void NativeModule::SetDebugState(DebugState new_debug_state) { |
| // Do not tier down asm.js (just never change the tiering state). |
| if (module()->origin != kWasmOrigin) return; |
| |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| debug_state_ = new_debug_state; |
| } |
| |
| namespace { |
| bool ShouldRemoveCode(WasmCode* code, NativeModule::RemoveFilter filter) { |
| if (filter == NativeModule::RemoveFilter::kRemoveDebugCode && |
| !code->for_debugging()) { |
| return false; |
| } |
| if (filter == NativeModule::RemoveFilter::kRemoveNonDebugCode && |
| code->for_debugging()) { |
| return false; |
| } |
| if (filter == NativeModule::RemoveFilter::kRemoveLiftoffCode && |
| !code->is_liftoff()) { |
| return false; |
| } |
| if (filter == NativeModule::RemoveFilter::kRemoveTurbofanCode && |
| !code->is_turbofan()) { |
| return false; |
| } |
| return true; |
| } |
| } // namespace |
| |
| void NativeModule::RemoveCompiledCode(RemoveFilter filter) { |
| const uint32_t num_imports = module_->num_imported_functions; |
| const uint32_t num_functions = module_->num_declared_functions; |
| WasmCodeRefScope ref_scope; |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| for (uint32_t i = 0; i < num_functions; i++) { |
| WasmCode* code = code_table_[i]; |
| if (code && ShouldRemoveCode(code, filter)) { |
| code_table_[i] = nullptr; |
| // Add the code to the {WasmCodeRefScope}, so the ref count cannot drop to |
| // zero here. It might in the {WasmCodeRefScope} destructor, though. |
| WasmCodeRefScope::AddRef(code); |
| code->DecRefOnLiveCode(); |
| uint32_t func_index = i + num_imports; |
| UseLazyStubLocked(func_index); |
| } |
| } |
| // When resuming optimized execution after a debugging session ends, or when |
| // discarding optimized code that made outdated assumptions, allow another |
| // tier-up task to get scheduled. |
| if (filter == RemoveFilter::kRemoveDebugCode || |
| filter == RemoveFilter::kRemoveTurbofanCode) { |
| compilation_state_->AllowAnotherTopTierJobForAllFunctions(); |
| } |
| } |
| |
| void NativeModule::FreeCode(base::Vector<WasmCode* const> codes) { |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| // Free the code space. |
| code_allocator_.FreeCode(codes); |
| |
| if (!new_owned_code_.empty()) TransferNewOwnedCodeLocked(); |
| DebugInfo* debug_info = debug_info_.get(); |
| // Free the {WasmCode} objects. This will also unregister trap handler data. |
| for (WasmCode* code : codes) { |
| DCHECK_EQ(1, owned_code_.count(code->instruction_start())); |
| owned_code_.erase(code->instruction_start()); |
| } |
| // Remove debug side tables for all removed code objects, after releasing our |
| // lock. This is to avoid lock order inversion. |
| if (debug_info) debug_info->RemoveDebugSideTables(codes); |
| } |
| |
| size_t NativeModule::GetNumberOfCodeSpacesForTesting() const { |
| base::RecursiveMutexGuard guard{&allocation_mutex_}; |
| return code_allocator_.GetNumCodeSpaces(); |
| } |
| |
| bool NativeModule::HasDebugInfo() const { |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| return debug_info_ != nullptr; |
| } |
| |
| DebugInfo* NativeModule::GetDebugInfo() { |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| if (!debug_info_) debug_info_ = std::make_unique<DebugInfo>(this); |
| return debug_info_.get(); |
| } |
| |
| NamesProvider* NativeModule::GetNamesProvider() { |
| DCHECK(HasWireBytes()); |
| base::RecursiveMutexGuard guard(&allocation_mutex_); |
| if (!names_provider_) { |
| names_provider_ = |
| std::make_unique<NamesProvider>(module_.get(), wire_bytes()); |
| } |
| return names_provider_.get(); |
| } |
| |
| size_t NativeModule::EstimateCurrentMemoryConsumption() const { |
| UPDATE_WHEN_CLASS_CHANGES(NativeModule, 536); |
| size_t result = sizeof(NativeModule); |
| result += module_->EstimateCurrentMemoryConsumption(); |
| |
| std::shared_ptr<base::OwnedVector<const uint8_t>> wire_bytes = |
| std::atomic_load(&wire_bytes_); |
| size_t wire_bytes_size = wire_bytes ? wire_bytes->size() : 0; |
| result += wire_bytes_size; |
| |
| if (source_map_) { |
| result += source_map_->EstimateCurrentMemoryConsumption(); |
| } |
| result += compilation_state_->EstimateCurrentMemoryConsumption(); |
| result += import_wrapper_cache_.EstimateCurrentMemoryConsumption(); |
| // For {tiering_budgets_}. |
| result += module_->num_declared_functions * sizeof(uint32_t); |
| |
| // For fast api call targets. |
| result += module_->num_imported_functions * |
| (sizeof(std::atomic<Address>) + sizeof(CFunctionInfo*)); |
| { |
| base::RecursiveMutexGuard lock(&allocation_mutex_); |
| result += ContentSize(owned_code_); |
| for (auto& [address, unique_code_ptr] : owned_code_) { |
| result += unique_code_ptr->EstimateCurrentMemoryConsumption(); |
| } |
| result += ContentSize(new_owned_code_); |
| for (std::unique_ptr<WasmCode>& code : new_owned_code_) { |
| result += code->EstimateCurrentMemoryConsumption(); |
| } |
| // For {code_table_}. |
| result += module_->num_declared_functions * sizeof(void*); |
| result += ContentSize(code_space_data_); |
| if (debug_info_) { |
| result += debug_info_->EstimateCurrentMemoryConsumption(); |
| } |
| if (names_provider_) { |
| result += names_provider_->EstimateCurrentMemoryConsumption(); |
| } |
| if (cached_code_) { |
| result += ContentSize(*cached_code_); |
| } |
| } |
| |
| if (v8_flags.trace_wasm_offheap_memory) { |
| PrintF("NativeModule wire bytes: %zu\n", wire_bytes_size); |
| PrintF("NativeModule: %zu\n", result); |
| } |
| return result; |
| } |
| |
| void WasmCodeManager::FreeNativeModule( |
| base::Vector<VirtualMemory> owned_code_space, size_t committed_size) { |
| base::MutexGuard lock(&native_modules_mutex_); |
| for (auto& code_space : owned_code_space) { |
| DCHECK(code_space.IsReserved()); |
| TRACE_HEAP("VMem Release: 0x%" PRIxPTR ":0x%" PRIxPTR " (%zu)\n", |
| code_space.address(), code_space.end(), code_space.size()); |
| |
| #if defined(V8_OS_WIN64) |
| if (CanRegisterUnwindInfoForNonABICompliantCodeRange()) { |
| win64_unwindinfo::UnregisterNonABICompliantCodeRange( |
| reinterpret_cast<void*>(code_space.address())); |
| } |
| #endif // V8_OS_WIN64 |
| |
| lookup_map_.erase(code_space.address()); |
| ThreadIsolation::UnregisterJitPage(code_space.address(), code_space.size()); |
| code_space.Free(); |
| DCHECK(!code_space.IsReserved()); |
| } |
| |
| DCHECK(IsAligned(committed_size, CommitPageSize())); |
| [[maybe_unused]] size_t old_committed = |
| total_committed_code_space_.fetch_sub(committed_size); |
| DCHECK_LE(committed_size, old_committed); |
| } |
| |
| NativeModule* WasmCodeManager::LookupNativeModule(Address pc) const { |
| base::MutexGuard lock(&native_modules_mutex_); |
| if (lookup_map_.empty()) return nullptr; |
| |
| auto iter = lookup_map_.upper_bound(pc); |
| if (iter == lookup_map_.begin()) return nullptr; |
| --iter; |
| Address region_start = iter->first; |
| Address region_end = iter->second.first; |
| NativeModule* candidate = iter->second.second; |
| |
| DCHECK_NOT_NULL(candidate); |
| return region_start <= pc && pc < region_end ? candidate : nullptr; |
| } |
| |
| WasmCode* WasmCodeManager::LookupCode(Address pc) const { |
| NativeModule* candidate = LookupNativeModule(pc); |
| return candidate ? candidate->Lookup(pc) : nullptr; |
| } |
| |
| WasmCode* WasmCodeManager::LookupCode(Isolate* isolate, Address pc) const { |
| // Since kNullAddress is used as a sentinel value, we should not try |
| // to look it up in the cache |
| if (pc == kNullAddress) return nullptr; |
| // If 'isolate' is nullptr, do not use a cache. This can happen when |
| // called from function V8NameConverter::NameOfAddress |
| if (isolate) { |
| return isolate->wasm_code_look_up_cache()->GetCacheEntry(pc)->code; |
| } else { |
| wasm::WasmCodeRefScope code_ref_scope; |
| return LookupCode(pc); |
| } |
| } |
| |
| std::pair<WasmCode*, SafepointEntry> WasmCodeManager::LookupCodeAndSafepoint( |
| Isolate* isolate, Address pc) { |
| auto* entry = isolate->wasm_code_look_up_cache()->GetCacheEntry(pc); |
| WasmCode* code = entry->code; |
| DCHECK_NOT_NULL(code); |
| // For protected instructions we usually do not emit a safepoint because the |
| // frame will be unwound anyway. The exception is debugging code, where the |
| // frame might be inspected if "pause on exception" is set. |
| // For those instructions, we thus need to explicitly return an empty |
| // safepoint; using any previously registered safepoint can lead to crashes |
| // when we try to visit spill slots that do not hold tagged values at this |
| // point. |
| // Evaluate this condition only on demand (the fast path does not need it). |
| auto expect_safepoint = [code, pc]() { |
| const bool is_protected_instruction = code->IsProtectedInstruction( |
| pc - WasmFrameConstants::kProtectedInstructionReturnAddressOffset); |
| return !is_protected_instruction || code->for_debugging(); |
| }; |
| if (!entry->safepoint_entry.is_initialized() && expect_safepoint()) { |
| entry->safepoint_entry = SafepointTable{code}.TryFindEntry(pc); |
| CHECK(entry->safepoint_entry.is_initialized()); |
| } else if (expect_safepoint()) { |
| DCHECK_EQ(entry->safepoint_entry, SafepointTable{code}.TryFindEntry(pc)); |
| } else { |
| DCHECK(!entry->safepoint_entry.is_initialized()); |
| } |
| return std::make_pair(code, entry->safepoint_entry); |
| } |
| |
| void WasmCodeManager::FlushCodeLookupCache(Isolate* isolate) { |
| return isolate->wasm_code_look_up_cache()->Flush(); |
| } |
| |
| namespace { |
| thread_local WasmCodeRefScope* current_code_refs_scope = nullptr; |
| } // namespace |
| |
| WasmCodeRefScope::WasmCodeRefScope() |
| : previous_scope_(current_code_refs_scope) { |
| current_code_refs_scope = this; |
| } |
| |
| WasmCodeRefScope::~WasmCodeRefScope() { |
| DCHECK_EQ(this, current_code_refs_scope); |
| current_code_refs_scope = previous_scope_; |
| WasmCode::DecrementRefCount(base::VectorOf(code_ptrs_)); |
| } |
| |
| // static |
| void WasmCodeRefScope::AddRef(WasmCode* code) { |
| DCHECK_NOT_NULL(code); |
| WasmCodeRefScope* current_scope = current_code_refs_scope; |
| DCHECK_NOT_NULL(current_scope); |
| current_scope->code_ptrs_.push_back(code); |
| code->IncRef(); |
| } |
| |
| void WasmCodeLookupCache::Flush() { |
| for (int i = 0; i < kWasmCodeLookupCacheSize; i++) |
| cache_[i].pc.store(kNullAddress, std::memory_order_release); |
| } |
| |
| WasmCodeLookupCache::CacheEntry* WasmCodeLookupCache::GetCacheEntry( |
| Address pc) { |
| static_assert(base::bits::IsPowerOfTwo(kWasmCodeLookupCacheSize)); |
| DCHECK(pc != kNullAddress); |
| uint32_t hash = ComputeAddressHash(pc); |
| uint32_t index = hash & (kWasmCodeLookupCacheSize - 1); |
| CacheEntry* entry = &cache_[index]; |
| if (entry->pc.load(std::memory_order_acquire) == pc) { |
| // Code can be deallocated at two points: |
| // - when the NativeModule that references it is garbage- |
| // collected; |
| // - when it is no longer referenced by its NativeModule, nor from |
| // any stack. |
| // The cache is cleared when a NativeModule is destroyed, and when |
| // the isolate reports the set of code referenced from its stacks. |
| // So, if the code is the cache, it is because it was live at some |
| // point (when inserted in the cache), its native module is still |
| // considered live, and it has not yet been reported as no longer |
| // referenced from any stack. It thus cannot have been released |
| // yet. |
| #ifdef DEBUG |
| wasm::WasmCodeRefScope code_ref_scope; |
| DCHECK_EQ(entry->code, wasm::GetWasmCodeManager()->LookupCode(pc)); |
| #endif // DEBUG |
| } else { |
| // For WebAssembly frames we perform a lookup in the handler table. |
| // This code ref scope is here to avoid a check failure when looking up |
| // the code. It's not actually necessary to keep the code alive as it's |
| // currently being executed. |
| wasm::WasmCodeRefScope code_ref_scope; |
| entry->pc.store(pc, std::memory_order_release); |
| entry->code = wasm::GetWasmCodeManager()->LookupCode(pc); |
| entry->safepoint_entry.Reset(); |
| } |
| return entry; |
| } |
| } // namespace wasm |
| } // namespace internal |
| } // namespace v8 |
| #undef TRACE_HEAP |