Commit 82726b64 authored by Leszek Swirski's avatar Leszek Swirski Committed by Commit Bot

[hashtable] Allow GlobalDictionary to have holes

Rather than marking deleted GlobalDictionary entries with a "The Hole"
valued PropertyCell, we now remove those PropertyCells entirely and
use the standard HashTable deleted item marker (also the Hole).

This comes with several simplifications:

  1) We no longer need a customizable IsKey method on HastTable shapes,
     which was only used by GlobalDictionary to mark "The Hole" cells
     as not real keys,
  2) We can get rid of IsLive/IsKey from the Shape entirely, and define
     it directly in the HashTable, which will also allow us (in the
     future) to encourage caching of "undefined" and "Hole" where used
     for IsKey checks,
  3) PropertyCell invalidation doesn't necessarily have to allocate a
     new replacement cell (specifically, on deletion), nor does it have
     to deal with cells that contain the Hole,
  4) kNeedsHoleCheck is renamed to kMatchNeedsHoleCheck (to be explicit
     that this is only needed to guard IsMatch, which may do an
     indentity comparison and thus not need the HoleCheck guard). It's
     also moved out of BaseShape and into the various shapes that
     define IsMatch, to make them more explicitly think about the
     value,
  5) Modified some while loops into for loops to allow clearer use of
     "continue" on successful hole checks.

