blob: d5b738676fd87a694d5c63127e39020f313bf055 [file] [log] [blame]
// Copyright 2022 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/canonical-types.h"
#include "src/base/hashing.h"
#include "src/execution/isolate.h"
#include "src/handles/handles-inl.h"
#include "src/heap/heap-inl.h"
#include "src/init/v8.h"
#include "src/roots/roots-inl.h"
#include "src/utils/utils.h"
#include "src/wasm/names-provider.h"
#include "src/wasm/std-object-sizes.h"
#include "src/wasm/wasm-engine.h"
namespace v8::internal::wasm {
TypeCanonicalizer* GetTypeCanonicalizer() {
return GetWasmEngine()->type_canonicalizer();
}
TypeCanonicalizer::TypeCanonicalizer() { AddPredefinedArrayTypes(); }
void TypeCanonicalizer::CheckMaxCanonicalIndex() const {
if (V8_UNLIKELY(canonical_supertypes_.size() > kMaxCanonicalTypes)) {
V8::FatalProcessOutOfMemory(nullptr, "too many canonicalized types");
}
}
void TypeCanonicalizer::AddRecursiveGroup(WasmModule* module, uint32_t size) {
if (size == 0) return;
// If the caller knows statically that {size == 1}, it should have called
// {AddRecursiveSingletonGroup} directly. For cases where this is not
// statically determined we add this dispatch here.
if (size == 1) return AddRecursiveSingletonGroup(module);
uint32_t start_index = static_cast<uint32_t>(module->types.size() - size);
// Multiple threads could try to register recursive groups concurrently.
// TODO(manoskouk): Investigate if we can fine-grain the synchronization.
base::MutexGuard mutex_guard(&mutex_);
// Compute the first canonical index in the recgroup in the case that it does
// not already exist.
CanonicalTypeIndex first_new_canonical_index{
static_cast<uint32_t>(canonical_supertypes_.size())};
// Create a snapshot of the zone; this will be restored in case we find a
// matching recursion group.
ZoneSnapshot zone_snapshot = zone_.Snapshot();
DCHECK_GE(module->types.size(), start_index + size);
CanonicalGroup group{&zone_, size, first_new_canonical_index};
for (uint32_t i = 0; i < size; i++) {
group.types[i] = CanonicalizeTypeDef(
module, ModuleTypeIndex{start_index + i}, ModuleTypeIndex{start_index},
first_new_canonical_index);
}
if (CanonicalTypeIndex canonical_index = FindCanonicalGroup(group);
canonical_index.valid()) {
// Delete zone memory from {CanonicalizeTypeDef} and {CanonicalGroup}.
zone_snapshot.Restore(&zone_);
// Identical group found. Map new types to the old types's canonical
// representatives.
for (uint32_t i = 0; i < size; i++) {
CanonicalTypeIndex existing_type_index =
CanonicalTypeIndex{canonical_index.index + i};
module->isorecursive_canonical_type_ids[start_index + i] =
existing_type_index;
}
return;
}
canonical_supertypes_.resize(first_new_canonical_index.index + size);
CheckMaxCanonicalIndex();
canonical_types_.reserve(first_new_canonical_index.index + size, &zone_);
for (uint32_t i = 0; i < size; i++) {
CanonicalType& canonical_type = group.types[i];
CanonicalTypeIndex canonical_id{first_new_canonical_index.index + i};
// {CanonicalGroup} allocates types in the Zone.
DCHECK(zone_.Contains(&canonical_type));
canonical_types_.set(canonical_id, &canonical_type);
canonical_supertypes_[canonical_id.index] = canonical_type.supertype;
module->isorecursive_canonical_type_ids[start_index + i] = canonical_id;
}
// Check that this canonical ID is not used yet.
DCHECK(std::none_of(
canonical_singleton_groups_.begin(), canonical_singleton_groups_.end(),
[=](auto& entry) { return entry.index == first_new_canonical_index; }));
DCHECK(std::none_of(
canonical_groups_.begin(), canonical_groups_.end(),
[=](auto& entry) { return entry.first == first_new_canonical_index; }));
canonical_groups_.emplace(group);
}
void TypeCanonicalizer::AddRecursiveSingletonGroup(WasmModule* module) {
DCHECK(!module->types.empty());
uint32_t type_index = static_cast<uint32_t>(module->types.size() - 1);
base::MutexGuard guard(&mutex_);
CanonicalTypeIndex new_canonical_index{
static_cast<uint32_t>(canonical_supertypes_.size())};
// Snapshot the zone before allocating the new type; the zone will be reset if
// we find an identical type.
ZoneSnapshot zone_snapshot = zone_.Snapshot();
CanonicalType type =
CanonicalizeTypeDef(module, ModuleTypeIndex{type_index},
ModuleTypeIndex{type_index}, new_canonical_index);
CanonicalSingletonGroup group{type, new_canonical_index};
if (CanonicalTypeIndex index = FindCanonicalGroup(group); index.valid()) {
zone_snapshot.Restore(&zone_);
module->isorecursive_canonical_type_ids[type_index] = index;
return;
}
// Check that the new canonical ID is not used yet.
DCHECK(std::none_of(
canonical_singleton_groups_.begin(), canonical_singleton_groups_.end(),
[=](auto& entry) { return entry.index == new_canonical_index; }));
DCHECK(std::none_of(
canonical_groups_.begin(), canonical_groups_.end(),
[=](auto& entry) { return entry.first == new_canonical_index; }));
// {group.type} is stack-allocated, whereas {canonical_singleton_groups_}
// creates a long-lived zone-allocated copy of it.
auto stored_group = canonical_singleton_groups_.emplace(group).first;
canonical_supertypes_.push_back(type.supertype);
CheckMaxCanonicalIndex();
canonical_types_.reserve(new_canonical_index.index + 1, &zone_);
canonical_types_.set(new_canonical_index, &stored_group->type);
module->isorecursive_canonical_type_ids[type_index] = new_canonical_index;
}
CanonicalTypeIndex TypeCanonicalizer::AddRecursiveGroup(
const FunctionSig* sig) {
// Types in the signature must be module-independent.
#if DEBUG
for (ValueType type : sig->all()) DCHECK(!type.has_index());
#endif
const bool kFinal = true;
// Because of the checks above, we can treat the type_def as canonical.
// TODO(366180605): It would be nice to not have to rely on a cast here.
// Is there a way to avoid it? In the meantime, these asserts provide at
// least partial assurances that the cast is safe:
static_assert(sizeof(CanonicalValueType) == sizeof(ValueType));
static_assert(
CanonicalValueType::Primitive(NumericKind::kI32).raw_bit_field() ==
ValueType::Primitive(kI32).raw_bit_field());
CanonicalType canonical{reinterpret_cast<const CanonicalSig*>(sig),
CanonicalTypeIndex{kNoSuperType}, kFinal, kNotShared};
base::MutexGuard guard(&mutex_);
// Fast path lookup before canonicalizing (== copying into the
// TypeCanonicalizer's zone) the function signature.
CanonicalTypeIndex new_canonical_index{
static_cast<uint32_t>(canonical_supertypes_.size())};
CanonicalTypeIndex index = FindCanonicalGroup(
CanonicalSingletonGroup{canonical, new_canonical_index});
if (index.valid()) return index;
// Copy into this class's zone to store this as a new canonical function type.
CanonicalSig::Builder builder(&zone_, sig->return_count(),
sig->parameter_count());
for (ValueType ret : sig->returns()) {
builder.AddReturn(CanonicalValueType{ret});
}
for (ValueType param : sig->parameters()) {
builder.AddParam(CanonicalValueType{param});
}
canonical.function_sig = builder.Get();
CanonicalSingletonGroup group{canonical, new_canonical_index};
// Copying the signature shouldn't make a difference: There is no match.
DCHECK(!FindCanonicalGroup(group).valid());
// Check that the new canonical ID is not used yet.
DCHECK(std::none_of(
canonical_singleton_groups_.begin(), canonical_singleton_groups_.end(),
[=](auto& entry) { return entry.index == new_canonical_index; }));
DCHECK(std::none_of(
canonical_groups_.begin(), canonical_groups_.end(),
[=](auto& entry) { return entry.first == new_canonical_index; }));
// {group.type} is stack-allocated, whereas {canonical_singleton_groups_}
// creates a long-lived zone-allocated copy of it.
const CanonicalSingletonGroup& stored_group =
*canonical_singleton_groups_.emplace(group).first;
canonical_supertypes_.push_back(CanonicalTypeIndex{kNoSuperType});
CheckMaxCanonicalIndex();
canonical_types_.reserve(new_canonical_index.index + 1, &zone_);
canonical_types_.set(new_canonical_index, &stored_group.type);
return new_canonical_index;
}
const CanonicalSig* TypeCanonicalizer::LookupFunctionSignature(
CanonicalTypeIndex index) const {
const CanonicalType* type = canonical_types_[index];
SBXCHECK_EQ(type->kind, CanonicalType::kFunction);
return type->function_sig;
}
const CanonicalStructType* TypeCanonicalizer::LookupStruct(
CanonicalTypeIndex index) const {
const CanonicalType* type = canonical_types_[index];
SBXCHECK_EQ(type->kind, CanonicalType::kStruct);
return type->struct_type;
}
const CanonicalArrayType* TypeCanonicalizer::LookupArray(
CanonicalTypeIndex index) const {
const CanonicalType* type = canonical_types_[index];
SBXCHECK_EQ(type->kind, CanonicalType::kArray);
return type->array_type;
}
void TypeCanonicalizer::AddPredefinedArrayTypes() {
static constexpr std::pair<CanonicalTypeIndex, CanonicalValueType>
kPredefinedArrayTypes[] = {{kPredefinedArrayI8Index, {kWasmI8}},
{kPredefinedArrayI16Index, {kWasmI16}}};
canonical_types_.reserve(kNumberOfPredefinedTypes, &zone_);
for (auto [index, element_type] : kPredefinedArrayTypes) {
DCHECK_GT(kNumberOfPredefinedTypes, index.index);
DCHECK_EQ(index.index, canonical_singleton_groups_.size());
static constexpr bool kMutable = true;
static constexpr bool kFinal = true;
static constexpr bool kShared = false; // TODO(14616): Fix this.
CanonicalArrayType* type =
zone_.New<CanonicalArrayType>(element_type, kMutable);
CanonicalSingletonGroup group{
.type = CanonicalType(type, CanonicalTypeIndex{kNoSuperType}, kFinal,
kShared),
.index = index};
const CanonicalSingletonGroup& stored_group =
*canonical_singleton_groups_.emplace(group).first;
canonical_types_.set(index, &stored_group.type);
canonical_supertypes_.emplace_back(CanonicalTypeIndex{kNoSuperType});
DCHECK_LE(canonical_supertypes_.size(), kMaxCanonicalTypes);
}
}
bool TypeCanonicalizer::IsCanonicalSubtype(CanonicalTypeIndex sub_index,
CanonicalTypeIndex super_index) {
// Fast path without synchronization:
if (sub_index == super_index) return true;
// Multiple threads could try to register and access recursive groups
// concurrently.
// TODO(manoskouk): Investigate if we can improve this synchronization.
base::MutexGuard mutex_guard(&mutex_);
return IsCanonicalSubtype_Locked(sub_index, super_index);
}
bool TypeCanonicalizer::IsCanonicalSubtype_Locked(
CanonicalTypeIndex sub_index, CanonicalTypeIndex super_index) const {
while (sub_index.valid()) {
if (sub_index == super_index) return true;
// TODO(jkummerow): Investigate if replacing this with
// `sub_index = canonical_types_[sub_index].supertype;`
// has acceptable performance, which would allow us to save the memory
// cost of storing {canonical_supertypes_}.
sub_index = canonical_supertypes_[sub_index.index];
}
return false;
}
bool TypeCanonicalizer::IsCanonicalSubtype(ModuleTypeIndex sub_index,
ModuleTypeIndex super_index,
const WasmModule* sub_module,
const WasmModule* super_module) {
CanonicalTypeIndex canonical_super =
super_module->canonical_type_id(super_index);
CanonicalTypeIndex canonical_sub = sub_module->canonical_type_id(sub_index);
return IsCanonicalSubtype(canonical_sub, canonical_super);
}
bool TypeCanonicalizer::IsHeapSubtype(CanonicalTypeIndex sub,
CanonicalTypeIndex super) const {
DCHECK_NE(sub, super);
base::MutexGuard mutex_guard(&mutex_);
return IsCanonicalSubtype_Locked(sub, super);
}
void TypeCanonicalizer::EmptyStorageForTesting() {
// Any remaining native modules might reference the types we're about to
// clear.
CHECK_EQ(GetWasmEngine()->NativeModuleCount(), 0);
base::MutexGuard mutex_guard(&mutex_);
canonical_types_.ClearForTesting();
canonical_supertypes_.clear();
canonical_groups_.clear();
canonical_singleton_groups_.clear();
zone_.Reset();
AddPredefinedArrayTypes();
}
TypeCanonicalizer::CanonicalType TypeCanonicalizer::CanonicalizeTypeDef(
const WasmModule* module, ModuleTypeIndex module_type_idx,
ModuleTypeIndex recgroup_start,
CanonicalTypeIndex canonical_recgroup_start) {
mutex_.AssertHeld(); // The caller must hold the mutex.
auto CanonicalizeTypeIndex = [=](ModuleTypeIndex type_index) {
DCHECK(type_index.valid());
if (type_index < recgroup_start) {
// This references a type from an earlier recgroup; use the
// already-canonicalized type index.
return module->canonical_type_id(type_index);
}
// For types within the same recgroup, generate indexes assuming that this
// is a new canonical recgroup. To prevent truncation in the
// CanonicalValueType's bit field, we must check the range here.
uint32_t new_index = canonical_recgroup_start.index +
(type_index.index - recgroup_start.index);
if (V8_UNLIKELY(new_index >= kMaxCanonicalTypes)) {
V8::FatalProcessOutOfMemory(nullptr, "too many canonicalized types");
}
return CanonicalTypeIndex{new_index};
};
auto CanonicalizeValueType = [=](ValueType type) {
if (!type.has_index()) return CanonicalValueType{type};
static_assert(kMaxCanonicalTypes <=
(1 << CanonicalValueType::kNumIndexBits));
return type.Canonicalize(CanonicalizeTypeIndex(type.ref_index()));
};
TypeDefinition type = module->type(module_type_idx);
CanonicalTypeIndex supertype = type.supertype.valid()
? CanonicalizeTypeIndex(type.supertype)
: CanonicalTypeIndex::Invalid();
switch (type.kind) {
case TypeDefinition::kFunction: {
const FunctionSig* original_sig = type.function_sig;
CanonicalSig::Builder builder(&zone_, original_sig->return_count(),
original_sig->parameter_count());
for (ValueType ret : original_sig->returns()) {
builder.AddReturn(CanonicalizeValueType(ret));
}
for (ValueType param : original_sig->parameters()) {
builder.AddParam(CanonicalizeValueType(param));
}
return CanonicalType(builder.Get(), supertype, type.is_final,
type.is_shared);
}
case TypeDefinition::kStruct: {
const StructType* original_type = type.struct_type;
CanonicalStructType::Builder builder(&zone_,
original_type->field_count());
for (uint32_t i = 0; i < original_type->field_count(); i++) {
builder.AddField(CanonicalizeValueType(original_type->field(i)),
original_type->mutability(i),
original_type->field_offset(i));
}
builder.set_total_fields_size(original_type->total_fields_size());
return CanonicalType(
builder.Build(CanonicalStructType::Builder::kUseProvidedOffsets),
supertype, type.is_final, type.is_shared);
}
case TypeDefinition::kArray: {
CanonicalValueType element_type =
CanonicalizeValueType(type.array_type->element_type());
CanonicalArrayType* array_type = zone_.New<CanonicalArrayType>(
element_type, type.array_type->mutability());
return CanonicalType(array_type, supertype, type.is_final,
type.is_shared);
}
case TypeDefinition::kCont: {
CanonicalTypeIndex canonical_index =
CanonicalizeTypeIndex(type.cont_type->contfun_typeindex());
CanonicalContType* canonical_cont =
zone_.New<CanonicalContType>(canonical_index);
return CanonicalType(canonical_cont, supertype, type.is_final,
type.is_shared);
}
}
}
// Returns the index of the canonical representative of the first type in this
// group if it exists, and `CanonicalTypeIndex::Invalid()` otherwise.
CanonicalTypeIndex TypeCanonicalizer::FindCanonicalGroup(
const CanonicalGroup& group) const {
// Groups of size 0 do not make sense here; groups of size 1 should use
// {CanonicalSingletonGroup} (see below).
DCHECK_LT(1, group.types.size());
auto it = canonical_groups_.find(group);
return it == canonical_groups_.end() ? CanonicalTypeIndex::Invalid()
: it->first;
}
// Returns the canonical index of the given group if it already exists.
CanonicalTypeIndex TypeCanonicalizer::FindCanonicalGroup(
const CanonicalSingletonGroup& group) const {
auto it = canonical_singleton_groups_.find(group);
static_assert(kMaxCanonicalTypes <= kMaxInt);
return it == canonical_singleton_groups_.end() ? CanonicalTypeIndex::Invalid()
: it->index;
}
size_t TypeCanonicalizer::EstimateCurrentMemoryConsumption() const {
UPDATE_WHEN_CLASS_CHANGES(TypeCanonicalizer, 8040);
// The storage of the canonical group's types is accounted for via the
// allocator below (which tracks the zone memory).
base::MutexGuard mutex_guard(&mutex_);
size_t result = ContentSize(canonical_supertypes_);
result += ContentSize(canonical_groups_);
result += ContentSize(canonical_singleton_groups_);
// Note: the allocator also tracks zone allocations of `canonical_types_`.
result += allocator_.GetCurrentMemoryUsage();
if (v8_flags.trace_wasm_offheap_memory) {
PrintF("TypeCanonicalizer: %zu\n", result);
}
return result;
}
size_t TypeCanonicalizer::GetCurrentNumberOfTypes() const {
base::MutexGuard mutex_guard(&mutex_);
return canonical_supertypes_.size();
}
// static
void TypeCanonicalizer::PrepareForCanonicalTypeId(Isolate* isolate,
CanonicalTypeIndex id) {
if (!id.valid()) return;
Heap* heap = isolate->heap();
// {2 * (id + 1)} needs to fit in an int.
CHECK_LE(id.index, kMaxInt / 2 - 1);
// Canonical types and wrappers are zero-indexed.
const int length = id.index + 1;
// The fast path is non-handlified.
Tagged<WeakFixedArray> old_rtts_raw = heap->wasm_canonical_rtts();
Tagged<WeakFixedArray> old_wrappers_raw = heap->js_to_wasm_wrappers();
// Fast path: Lengths are sufficient.
int old_length = old_rtts_raw->length();
DCHECK_EQ(old_length, old_wrappers_raw->length());
if (old_length >= length) return;
// Allocate bigger WeakFixedArrays for rtts and wrappers. Grow them
// exponentially.
const int new_length = std::max(old_length * 3 / 2, length);
CHECK_LT(old_length, new_length);
// Allocation can invalidate previous unhandled pointers.
DirectHandle<WeakFixedArray> old_rtts{old_rtts_raw, isolate};
DirectHandle<WeakFixedArray> old_wrappers{old_wrappers_raw, isolate};
old_rtts_raw = old_wrappers_raw = {};
// We allocate the WeakFixedArray filled with undefined values, as we cannot
// pass the cleared value in a handle (see https://crbug.com/364591622). We
// overwrite the new entries via {MemsetTagged} afterwards.
DirectHandle<WeakFixedArray> new_rtts =
WeakFixedArray::New(isolate, new_length, AllocationType::kOld);
WeakFixedArray::CopyElements(isolate, *new_rtts, 0, *old_rtts, 0, old_length);
MemsetTagged(new_rtts->RawFieldOfFirstElement() + old_length,
ClearedValue(isolate), new_length - old_length);
DirectHandle<WeakFixedArray> new_wrappers =
WeakFixedArray::New(isolate, new_length, AllocationType::kOld);
WeakFixedArray::CopyElements(isolate, *new_wrappers, 0, *old_wrappers, 0,
old_length);
MemsetTagged(new_wrappers->RawFieldOfFirstElement() + old_length,
ClearedValue(isolate), new_length - old_length);
heap->SetWasmCanonicalRttsAndJSToWasmWrappers(*new_rtts, *new_wrappers);
}
// static
void TypeCanonicalizer::ClearWasmCanonicalTypesForTesting(Isolate* isolate) {
ReadOnlyRoots roots(isolate);
isolate->heap()->SetWasmCanonicalRttsAndJSToWasmWrappers(
roots.empty_weak_fixed_array(), roots.empty_weak_fixed_array());
}
bool TypeCanonicalizer::IsFunctionSignature(CanonicalTypeIndex index) const {
return canonical_types_[index]->kind == CanonicalType::kFunction;
}
bool TypeCanonicalizer::IsStruct(CanonicalTypeIndex index) const {
return canonical_types_[index]->kind == CanonicalType::kStruct;
}
bool TypeCanonicalizer::IsArray(CanonicalTypeIndex index) const {
return canonical_types_[index]->kind == CanonicalType::kArray;
}
CanonicalTypeIndex TypeCanonicalizer::FindIndex_Slow(
const CanonicalSig* sig) const {
// TODO(397489547): Make this faster. The plan is to allocate an extra
// slot in the Zone immediately preceding each CanonicalSig, so we can
// get from the sig's address to that slot's address via pointer arithmetic.
// For now, just search through all known signatures, which is acceptable
// as long as only the type-reflection proposal needs this.
// TODO(42210967): Improve this before shipping Type Reflection.
return canonical_types_.FindIndex_Slow(sig);
}
#ifdef DEBUG
bool TypeCanonicalizer::Contains(const CanonicalSig* sig) const {
base::MutexGuard mutex_guard(&mutex_);
return zone_.Contains(sig);
}
#endif
} // namespace v8::internal::wasm