Change-Id: If591cbb6b49d59726bdc615413aba4f78fd64632
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2292230
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: 's avatarIgor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#68807}
parent dc82799d
......@@ -7557,6 +7557,7 @@ void CodeStubAssembler::NameDictionaryLookup(
Goto(&loop);
BIND(&loop);
{
Label next_probe(this);
TNode<IntPtrT> entry = var_entry.value();
TNode<IntPtrT> index = EntryToIndex<Dictionary>(entry);
......@@ -7566,13 +7567,18 @@ void CodeStubAssembler::NameDictionaryLookup(
CAST(UnsafeLoadFixedArrayElement(dictionary, index));
GotoIf(TaggedEqual(current, undefined), if_not_found);
if (mode == kFindExisting) {
if (Dictionary::ShapeT::kMatchNeedsHoleCheck) {
GotoIf(TaggedEqual(current, TheHoleConstant()), &next_probe);
}
current = LoadName<Dictionary>(current);
GotoIf(TaggedEqual(current, unique_name), if_found);
} else {
DCHECK_EQ(kFindInsertionIndex, mode);
GotoIf(TaggedEqual(current, TheHoleConstant()), if_not_found);
}
Goto(&next_probe);
BIND(&next_probe);
// See Dictionary::NextProbe().
Increment(&var_count);
entry = Signed(WordAnd(IntPtrAdd(entry, var_count.value()), mask));
......
......@@ -38,6 +38,7 @@ class CompilationCacheShape : public BaseShape<HashTableKey*> {
static const int kPrefixSize = 0;
static const int kEntrySize = 3;
static const bool kMatchNeedsHoleCheck = true;
};
class InfoCellPair {
......
......@@ -205,15 +205,6 @@ PropertyCell GlobalDictionary::CellAt(const Isolate* isolate,
return PropertyCell::cast(KeyAt(isolate, entry));
}
bool GlobalDictionaryShape::IsLive(ReadOnlyRoots roots, Object k) {
DCHECK_NE(roots.the_hole_value(), k);
return k != roots.undefined_value();
}
bool GlobalDictionaryShape::IsKey(ReadOnlyRoots roots, Object k) {
return IsLive(roots, k) && !PropertyCell::cast(k).value().IsTheHole(roots);
}
Name GlobalDictionary::NameAt(InternalIndex entry) {
const Isolate* isolate = GetIsolateForPtrCompr(*this);
return NameAt(isolate, entry);
......@@ -239,6 +230,11 @@ void GlobalDictionary::SetEntry(InternalIndex entry, Object key, Object value,
DetailsAtPut(entry, details);
}
void GlobalDictionary::ClearEntry(InternalIndex entry) {
Object the_hole = this->GetReadOnlyRoots().the_hole_value();
set(EntryToIndex(entry) + kEntryKeyIndex, the_hole);
}
void GlobalDictionary::ValueAtPut(InternalIndex entry, Object value) {
set(EntryToIndex(entry), value);
}
......
......@@ -117,7 +117,7 @@ class NameDictionaryShape : public BaseDictionaryShape<Handle<Name>> {
static const int kPrefixSize = 2;
static const int kEntrySize = 3;
static const int kEntryValueIndex = 1;
static const bool kNeedsHoleCheck = false;
static const bool kMatchNeedsHoleCheck = false;
};
template <typename Derived, typename Shape>
......@@ -212,6 +212,7 @@ class V8_EXPORT_PRIVATE GlobalDictionaryShape : public NameDictionaryShape {
static inline uint32_t HashForObject(ReadOnlyRoots roots, Object object);
static const int kEntrySize = 1; // Overrides NameDictionaryShape::kEntrySize
static const bool kMatchNeedsHoleCheck = true;
template <typename Dictionary>
static inline PropertyDetails DetailsAt(Dictionary dict, InternalIndex entry);
......@@ -221,8 +222,6 @@ class V8_EXPORT_PRIVATE GlobalDictionaryShape : public NameDictionaryShape {
PropertyDetails value);
static inline Object Unwrap(Object key);
static inline bool IsKey(ReadOnlyRoots roots, Object k);
static inline bool IsLive(ReadOnlyRoots roots, Object key);
};
EXTERN_DECLARE_BASE_NAME_DICTIONARY(GlobalDictionary, GlobalDictionaryShape)
......@@ -240,6 +239,7 @@ class V8_EXPORT_PRIVATE GlobalDictionary
inline PropertyCell CellAt(const Isolate* isolate, InternalIndex entry);
inline void SetEntry(InternalIndex entry, Object key, Object value,
PropertyDetails details);
inline void ClearEntry(InternalIndex entry);
inline Name NameAt(InternalIndex entry);
inline Name NameAt(const Isolate* isolate, InternalIndex entry);
inline void ValueAtPut(InternalIndex entry, Object value);
......@@ -258,6 +258,8 @@ class NumberDictionaryBaseShape : public BaseDictionaryShape<uint32_t> {
static inline uint32_t Hash(ReadOnlyRoots roots, uint32_t key);
static inline uint32_t HashForObject(ReadOnlyRoots roots, Object object);
static const bool kMatchNeedsHoleCheck = true;
};
class NumberDictionaryShape : public NumberDictionaryBaseShape {
......
......@@ -147,25 +147,30 @@ InternalIndex HashTable<Derived, Shape>::FindEntry(const LocalIsolate* isolate,
ReadOnlyRoots roots, Key key,
int32_t hash) {
uint32_t capacity = Capacity();
InternalIndex entry = FirstProbe(hash, capacity);
uint32_t count = 1;
// EnsureCapacity will guarantee the hash table is never full.
Object undefined = roots.undefined_value();
Object the_hole = roots.the_hole_value();
USE(the_hole);
while (true) {
// EnsureCapacity will guarantee the hash table is never full.
for (InternalIndex entry = FirstProbe(hash, capacity);;
entry = NextProbe(entry, count++, capacity)) {
Object element = KeyAt(isolate, entry);
// Empty entry. Uses raw unchecked accessors because it is called by the
// string table during bootstrapping.
if (element == undefined) break;
if (!(Shape::kNeedsHoleCheck && the_hole == element)) {
if (Shape::IsMatch(key, element)) return entry;
}
entry = NextProbe(entry, count++, capacity);
if (Shape::kMatchNeedsHoleCheck && element == the_hole) continue;
if (Shape::IsMatch(key, element)) return entry;
}
return InternalIndex::NotFound();
}
// static
template <typename Derived, typename Shape>
bool HashTable<Derived, Shape>::IsKey(ReadOnlyRoots roots, Object k) {
// TODO(leszeks): Dictionaries that don't delete could skip the hole check.
return k != roots.undefined_value() && k != roots.the_hole_value();
}
template <typename Derived, typename Shape>
bool HashTable<Derived, Shape>::ToKey(ReadOnlyRoots roots, InternalIndex entry,
Object* out_k) {
......@@ -221,16 +226,6 @@ void HashTable<Derived, Shape>::SetCapacity(int capacity) {
set(kCapacityIndex, Smi::FromInt(capacity));
}
template <typename KeyT>
bool BaseShape<KeyT>::IsKey(ReadOnlyRoots roots, Object key) {
return IsLive(roots, key);
}
template <typename KeyT>
bool BaseShape<KeyT>::IsLive(ReadOnlyRoots roots, Object k) {
return k != roots.the_hole_value() && k != roots.undefined_value();
}
bool ObjectHashSet::Has(Isolate* isolate, Handle<Object> key, int32_t hash) {
return FindEntry(isolate, ReadOnlyRoots(isolate), key, hash).is_found();
}
......
......@@ -50,7 +50,7 @@ namespace internal {
// static const int kEntrySize = ..;
// // Indicates whether IsMatch can deal with other being the_hole (a
// // deleted entry).
// static const bool kNeedsHoleCheck = ..;
// static const bool kMatchNeedsHoleCheck = ..;
// };
// The prefix size indicates an amount of memory in the
// beginning of the backing storage that can be used for non-element
......@@ -60,10 +60,7 @@ template <typename KeyT>
class V8_EXPORT_PRIVATE BaseShape {
public:
using Key = KeyT;
static const bool kNeedsHoleCheck = true;
static Object Unwrap(Object key) { return key; }
static inline bool IsKey(ReadOnlyRoots roots, Object key);
static inline bool IsLive(ReadOnlyRoots roots, Object key);
};
class V8_EXPORT_PRIVATE HashTableBase : public NON_EXPORTED_BASE(FixedArray) {
......@@ -152,9 +149,7 @@ class EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE) HashTable
// Returns whether k is a real key. The hole and undefined are not allowed as
// keys and can be used to indicate missing or deleted elements.
static bool IsKey(ReadOnlyRoots roots, Object k) {
return Shape::IsKey(roots, k);
}
static inline bool IsKey(ReadOnlyRoots roots, Object k);
inline bool ToKey(ReadOnlyRoots roots, InternalIndex entry, Object* out_k);
inline bool ToKey(const Isolate* isolate, InternalIndex entry, Object* out_k);
......@@ -303,7 +298,7 @@ class ObjectHashTableShape : public BaseShape<Handle<Object>> {
static const int kPrefixSize = 0;
static const int kEntryValueIndex = 1;
static const int kEntrySize = 2;
static const bool kNeedsHoleCheck = false;
static const bool kMatchNeedsHoleCheck = false;
};
template <typename Derived, typename Shape>
......
......@@ -705,14 +705,18 @@ void JSReceiver::DeleteNormalizedProperty(Handle<JSReceiver> object,
DCHECK(entry.is_found());
if (object->IsJSGlobalObject()) {
// If we have a global object, invalidate the cell and swap in a new one.
// If we have a global object, invalidate the cell and remove it from the
// global object's dictionary.
Handle<GlobalDictionary> dictionary(
JSGlobalObject::cast(*object).global_dictionary(), isolate);
auto cell = PropertyCell::InvalidateEntry(isolate, dictionary, entry);
cell->set_value(ReadOnlyRoots(isolate).the_hole_value());
cell->set_property_details(
PropertyDetails::Empty(PropertyCellType::kUninitialized));
Handle<PropertyCell> cell(dictionary->CellAt(entry), isolate);
Handle<GlobalDictionary> new_dictionary =
GlobalDictionary::DeleteEntry(isolate, dictionary, entry);
JSGlobalObject::cast(*object).set_global_dictionary(*new_dictionary);
cell->ClearAndInvalidate(ReadOnlyRoots(isolate));
} else {
Handle<NameDictionary> dictionary(object->property_dictionary(), isolate);
......@@ -5675,38 +5679,8 @@ void JSGlobalObject::InvalidatePropertyCell(Handle<JSGlobalObject> global,
auto dictionary = handle(global->global_dictionary(), global->GetIsolate());
InternalIndex entry = dictionary->FindEntry(global->GetIsolate(), name);
if (entry.is_not_found()) return;
PropertyCell::InvalidateEntry(global->GetIsolate(), dictionary, entry);
}
Handle<PropertyCell> JSGlobalObject::EnsureEmptyPropertyCell(
Handle<JSGlobalObject> global, Handle<Name> name,
PropertyCellType cell_type, InternalIndex* entry_out) {
Isolate* isolate = global->GetIsolate();
DCHECK(!global->HasFastProperties());
Handle<GlobalDictionary> dictionary(global->global_dictionary(), isolate);
InternalIndex entry = dictionary->FindEntry(isolate, name);
Handle<PropertyCell> cell;
if (entry.is_found()) {
if (entry_out) *entry_out = entry;
cell = handle(dictionary->CellAt(entry), isolate);
PropertyCellType original_cell_type = cell->property_details().cell_type();
DCHECK(original_cell_type == PropertyCellType::kInvalidated ||
original_cell_type == PropertyCellType::kUninitialized);
DCHECK(cell->value().IsTheHole(isolate));
if (original_cell_type == PropertyCellType::kInvalidated) {
cell = PropertyCell::InvalidateEntry(isolate, dictionary, entry);
}
PropertyDetails details(kData, NONE, cell_type);
cell->set_property_details(details);
return cell;
}
cell = isolate->factory()->NewPropertyCell(name);
PropertyDetails details(kData, NONE, cell_type);
dictionary = GlobalDictionary::Add(isolate, dictionary, name, cell, details,
entry_out);
// {*entry_out} is initialized inside GlobalDictionary::Add().
global->SetProperties(*dictionary);
return cell;
PropertyCell::InvalidateAndReplaceEntry(global->GetIsolate(), dictionary,
entry);
}
// static
......
......@@ -1232,10 +1232,6 @@ class JSGlobalObject : public JSSpecialObject {
static void InvalidatePropertyCell(Handle<JSGlobalObject> object,
Handle<Name> name);
// Ensure that the global object has a cell for the given property name.
static Handle<PropertyCell> EnsureEmptyPropertyCell(
Handle<JSGlobalObject> global, Handle<Name> name,
PropertyCellType cell_type, InternalIndex* entry_out = nullptr);
DECL_CAST(JSGlobalObject)
......
......@@ -552,23 +552,23 @@ void LookupIterator::PrepareTransitionToDataProperty(
if (map->IsJSGlobalObjectMap()) {
// Install a property cell.
Handle<JSGlobalObject> global = Handle<JSGlobalObject>::cast(receiver);
Handle<PropertyCell> cell = JSGlobalObject::EnsureEmptyPropertyCell(
global, name(), PropertyCellType::kUninitialized, &number_);
DCHECK(!global->HasFastProperties());
Handle<GlobalDictionary> dictionary(global->global_dictionary(isolate_),
isolate_);
Handle<PropertyCell> cell = isolate_->factory()->NewPropertyCell(name());
DCHECK(cell->value(isolate_).IsTheHole(isolate_));
DCHECK(!value->IsTheHole(isolate_));
transition_ = cell;
// Assign an enumeration index to the property and update
// SetNextEnumerationIndex.
int index = GlobalDictionary::NextEnumerationIndex(isolate_, dictionary);
dictionary->set_next_enumeration_index(index + 1);
property_details_ = PropertyDetails(
kData, attributes, PropertyCellType::kUninitialized, index);
PropertyCellType new_type =
PropertyCell::UpdatedType(isolate(), cell, value, property_details_);
property_details_ = property_details_.set_cell_type(new_type);
cell->set_property_details(property_details_);
kData, attributes,
PropertyCell::TypeForUninitializedCell(isolate_, value));
dictionary = GlobalDictionary::Add(isolate_, dictionary, name(), cell,
property_details_, &number_);
global->set_global_dictionary(*dictionary);
transition_ = cell;
has_property_ = true;
} else {
// Don't set enumeration index (it will be set during value store).
......
......@@ -6668,7 +6668,7 @@ void HashTable<Derived, Shape>::Rehash(const Isolate* isolate,
for (InternalIndex i : this->IterateEntries()) {
uint32_t from_index = EntryToIndex(i);
Object k = this->get(isolate, from_index);
if (!Shape::IsLive(roots, k)) continue;
if (!IsKey(roots, k)) continue;
uint32_t hash = Shape::HashForObject(roots, k);
uint32_t insertion_index =
EntryToIndex(new_table.FindInsertionEntry(isolate, roots, hash));
......@@ -6729,7 +6729,7 @@ void HashTable<Derived, Shape>::Rehash(const Isolate* isolate) {
for (InternalIndex current(0); current.raw_value() < capacity;
/* {current} is advanced manually below, when appropriate.*/) {
Object current_key = KeyAt(isolate, current);
if (!Shape::IsLive(roots, current_key)) {
if (!IsKey(roots, current_key)) {
++current; // Advance to next entry.
continue;
}
......@@ -6739,7 +6739,7 @@ void HashTable<Derived, Shape>::Rehash(const Isolate* isolate) {
continue;
}
Object target_key = KeyAt(isolate, target);
if (!Shape::IsLive(roots, target_key) ||
if (!IsKey(roots, target_key) ||
EntryForProbe(roots, target_key, probe, target) != target) {
// Put the current element into the correct position.
Swap(current, target, mode);
......@@ -6839,14 +6839,12 @@ template <typename Derived, typename Shape>
InternalIndex HashTable<Derived, Shape>::FindInsertionEntry(
const Isolate* isolate, ReadOnlyRoots roots, uint32_t hash) {
uint32_t capacity = Capacity();
InternalIndex entry = FirstProbe(hash, capacity);
uint32_t count = 1;
// EnsureCapacity will guarantee the hash table is never full.
while (true) {
if (!Shape::IsLive(roots, KeyAt(isolate, entry))) break;
entry = NextProbe(entry, count++, capacity);
for (InternalIndex entry = FirstProbe(hash, capacity);;
entry = NextProbe(entry, count++, capacity)) {
if (!IsKey(roots, KeyAt(isolate, entry))) return entry;
}
return entry;
}
template <typename Derived, typename Shape>
......@@ -8015,32 +8013,36 @@ Handle<JSArray> JSWeakCollection::GetEntries(Handle<JSWeakCollection> holder,
return isolate->factory()->NewJSArrayWithElements(entries);
}
Handle<PropertyCell> PropertyCell::InvalidateEntry(
void PropertyCell::ClearAndInvalidate(ReadOnlyRoots roots) {
// Cell is officially mutable henceforth.
DCHECK(!value().IsTheHole(roots));
PropertyDetails details = property_details();
details = details.set_cell_type(PropertyCellType::kInvalidated);
set_value(roots.the_hole_value());
set_property_details(details);
dependent_code().DeoptimizeDependentCodeGroup(
DependentCode::kPropertyCellChangedGroup);
}
// static
Handle<PropertyCell> PropertyCell::InvalidateAndReplaceEntry(
Isolate* isolate, Handle<GlobalDictionary> dictionary,
InternalIndex entry) {
// Swap with a copy.
Handle<PropertyCell> cell(dictionary->CellAt(entry), isolate);
Handle<Name> name(cell->name(), isolate);
PropertyDetails details = cell->property_details();
DCHECK(details.IsConfigurable());
DCHECK(!cell->value().IsTheHole(isolate));
// Swap with a copy.
Handle<PropertyCell> new_cell = isolate->factory()->NewPropertyCell(name);
new_cell->set_value(cell->value());
dictionary->ValueAtPut(entry, *new_cell);
bool is_the_hole = cell->value().IsTheHole(isolate);
// Cell is officially mutable henceforth.
PropertyDetails details = cell->property_details();
DCHECK(details.IsConfigurable());
details = details.set_cell_type(is_the_hole ? PropertyCellType::kUninitialized
: PropertyCellType::kMutable);
details = details.set_cell_type(PropertyCellType::kMutable);
new_cell->set_property_details(details);
// Old cell is ready for invalidation.
if (is_the_hole) {
cell->set_value(ReadOnlyRoots(isolate).undefined_value());
} else {
cell->set_value(ReadOnlyRoots(isolate).the_hole_value());
}
details = details.set_cell_type(PropertyCellType::kInvalidated);
cell->set_property_details(details);
cell->dependent_code().DeoptimizeDependentCodeGroup(
DependentCode::kPropertyCellChangedGroup);
dictionary->ValueAtPut(entry, *new_cell);
cell->ClearAndInvalidate(ReadOnlyRoots(isolate));
return new_cell;
}
......@@ -8062,6 +8064,14 @@ static bool RemainsConstantType(Handle<PropertyCell> cell,
return false;
}
// static
PropertyCellType PropertyCell::TypeForUninitializedCell(Isolate* isolate,
Handle<Object> value) {
if (value->IsUndefined(isolate)) return PropertyCellType::kUndefined;
return PropertyCellType::kConstant;
}
// static
PropertyCellType PropertyCell::UpdatedType(Isolate* isolate,
Handle<PropertyCell> cell,
Handle<Object> value,
......@@ -8072,8 +8082,7 @@ PropertyCellType PropertyCell::UpdatedType(Isolate* isolate,
switch (type) {
// Only allow a cell to transition once into constant state.
case PropertyCellType::kUninitialized:
if (value->IsUndefined(isolate)) return PropertyCellType::kUndefined;
return PropertyCellType::kConstant;
return TypeForUninitializedCell(isolate, value);
case PropertyCellType::kInvalidated:
return PropertyCellType::kMutable;
default:
......@@ -8122,7 +8131,7 @@ Handle<PropertyCell> PropertyCell::PrepareForValue(
PropertyCellType new_type =
UpdatedType(isolate, cell, value, original_details);
if (invalidate) {
cell = PropertyCell::InvalidateEntry(isolate, dictionary, entry);
cell = PropertyCell::InvalidateAndReplaceEntry(isolate, dictionary, entry);
}
// Install new property details.
......
......@@ -31,12 +31,17 @@ class PropertyCell : public HeapObject {
PropertyCellConstantType GetConstantType();
// Computes the new type of a previously uninitialized cell for the given
// value.
static PropertyCellType TypeForUninitializedCell(Isolate* isolate,
Handle<Object> value);
// Computes the new type of the cell's contents for the given value, but
// without actually modifying the details.
static PropertyCellType UpdatedType(Isolate* isolate,
Handle<PropertyCell> cell,
Handle<Object> value,
PropertyDetails details);
// Prepares property cell at given entry for receiving given value.
// As a result the old cell could be invalidated and/or dependent code could
// be deoptimized. Returns the prepared property cell.
......@@ -44,7 +49,8 @@ class PropertyCell : public HeapObject {
Isolate* isolate, Handle<GlobalDictionary> dictionary,
InternalIndex entry, Handle<Object> value, PropertyDetails details);
static Handle<PropertyCell> InvalidateEntry(
void ClearAndInvalidate(ReadOnlyRoots roots);
static Handle<PropertyCell> InvalidateAndReplaceEntry(
Isolate* isolate, Handle<GlobalDictionary> dictionary,
InternalIndex entry);
......
......@@ -49,6 +49,7 @@ class V8_EXPORT_PRIVATE StringTableShape : public BaseShape<StringTableKey*> {
static const int kPrefixSize = 0;
static const int kEntrySize = 1;
static const bool kMatchNeedsHoleCheck = true;
};
class SeqOneByteString;
......@@ -105,6 +106,7 @@ class StringSetShape : public BaseShape<String> {
static const int kPrefixSize = 0;
static const int kEntrySize = 1;
static const bool kMatchNeedsHoleCheck = true;
};
EXTERN_DECLARE_HASH_TABLE(StringSet, StringSetShape)
......
......@@ -79,7 +79,7 @@ RUNTIME_FUNCTION(Runtime_WeakCollectionDelete) {
#ifdef DEBUG
DCHECK(key->IsJSReceiver());
DCHECK(EphemeronHashTable::ShapeT::IsLive(ReadOnlyRoots(isolate), *key));
DCHECK(EphemeronHashTable::IsKey(ReadOnlyRoots(isolate), *key));
Handle<EphemeronHashTable> table(
EphemeronHashTable::cast(weak_collection->table()), isolate);
// Should only be called when shrinking the table is necessary. See
......@@ -102,7 +102,7 @@ RUNTIME_FUNCTION(Runtime_WeakCollectionSet) {
#ifdef DEBUG
DCHECK(key->IsJSReceiver());
DCHECK(EphemeronHashTable::ShapeT::IsLive(ReadOnlyRoots(isolate), *key));
DCHECK(EphemeronHashTable::IsKey(ReadOnlyRoots(isolate), *key));
Handle<EphemeronHashTable> table(
EphemeronHashTable::cast(weak_collection->table()), isolate);
// Should only be called when rehashing or resizing the table is necessary.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment