[dict-proto] C++ implementation of SwissNameDictionary, pt. 10
This CL is part of a series that adds the C++ implementation of SwissNameDictionary, a deterministic property backing store based on Swiss Tables. This CL adds the actual tests for SwissNameDictionary, defined in test-swiss-name-dictionary-shared-tests.h, using the infrastructure in test-swiss-name-dictionary-infra.[h|cc]. Bug: v8:11388 Change-Id: I5d91cede4f74b85a4101c5f2de3deda01a72edb2 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2744138 Reviewed-by: Igor Sheludko <ishell@chromium.org> Reviewed-by: Marja Hölttä <marja@chromium.org> Commit-Queue: Frank Emrich <emrich@google.com> Cr-Commit-Position: refs/heads/master@{#73572}
This commit is contained in:
parent
907aa27db0
commit
8e6047e51d
@ -89,6 +89,19 @@ class JSHeapBroker;
|
||||
static void Test##Name()
|
||||
#endif
|
||||
|
||||
// Similar to TEST, but used when test definitions appear as members of a
|
||||
// (probably parameterized) class. This allows re-using the given tests multiple
|
||||
// times. For this to work, the following conditions must hold:
|
||||
// 1. The class has a template parameter named kTestFileName of type char
|
||||
// const*, which is instantiated with __FILE__ at the *use site*, in order
|
||||
// to correctly associate the tests with the test suite using them.
|
||||
// 2. To actually execute the tests, create an instance of the class
|
||||
// containing the MEMBER_TESTs.
|
||||
#define MEMBER_TEST(Name) \
|
||||
CcTest register_test_##Name = \
|
||||
CcTest(Test##Name, kTestFileName, #Name, true, true); \
|
||||
static void Test##Name()
|
||||
|
||||
#define EXTENSION_LIST(V) \
|
||||
V(GC_EXTENSION, "v8/gc") \
|
||||
V(PRINT_EXTENSION, "v8/print") \
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "test/cctest/test-swiss-name-dictionary-infra.h"
|
||||
#include "test/cctest/test-swiss-name-dictionary-shared-tests.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
@ -312,6 +313,13 @@ void CSATestRunner::CheckAgainstReference() {
|
||||
CHECK(table->EqualsForTesting(*reference_));
|
||||
}
|
||||
|
||||
// Executes the tests defined in test-swiss-name-dictionary-shared-tests.h as if
|
||||
// they were defined in this file, using the CSATestRunner. See comments in
|
||||
// test-swiss-name-dictionary-shared-tests.h and in
|
||||
// swiss-name-dictionary-infra.h for details.
|
||||
const char kCSATestFileName[] = __FILE__;
|
||||
SharedSwissTableTests<CSATestRunner, kCSATestFileName> execute_shared_tests_csa;
|
||||
|
||||
} // namespace test_swiss_hash_table
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
@ -76,6 +76,13 @@ Handle<Name> CreateKeyWithHash(Isolate* isolate, KeyCache& keys,
|
||||
int fake_hash = actual_hash;
|
||||
if (key.h1_override) {
|
||||
uint32_t override_with = key.h1_override.value().value;
|
||||
|
||||
// We cannot override h1 with 0 unless we also override h2 with a
|
||||
// non-zero value. Otherwise, the overall hash may become 0 (which is
|
||||
// forbidden) based on the (nondeterminstic) choice of h2.
|
||||
CHECK_IMPLIES(override_with == 0,
|
||||
key.h2_override && key.h2_override.value().value != 0);
|
||||
|
||||
fake_hash = (override_with << swiss_table::kH2Bits) |
|
||||
swiss_table::H2(actual_hash);
|
||||
}
|
||||
@ -83,8 +90,14 @@ Handle<Name> CreateKeyWithHash(Isolate* isolate, KeyCache& keys,
|
||||
// Unset 7 bits belonging to H2:
|
||||
fake_hash &= ~((1 << swiss_table::kH2Bits) - 1);
|
||||
|
||||
DCHECK_LT(key.h2_override.value().value, 1 << swiss_table::kH2Bits);
|
||||
fake_hash |= swiss_table::H2(key.h2_override.value().value);
|
||||
uint8_t override_with = key.h2_override.value().value;
|
||||
|
||||
// Same as above, but for h2: Prevent accidentally creating 0 fake hash.
|
||||
CHECK_IMPLIES(override_with == 0,
|
||||
key.h1_override && key.h1_override.value().value != 0);
|
||||
|
||||
CHECK_LT(key.h2_override.value().value, 1 << swiss_table::kH2Bits);
|
||||
fake_hash |= swiss_table::H2(override_with);
|
||||
}
|
||||
|
||||
// Ensure that just doing a shift below is correct.
|
||||
@ -96,9 +109,10 @@ Handle<Name> CreateKeyWithHash(Isolate* isolate, KeyCache& keys,
|
||||
|
||||
// Prepare what to put into the hash field.
|
||||
uint32_t hash_field = fake_hash << Name::kHashShift;
|
||||
CHECK_NE(hash_field, 0);
|
||||
|
||||
key_symbol->set_raw_hash_field(hash_field);
|
||||
DCHECK_EQ(fake_hash, key_symbol->hash());
|
||||
CHECK_EQ(fake_hash, key_symbol->hash());
|
||||
}
|
||||
|
||||
return key_symbol;
|
||||
@ -110,8 +124,8 @@ Handle<Name> CreateKeyWithHash(Isolate* isolate, KeyCache& keys,
|
||||
// else w.r.t. hash faking when using this key before. If so, the test case
|
||||
// would make inconsistent assumptions about how the hashes should be faked
|
||||
// and be broken.
|
||||
DCHECK_EQ(cached_info.h1_override, key.h1_override);
|
||||
DCHECK_EQ(cached_info.h2_override, key.h2_override);
|
||||
CHECK_EQ(cached_info.h1_override, key.h1_override);
|
||||
CHECK_EQ(cached_info.h2_override, key.h2_override);
|
||||
|
||||
return cached_info.key_symbol;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ using IndexOpt = base::Optional<InternalIndex>;
|
||||
static const ValueOpt kNoValue;
|
||||
static const PropertyDetailsOpt kNoDetails;
|
||||
static const base::Optional<int> kNoInt;
|
||||
static const IndexOpt kNoIndex;
|
||||
static const IndexOpt kIndexUnknown;
|
||||
|
||||
static const std::vector<int> interesting_initial_capacities = {
|
||||
4,
|
||||
|
858
test/cctest/test-swiss-name-dictionary-shared-tests.h
Normal file
858
test/cctest/test-swiss-name-dictionary-shared-tests.h
Normal file
@ -0,0 +1,858 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
#ifndef V8_TEST_CCTEST_TEST_SWISS_HASH_TABLE_SHARED_TESTS_H_
|
||||
#define V8_TEST_CCTEST_TEST_SWISS_HASH_TABLE_SHARED_TESTS_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "test/cctest/test-swiss-name-dictionary-infra.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
namespace test_swiss_hash_table {
|
||||
|
||||
// The name of the test-*.cc file that executes the tests below with the
|
||||
// RuntimeTestRunner.
|
||||
extern const char kRuntimeTestFileName[];
|
||||
|
||||
// The name of the test-*.cc file that executes the tests below with the
|
||||
// CSATestRunner.
|
||||
extern const char kCSATestFileName[];
|
||||
|
||||
// This class contains test cases for SwissNameDictionary that can be executed
|
||||
// by different "test runners", which are supplied as a template parameter. The
|
||||
// TestRunner determines how the operations on dictionaries are actually
|
||||
// executed. Currently there are two TestRunners: RuntimeTestRunner calls C++
|
||||
// functions, whereas CSATestRunner executes dictionary operations by executing
|
||||
// CSA-generated code.
|
||||
// To execute the tests, just create an instance of the class below with an
|
||||
// appropriate TestRunner.
|
||||
// Whenever creating an instance of this class in a file bar.cc, the template
|
||||
// parameter |kTestFileName| should be set to the name of the file that
|
||||
// *instantiates the class* (i.e., "bar.cc"). This ensures that the tests
|
||||
// defined below are then registred within the overall cctest machinery as if
|
||||
// they were directly written within bar.cc.
|
||||
template <typename TestRunner, char const* kTestFileName>
|
||||
struct SharedSwissTableTests {
|
||||
STATIC_ASSERT((std::is_same<TestRunner, RuntimeTestRunner>::value) ||
|
||||
(std::is_same<TestRunner, CSATestRunner>::value));
|
||||
|
||||
SharedSwissTableTests() {
|
||||
CHECK(kTestFileName == kRuntimeTestFileName ||
|
||||
kTestFileName == kCSATestFileName);
|
||||
}
|
||||
|
||||
using TS = TestSequence<TestRunner>;
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
// We add this value when we want to create fake H1 values to prevent us from
|
||||
// accidentally creating an overall hash of 0, which is forbidden. Due to all
|
||||
// H1 values are used modulo the capacity of the table, this has no further
|
||||
// effects. Note that using just this value itself as an H1 value means that a
|
||||
// key will (try to) occupy bucket 0.
|
||||
static const int kBigModulus = (1 << 22);
|
||||
STATIC_ASSERT(SwissNameDictionary::IsValidCapacity(kBigModulus));
|
||||
|
||||
// Returns elements from TS::distinct_property_details in a determinstic
|
||||
// order. Subsequent calls with increasing |index| (and the same |offset|)
|
||||
// will return pairwise different values until |index| has risen by more than
|
||||
// {TS::distinct_property_details.size()}.
|
||||
static PropertyDetails distinct_details(int index, int offset = 0) {
|
||||
int size = static_cast<int>(distinct_property_details.size());
|
||||
return distinct_property_details[(index + offset) % size];
|
||||
}
|
||||
|
||||
// Adds elements at the boundaries of the table, e.g. to buckets 0, 1,
|
||||
// Capacity() - 2, and Capacity() - 1. (But only three of those if the table
|
||||
// can't hold 4 elements without resizing).
|
||||
static void AddAtBoundaries(TS& s) {
|
||||
int capacity = s.initial_capacity;
|
||||
std::vector<int> interesting_indices = s.boundary_indices(capacity);
|
||||
|
||||
s.CheckCounts(capacity, 0, 0);
|
||||
|
||||
int count = 0;
|
||||
for (int index : interesting_indices) {
|
||||
std::string key = "k" + std::to_string(index);
|
||||
std::string value = "v" + std::to_string(index);
|
||||
PropertyDetails details = distinct_details(count++);
|
||||
s.Add(Key{key, FakeH1{index + kBigModulus}}, value, details);
|
||||
}
|
||||
|
||||
// We didn't want to cause a resize:
|
||||
s.CheckCounts(capacity);
|
||||
}
|
||||
|
||||
// Adds |count| entries to the table, using their unmodified hashes, of the
|
||||
// form key_i -> (value_i, details_i), where key_i and value_i are build from
|
||||
// appending the actual index (e.g., 0, ...., counts - 1) to |key_prefix| and
|
||||
// |value_prefix|, respectively. The property details are taken from
|
||||
// |distinct_property_details|.
|
||||
static void AddMultiple(TS& s, int count, std::string key_prefix = "key",
|
||||
std::string value_prefix = "value",
|
||||
int details_offset = 0) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::string key = key_prefix + std::to_string(i);
|
||||
std::string value = value_prefix + std::to_string(i);
|
||||
PropertyDetails d = distinct_details(i);
|
||||
s.Add(Key{key}, value, d);
|
||||
}
|
||||
}
|
||||
|
||||
// Checks that |count| entries exist, as they would have been added by a call
|
||||
// to AddMultiple with the same arguments.
|
||||
static void CheckMultiple(TS& s, int count, std::string key_prefix = "key",
|
||||
std::string value_prefix = "value",
|
||||
int details_offset = 0) {
|
||||
DCHECK_LE(count,
|
||||
SwissNameDictionary::MaxUsableCapacity(s.initial_capacity));
|
||||
|
||||
std::vector<std::string> expected_keys;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::string key = key_prefix + std::to_string(i);
|
||||
expected_keys.push_back(key);
|
||||
std::string value = value_prefix + std::to_string(i);
|
||||
int details_index =
|
||||
(details_offset + i) % distinct_property_details.size();
|
||||
PropertyDetails d = distinct_property_details[details_index];
|
||||
s.CheckDataAtKey(Key{key}, kIndexUnknown, value, d);
|
||||
}
|
||||
s.CheckEnumerationOrder(expected_keys);
|
||||
}
|
||||
|
||||
//
|
||||
// Start of actual tests.
|
||||
//
|
||||
|
||||
MEMBER_TEST(Allocation) {
|
||||
TS::WithAllInterestingInitialCapacities([](TS& s) {
|
||||
// The test runner does the allocation automatically.
|
||||
s.CheckCounts(s.initial_capacity, 0, 0);
|
||||
s.VerifyHeap();
|
||||
});
|
||||
}
|
||||
|
||||
// Simple test for adding entries. Also uses non-Symbol keys and non-String
|
||||
// values, which is not supported by the higher-level testing infrastructure.
|
||||
MEMBER_TEST(SimpleAdd) {
|
||||
TS::WithInitialCapacity(4, [](TS& s) {
|
||||
Handle<String> key1 = s.isolate->factory()->InternalizeUtf8String("foo");
|
||||
Handle<String> value1 =
|
||||
s.isolate->factory()->InternalizeUtf8String("bar");
|
||||
PropertyDetails details1 =
|
||||
PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE,
|
||||
PropertyCellType::kNoCell);
|
||||
|
||||
s.CheckCounts(4, 0, 0);
|
||||
s.CheckKeyAbsent(key1);
|
||||
|
||||
s.Add(key1, value1, details1);
|
||||
s.CheckDataAtKey(key1, kIndexUnknown, value1, details1);
|
||||
s.CheckCounts(4, 1, 0);
|
||||
|
||||
Handle<Symbol> key2 = s.isolate->factory()->NewSymbol();
|
||||
Handle<Smi> value2 = handle(Smi::FromInt(123), s.isolate);
|
||||
PropertyDetails details2 =
|
||||
PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE,
|
||||
PropertyCellType::kNoCell);
|
||||
|
||||
s.CheckKeyAbsent(key2);
|
||||
s.Add(key2, value2, details2);
|
||||
s.CheckDataAtKey(key2, kIndexUnknown, value2, details2);
|
||||
s.CheckCounts(4, 2, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Simple test for updating existing entries. Also uses non-Symbol keys and
|
||||
// non-String values, which is not supported by the higher-level testing
|
||||
// infrastructure.
|
||||
MEMBER_TEST(SimpleUpdate) {
|
||||
TS::WithInitialCapacity(4, [](TS& s) {
|
||||
Handle<String> key1 = s.isolate->factory()->InternalizeUtf8String("foo");
|
||||
Handle<String> value1 =
|
||||
s.isolate->factory()->InternalizeUtf8String("bar");
|
||||
PropertyDetails details1 =
|
||||
PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE,
|
||||
PropertyCellType::kNoCell);
|
||||
|
||||
s.Add(key1, value1, details1);
|
||||
|
||||
Handle<Symbol> key2 = s.isolate->factory()->NewSymbol();
|
||||
Handle<Smi> value2 = handle(Smi::FromInt(123), s.isolate);
|
||||
PropertyDetails details2 =
|
||||
PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE,
|
||||
PropertyCellType::kNoCell);
|
||||
|
||||
s.Add(key2, value2, details2);
|
||||
|
||||
// Until here same operations as in Test "Add".
|
||||
|
||||
Handle<Smi> value1_updated = handle(Smi::FromInt(456), s.isolate);
|
||||
Handle<String> value2_updated =
|
||||
s.isolate->factory()->InternalizeUtf8String("updated");
|
||||
PropertyDetails details1_updated = details2;
|
||||
PropertyDetails details2_updated = details1;
|
||||
|
||||
s.UpdateByKey(key1, value1_updated, details1_updated);
|
||||
s.CheckDataAtKey(key1, kIndexUnknown, value1_updated, details1_updated);
|
||||
s.CheckDataAtKey(key2, kIndexUnknown, value2, details2);
|
||||
|
||||
s.UpdateByKey(key2, value2_updated, details2_updated);
|
||||
s.CheckDataAtKey(key1, kIndexUnknown, value1_updated, details1_updated);
|
||||
s.CheckDataAtKey(key2, kIndexUnknown, value2_updated, details2_updated);
|
||||
s.CheckCounts(4, 2, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Simple test for deleting existing entries. Also uses non-Symbol keys and
|
||||
// non-String values, which is not supported by the higher-level testing
|
||||
// infrastructure.
|
||||
MEMBER_TEST(SimpleDelete) {
|
||||
TS::WithInitialCapacity(4, [](TS& s) {
|
||||
Handle<String> key1 = s.isolate->factory()->InternalizeUtf8String("foo");
|
||||
Handle<String> value1 =
|
||||
s.isolate->factory()->InternalizeUtf8String("bar");
|
||||
PropertyDetails details1 =
|
||||
PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE,
|
||||
PropertyCellType::kNoCell);
|
||||
|
||||
s.Add(key1, value1, details1);
|
||||
|
||||
Handle<Symbol> key2 = s.isolate->factory()->NewSymbol();
|
||||
Handle<Smi> value2 = handle(Smi::FromInt(123), s.isolate);
|
||||
PropertyDetails details2 =
|
||||
PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE,
|
||||
PropertyCellType::kNoCell);
|
||||
|
||||
s.Add(key2, value2, details2);
|
||||
|
||||
// Until here same operations as in Test "Add".
|
||||
|
||||
s.DeleteByKey(key1);
|
||||
s.CheckKeyAbsent(key1);
|
||||
s.CheckDataAtKey(key2, kIndexUnknown, value2, details2);
|
||||
s.CheckCounts(4, 1, 1);
|
||||
|
||||
s.DeleteByKey(key2);
|
||||
s.CheckKeyAbsent(key1);
|
||||
s.CheckKeyAbsent(key2);
|
||||
s.CheckCounts(4, 0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Adds entries that occuppy the boundaries (first and last
|
||||
// buckets) of the hash table.
|
||||
MEMBER_TEST(AddAtBoundaries) {
|
||||
TS::WithAllInterestingInitialCapacities([](TS& s) {
|
||||
AddAtBoundaries(s);
|
||||
|
||||
int capacity = s.initial_capacity;
|
||||
|
||||
std::vector<int> boundary_indices = s.boundary_indices(capacity);
|
||||
int size = static_cast<int>(boundary_indices.size());
|
||||
|
||||
int count = 0;
|
||||
for (int index : boundary_indices) {
|
||||
std::string key = "k" + std::to_string(index);
|
||||
std::string value = "v" + std::to_string(index);
|
||||
PropertyDetails details = distinct_details(count++);
|
||||
|
||||
s.CheckDataAtKey(Key{key, FakeH1{index + kBigModulus}},
|
||||
InternalIndex(index), value, details);
|
||||
}
|
||||
s.CheckCounts(capacity, size, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Adds entries that occuppy the boundaries of the hash table, then updates
|
||||
// their values and property details.
|
||||
MEMBER_TEST(UpdateAtBoundaries) {
|
||||
TS::WithAllInterestingInitialCapacities([](TS& s) {
|
||||
AddAtBoundaries(s);
|
||||
|
||||
int capacity = s.initial_capacity;
|
||||
|
||||
std::vector<int> boundary_indices = s.boundary_indices(capacity);
|
||||
int size = static_cast<int>(boundary_indices.size());
|
||||
|
||||
int count = 0;
|
||||
for (int index : boundary_indices) {
|
||||
std::string key = "k" + std::to_string(index);
|
||||
std::string value = "newv" + std::to_string(index);
|
||||
// setting offset means getting other PropertyDetails than before
|
||||
PropertyDetails details = distinct_details(count++, size);
|
||||
|
||||
s.UpdateByKey(Key{key, FakeH1{index + kBigModulus}}, value, details);
|
||||
}
|
||||
|
||||
count = 0;
|
||||
for (int index : boundary_indices) {
|
||||
std::string key = "k" + std::to_string(index);
|
||||
std::string value = "newv" + std::to_string(index);
|
||||
PropertyDetails details = distinct_details(count++, size);
|
||||
|
||||
s.CheckDataAtKey(Key{key, FakeH1{index + kBigModulus}},
|
||||
InternalIndex(index), value, details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Adds entries that occuppy the boundaries of the hash table, then updates
|
||||
// their values and property details.
|
||||
MEMBER_TEST(DeleteAtBoundaries) {
|
||||
// The maximum value of {TS::boundary_indices(capacity).size()} for any
|
||||
// |capacity|.
|
||||
int count = 4;
|
||||
|
||||
// Due to shrink-on-delete, we create a new dictionary prior to each
|
||||
// deletion, so that we don't re-hash (which would defeat the purpose of
|
||||
// this test).
|
||||
for (int i = 0; i < count; ++i) {
|
||||
// In this iteration, we delete the i-th element of |boundary_indices|.
|
||||
|
||||
TS::WithAllInterestingInitialCapacities([&](TS& s) {
|
||||
std::vector<int> boundary_indices =
|
||||
TS::boundary_indices(s.initial_capacity);
|
||||
int number_of_entries = static_cast<int>(boundary_indices.size());
|
||||
DCHECK_GE(count, number_of_entries);
|
||||
|
||||
if (i >= static_cast<int>(boundary_indices.size())) {
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
AddAtBoundaries(s);
|
||||
|
||||
int entry_to_delete = boundary_indices[i];
|
||||
int h1 = entry_to_delete + kBigModulus;
|
||||
|
||||
// We know that the key in question was added at bucket
|
||||
// |entry_to_delete| by AddAtBoundaries.
|
||||
Key key = Key{"k" + std::to_string(entry_to_delete), FakeH1{h1}};
|
||||
s.DeleteByKey(key);
|
||||
s.CheckKeyAbsent(key);
|
||||
|
||||
// Account for the fact that a shrink-on-delete may have happened.
|
||||
int expected_capacity = number_of_entries - 1 < s.initial_capacity / 4
|
||||
? s.initial_capacity / 2
|
||||
: s.initial_capacity;
|
||||
s.CheckCounts(expected_capacity, number_of_entries - 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Adds entries that occuppy the boundaries of the hash table, then add
|
||||
// further entries targeting the same buckets.
|
||||
MEMBER_TEST(OverwritePresentAtBoundaries) {
|
||||
TS::WithAllInterestingInitialCapacities([](TS& s) {
|
||||
AddAtBoundaries(s);
|
||||
|
||||
int capacity = s.initial_capacity;
|
||||
|
||||
std::vector<int> boundary_indices = s.boundary_indices(capacity);
|
||||
|
||||
std::vector<std::string> keys, values;
|
||||
std::vector<PropertyDetails> details;
|
||||
|
||||
int count = 0;
|
||||
for (int index : boundary_indices) {
|
||||
std::string key = "additional_k" + std::to_string(index);
|
||||
std::string value = "additional_v" + std::to_string(index);
|
||||
|
||||
PropertyDetails d = distinct_details(count++);
|
||||
keys.push_back(key);
|
||||
values.push_back(value);
|
||||
details.push_back(d);
|
||||
s.Add(Key{key, FakeH1{index + kBigModulus}}, value, d);
|
||||
}
|
||||
|
||||
count = 0;
|
||||
for (int entry : boundary_indices) {
|
||||
std::string key = keys[count];
|
||||
std::string value = values[count];
|
||||
PropertyDetails d = details[count];
|
||||
|
||||
// We don't know the indices where the new entries will land.
|
||||
s.CheckDataAtKey(Key{key, FakeH1{entry + kBigModulus}},
|
||||
base::Optional<InternalIndex>(), value, d);
|
||||
count++;
|
||||
}
|
||||
|
||||
// The entries added by AddAtBoundaries must also still be there, at their
|
||||
// original indices.
|
||||
count = 0;
|
||||
for (int index : boundary_indices) {
|
||||
std::string key = "k" + std::to_string(index);
|
||||
std::string value = "v" + std::to_string(index);
|
||||
PropertyDetails details = distinct_property_details.at(count++);
|
||||
s.CheckDataAtKey(Key{key, FakeH1{index + kBigModulus}},
|
||||
InternalIndex(index), value, details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MEMBER_TEST(Empty) {
|
||||
TS::WithInitialCapacities({0}, [](TS& s) {
|
||||
// FindEntry on empty table succeeds.
|
||||
s.CheckKeyAbsent(Key{"some non-existing key"});
|
||||
});
|
||||
|
||||
TS::WithInitialCapacities({0}, [](TS& s) {
|
||||
PropertyDetails d = PropertyDetails::Empty();
|
||||
|
||||
// Adding to empty table causes resize.
|
||||
s.Add(Key{"some key"}, "some value", d);
|
||||
s.CheckDataAtKey(Key{"some key"}, kIndexUnknown, "some value", d);
|
||||
|
||||
s.CheckCounts(SwissNameDictionary::kInitialCapacity, 1, 0);
|
||||
});
|
||||
|
||||
TS::WithInitialCapacity(0, [](TS& s) { s.CheckEnumerationOrder({}); });
|
||||
|
||||
// Inplace rehashing and shrinking don't have CSA versions.
|
||||
if (TS::IsRuntimeTest()) {
|
||||
TS::WithInitialCapacity(0, [](TS& s) {
|
||||
s.RehashInplace();
|
||||
s.CheckCounts(0, 0, 0);
|
||||
s.VerifyHeap();
|
||||
});
|
||||
|
||||
TS::WithInitialCapacity(0, [](TS& s) {
|
||||
s.Shrink();
|
||||
s.CheckCounts(0, 0, 0);
|
||||
s.VerifyHeap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We test that hash tables get resized/rehashed correctly by repeatedly
|
||||
// adding an deleting elements.
|
||||
MEMBER_TEST(Resize1) {
|
||||
TS::WithInitialCapacity(0, [](TS& s) {
|
||||
// Should be at least 8 so that we capture the transition from 8 bit to 16
|
||||
// bit meta table entries:
|
||||
const int max_exponent = 9;
|
||||
|
||||
// For all |exponent| between 0 and |max_exponent|, we add 2^|exponent|
|
||||
// entries, and then delete every second one of those. Note that we do
|
||||
// this all on a single table, meaning that the entries from the previous
|
||||
// value of |exponent| are still present.
|
||||
int added = 0;
|
||||
int deleted = 0;
|
||||
int offset = 0;
|
||||
for (int exponent = 0; exponent <= max_exponent; ++exponent) {
|
||||
int count = 1 << exponent;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::string key = "key" + std::to_string(offset + i);
|
||||
std::string value = "value" + std::to_string(offset + i);
|
||||
|
||||
s.Add(Key{key}, value, distinct_details(i, offset));
|
||||
++added;
|
||||
}
|
||||
for (int i = 0; i < count; i += 2) {
|
||||
if (offset + i == 0) {
|
||||
continue;
|
||||
}
|
||||
std::string key = "key" + std::to_string(offset + i);
|
||||
s.DeleteByKey(Key{key});
|
||||
++deleted;
|
||||
}
|
||||
|
||||
s.CheckCounts(kNoInt, added - deleted, kNoInt);
|
||||
offset += count;
|
||||
}
|
||||
|
||||
// Some internal consistency checks on the test itself:
|
||||
DCHECK_EQ((1 << (max_exponent + 1)) - 1, offset);
|
||||
DCHECK_EQ(offset, added);
|
||||
DCHECK_EQ(offset / 2, deleted);
|
||||
|
||||
// Check that those entries that we expect are indeed present.
|
||||
for (int i = 0; i < offset; i += 2) {
|
||||
std::string key = "key" + std::to_string(i);
|
||||
std::string value = "value" + std::to_string(i);
|
||||
|
||||
s.CheckDataAtKey(Key{key}, kIndexUnknown, value, distinct_details(i));
|
||||
}
|
||||
s.VerifyHeap();
|
||||
});
|
||||
}
|
||||
|
||||
// Check that we resize exactly when expected.
|
||||
MEMBER_TEST(Resize2) {
|
||||
TS::WithInitialCapacities({4, 8, 16, 128}, [](TS& s) {
|
||||
int count = SwissNameDictionary::MaxUsableCapacity(s.initial_capacity);
|
||||
|
||||
AddMultiple(s, count, "resize2");
|
||||
|
||||
// No resize:
|
||||
s.CheckCounts(s.initial_capacity, count, 0);
|
||||
|
||||
s.Add(Key{"key causing resize"});
|
||||
s.CheckCounts(2 * s.initial_capacity, count + 1, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// There are certain capacities where we can fill every single bucket of the
|
||||
// table before resizing (i.e., the max load factor is 100% for those
|
||||
// particular configurations. Test that this works as intended.
|
||||
MEMBER_TEST(AtFullCapacity) {
|
||||
// Determine those capacities, allowing 100% max load factor. We trust
|
||||
// MaxUsableCapacity to tell us which capacities that are (e.g., 4 and 8),
|
||||
// because we tested that function separately elsewhere.
|
||||
std::vector<int> capacities_allowing_full_utilization;
|
||||
for (int c = SwissNameDictionary::kInitialCapacity;
|
||||
c <= static_cast<int>(SwissNameDictionary::kGroupWidth); c *= 2) {
|
||||
if (SwissNameDictionary::MaxUsableCapacity(c) == c) {
|
||||
capacities_allowing_full_utilization.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
DCHECK_IMPLIES(SwissNameDictionary::kGroupWidth == 16,
|
||||
capacities_allowing_full_utilization.size() > 0);
|
||||
|
||||
TS::WithInitialCapacities(capacities_allowing_full_utilization, [](TS& s) {
|
||||
AddMultiple(s, s.initial_capacity, "k_full_capacity", "v_full_capacity");
|
||||
|
||||
// No resize must have happened.
|
||||
s.CheckCounts(s.initial_capacity, s.initial_capacity, 0);
|
||||
|
||||
CheckMultiple(s, s.initial_capacity, "k_full_capacity",
|
||||
"v_full_capacity");
|
||||
|
||||
// Must make sure that the first |SwissNameDictionary::kGroupWidth|
|
||||
// entries of the ctrl table contain a kEmpty, so that an unsuccessful
|
||||
// search stop, instead of going into an infinite loop. Therefore, search
|
||||
// for a fake key whose H1 is 0, making us start from ctrl table bucket 0.
|
||||
s.CheckKeyAbsent(Key{"non_existing_key", FakeH1{0}, FakeH2{1}});
|
||||
});
|
||||
}
|
||||
|
||||
MEMBER_TEST(EnumerationOrder) {
|
||||
TS::WithAllInterestingInitialCapacities([](TS& s) {
|
||||
std::vector<std::string> expected_keys;
|
||||
int count = std::min(
|
||||
SwissNameDictionary::MaxUsableCapacity(s.initial_capacity), 1000);
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::string key = "enumkey" + std::to_string(i);
|
||||
expected_keys.push_back(key);
|
||||
s.Add(Key{key});
|
||||
}
|
||||
s.CheckEnumerationOrder(expected_keys);
|
||||
|
||||
// Delete some entries.
|
||||
|
||||
std::string last_key = "enumkey" + std::to_string(count - 1);
|
||||
s.DeleteByKey(Key{"enumkey0"});
|
||||
s.DeleteByKey(Key{"enumkey1"});
|
||||
s.DeleteByKey(Key{last_key});
|
||||
|
||||
auto should_be_deleted = [&](const std::string& k) -> bool {
|
||||
return k == "enumkey0" || k == "enumkey1" || k == last_key;
|
||||
};
|
||||
expected_keys.erase(
|
||||
std::remove_if(expected_keys.begin(), expected_keys.end(),
|
||||
should_be_deleted),
|
||||
expected_keys.end());
|
||||
DCHECK_EQ(expected_keys.size(), count - 3);
|
||||
|
||||
s.CheckEnumerationOrder(expected_keys);
|
||||
|
||||
if (s.initial_capacity <= 1024) {
|
||||
// Now cause a resize. Doing + 4 on top of the maximum usable capacity
|
||||
// rather than just + 1 because in the case where the initial capacity
|
||||
// is 4 and the group size is 8, the three deletes above caused a
|
||||
// shrink, which in this case was just a rehash. So we need to add 4
|
||||
// elements to cause a resize.
|
||||
int resize_at =
|
||||
SwissNameDictionary::MaxUsableCapacity(s.initial_capacity) + 4;
|
||||
|
||||
for (int i = count; i < resize_at; ++i) {
|
||||
std::string key = "enumkey" + std::to_string(i);
|
||||
expected_keys.push_back(key);
|
||||
s.Add(Key{key});
|
||||
}
|
||||
s.CheckCounts(2 * s.initial_capacity);
|
||||
s.CheckEnumerationOrder(expected_keys);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make sure that keys with colliding H1 and same H2 don't get mixed up.
|
||||
MEMBER_TEST(SameH2) {
|
||||
int i = 0;
|
||||
TS::WithAllInterestingInitialCapacities([&](TS& s) {
|
||||
// Let's try a few differnet values for h1, starting at big_modulus;.
|
||||
int first_h1 = i * 13 + kBigModulus;
|
||||
int second_h1 = first_h1 + s.initial_capacity;
|
||||
|
||||
int first_entry = first_h1 % s.initial_capacity;
|
||||
int second_entry = (first_h1 + 1) % s.initial_capacity;
|
||||
|
||||
// Add two keys with same H1 modulo capacity and same H2.
|
||||
Key k1{"first_key", FakeH1{first_h1}, FakeH2{42}};
|
||||
Key k2{"second_key", FakeH1{second_h1}, FakeH2{42}};
|
||||
|
||||
s.Add(k1, "v1");
|
||||
s.Add(k2, "v2");
|
||||
|
||||
s.CheckDataAtKey(k1, InternalIndex(first_entry), "v1");
|
||||
s.CheckDataAtKey(k2, InternalIndex(second_entry), "v2");
|
||||
|
||||
// Deletion works, too.
|
||||
s.DeleteByKey(k2);
|
||||
s.CheckHasKey(k1);
|
||||
s.CheckKeyAbsent(k2);
|
||||
|
||||
++i;
|
||||
});
|
||||
}
|
||||
|
||||
// Check that we can delete a key and add it again.
|
||||
MEMBER_TEST(ReAddSameKey) {
|
||||
TS::WithInitialCapacity(4, [](TS& s) {
|
||||
s.Add(Key{"some_key"}, "some_value", distinct_details(0));
|
||||
s.DeleteByKey(Key{"some_key"});
|
||||
s.Add(Key{"some_key"}, "new_value", distinct_details(1));
|
||||
s.CheckDataAtKey(Key{"some_key"}, kIndexUnknown, "new_value",
|
||||
distinct_details(1));
|
||||
s.CheckEnumerationOrder({"some_key"});
|
||||
});
|
||||
}
|
||||
|
||||
// Make sure that we continue probing if there is no match in the first
|
||||
// group and that the quadratic probing for choosing subsequent groups to
|
||||
// probe works as intended.
|
||||
MEMBER_TEST(BeyondInitialGroup) {
|
||||
TS::WithInitialCapacity(128, [](TS& s) {
|
||||
int h1 = 33; // Arbitrarily chosen.
|
||||
int count = 37; // Will lead to more than 2 groups being filled.
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::string key = "key" + std::to_string(i);
|
||||
std::string value = "value" + std::to_string(i);
|
||||
|
||||
s.Add(Key{key, FakeH1{h1}}, value);
|
||||
}
|
||||
|
||||
s.CheckDataAtKey(Key{"key36", FakeH1{h1}}, kIndexUnknown, "value36");
|
||||
|
||||
// Deleting something shouldn't disturb further additions.
|
||||
s.DeleteByKey(Key{"key14", FakeH1{h1}});
|
||||
s.DeleteByKey(Key{"key15", FakeH1{h1}});
|
||||
s.DeleteByKey(Key{"key16", FakeH1{h1}});
|
||||
s.DeleteByKey(Key{"key17", FakeH1{h1}});
|
||||
|
||||
s.Add(Key{"key37", FakeH1{h1}}, "value37");
|
||||
s.CheckDataAtKey(Key{"key37", FakeH1{h1}}, kIndexUnknown, "value37");
|
||||
});
|
||||
}
|
||||
|
||||
// Check that we correclty "wrap around" when probing the control table. This
|
||||
// means that when we probe a group starting at a bucket such that there are
|
||||
// fewer than kGroupWidth bucktets before the end of the control table, we
|
||||
// (logically) continue at bucket 0. Note that actually, we use the copy of
|
||||
// first group at the end of the control table.
|
||||
MEMBER_TEST(WrapAround) {
|
||||
int width = SwissNameDictionary::kGroupWidth;
|
||||
|
||||
for (int offset_from_end = 0; offset_from_end < width; ++offset_from_end) {
|
||||
TS::WithAllInterestingInitialCapacities([&](TS& s) {
|
||||
int capacity = s.initial_capacity;
|
||||
int first_bucket = capacity - offset_from_end;
|
||||
|
||||
// How many entries to add (carefully chosen not to cause a resize).
|
||||
int filler_entries =
|
||||
std::min(width, SwissNameDictionary::MaxUsableCapacity(capacity)) -
|
||||
1;
|
||||
|
||||
if (first_bucket < 0 ||
|
||||
// No wraparound in this case:
|
||||
first_bucket + filler_entries < capacity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Starting at bucket |first_bucket|, add a sequence of |kGroupWitdth|
|
||||
// - 1 (if table can take that many, see calculation of |filler_entries|
|
||||
// above) entries in a single collision chain.
|
||||
for (int f = 0; f < filler_entries; ++f) {
|
||||
std::string key = "filler" + std::to_string(f);
|
||||
s.Add(Key{key, FakeH1{first_bucket}});
|
||||
}
|
||||
|
||||
// ... then add a final key which (unless table too small) will end up
|
||||
// in the last bucket belonging to the group started at |first_bucket|.
|
||||
// Check that we can indeed find it.
|
||||
s.Add(Key{"final_key", FakeH1{first_bucket}});
|
||||
s.CheckDataAtKey(Key{"final_key", FakeH1{first_bucket}},
|
||||
InternalIndex(filler_entries - offset_from_end));
|
||||
|
||||
// + 1 due to the final key.
|
||||
s.CheckCounts(s.initial_capacity, filler_entries + 1, 0);
|
||||
|
||||
// Now delete the entries in between and make sure that this
|
||||
// doesn't break anything.
|
||||
for (int f = 0; f < filler_entries; ++f) {
|
||||
std::string key = "filler" + std::to_string(f);
|
||||
s.DeleteByKey(Key{key, FakeH1{first_bucket}});
|
||||
}
|
||||
|
||||
s.CheckHasKey(Key{"final_key", FakeH1{first_bucket}});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MEMBER_TEST(RehashInplace) {
|
||||
if (TS::IsRuntimeTest()) {
|
||||
TS::WithAllInterestingInitialCapacities([](TS& s) {
|
||||
if (s.initial_capacity <= 8) {
|
||||
// Add 3 elements, which will not cause a resize. Then delete the
|
||||
// first key before rehasing.
|
||||
|
||||
AddMultiple(s, 3);
|
||||
s.DeleteByKey(Key{"key0"});
|
||||
|
||||
// We shouldn't have done a resize on deletion or addition:
|
||||
s.CheckCounts(s.initial_capacity, 2, 1);
|
||||
|
||||
s.RehashInplace();
|
||||
|
||||
s.CheckDataAtKey(Key{"key1"}, kIndexUnknown, "value1");
|
||||
s.CheckDataAtKey(Key{"key2"}, kIndexUnknown, "value2");
|
||||
s.CheckEnumerationOrder({"key1", "key2"});
|
||||
} else {
|
||||
int count =
|
||||
SwissNameDictionary::MaxUsableCapacity(s.initial_capacity) - 5;
|
||||
AddMultiple(s, count);
|
||||
|
||||
s.DeleteByKey(Key{"key1"});
|
||||
s.DeleteByKey(Key{"key2"});
|
||||
s.DeleteByKey(Key{"key" + std::to_string(count - 1)});
|
||||
|
||||
// We shouldn't have done a resize on deletion or addition:
|
||||
s.CheckCounts(s.initial_capacity, count - 3, 3);
|
||||
|
||||
s.RehashInplace();
|
||||
|
||||
std::vector<std::string> expected_enum_order;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (i == 1 || i == 2 || i == count - 1) {
|
||||
// These are the keys we deleted.
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string key = "key" + std::to_string(i);
|
||||
PropertyDetails d =
|
||||
distinct_property_details[i % distinct_property_details.size()];
|
||||
s.CheckDataAtKey(Key{key}, kIndexUnknown,
|
||||
"value" + std::to_string(i), d);
|
||||
|
||||
expected_enum_order.push_back(key);
|
||||
}
|
||||
|
||||
s.CheckEnumerationOrder(expected_enum_order);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MEMBER_TEST(Shrink) {
|
||||
if (TS::IsRuntimeTest()) {
|
||||
TS::WithInitialCapacity(32, [&](TS& s) {
|
||||
// Filling less than a forth of the table:
|
||||
int count = 4;
|
||||
|
||||
AddMultiple(s, count);
|
||||
|
||||
s.Shrink();
|
||||
|
||||
CheckMultiple(s, count, "key", "value", 0);
|
||||
|
||||
// Shrink doesn't shrink to fit, but only halves the capacity.
|
||||
int expected_capacity = s.initial_capacity / 2;
|
||||
s.CheckCounts(expected_capacity, 4, 0);
|
||||
|
||||
s.CheckEnumerationOrder({"key0", "key1", "key2", "key3"});
|
||||
s.VerifyHeap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MEMBER_TEST(ShrinkToInitial) {
|
||||
// When shrinking, we never go below SwissNameDictionary::kInitialCapacity.
|
||||
if (TS::IsRuntimeTest()) {
|
||||
TS::WithInitialCapacity(8, [&](TS& s) {
|
||||
s.Shrink();
|
||||
|
||||
s.CheckCounts(SwissNameDictionary::kInitialCapacity, 0, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MEMBER_TEST(ShrinkOnDelete) {
|
||||
TS::WithInitialCapacity(32, [](TS& s) {
|
||||
// Adds key0 ... key9:
|
||||
AddMultiple(s, 10);
|
||||
|
||||
// We remove some entries. Each time less than a forth of the table is
|
||||
// used by present entries, it's shrunk to half.
|
||||
|
||||
s.DeleteByKey(Key{"key9"});
|
||||
s.DeleteByKey(Key{"key8"});
|
||||
|
||||
s.CheckCounts(32, 8, 2);
|
||||
|
||||
s.DeleteByKey(Key{"key7"});
|
||||
|
||||
// Deleted count is 0 after rehash.
|
||||
s.CheckCounts(16, 7, 0);
|
||||
});
|
||||
}
|
||||
|
||||
MEMBER_TEST(Copy) {
|
||||
TS::WithAllInterestingInitialCapacities([](TS& s) {
|
||||
int fill = std::min(
|
||||
1000,
|
||||
// -2 due to the two manually added keys below.
|
||||
SwissNameDictionary::MaxUsableCapacity(s.initial_capacity) - 2);
|
||||
AddMultiple(s, fill);
|
||||
|
||||
// Occupy first and last bucket (another key may occuppy these already,
|
||||
// but let's don't bother with that):
|
||||
s.Add(Key{"first_bucket_key", FakeH1{kBigModulus}});
|
||||
s.Add(Key{"last_bucket_key", FakeH1{s.initial_capacity - 1}});
|
||||
|
||||
// We shouldn't have caused a resize.
|
||||
s.CheckCounts(s.initial_capacity);
|
||||
|
||||
// Creates a copy and compares it against the original. In order to check
|
||||
// copying of large dictionary, need to check before deletion due to
|
||||
// shrink-on-delete kicking in.
|
||||
s.CheckCopy();
|
||||
|
||||
// Let's delete a few entries, most notably the first and last two in enum
|
||||
// order and the keys (potentially) occupying the first and last bucket.
|
||||
s.DeleteByKey(Key{"key0"});
|
||||
if (fill > 1) {
|
||||
s.DeleteByKey(Key{"key1"});
|
||||
}
|
||||
s.DeleteByKey(Key{"first_bucket_key", FakeH1{kBigModulus}});
|
||||
s.DeleteByKey(Key{"last_bucket_key", FakeH1{s.initial_capacity - 1}});
|
||||
|
||||
s.CheckCopy();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace test_swiss_hash_table
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
||||
#endif // V8_TEST_CCTEST_TEST_SWISS_HASH_TABLE_SHARED_TESTS_H_
|
@ -5,6 +5,7 @@
|
||||
#include "src/objects/swiss-name-dictionary-inl.h"
|
||||
#include "test/cctest/cctest.h"
|
||||
#include "test/cctest/test-swiss-name-dictionary-infra.h"
|
||||
#include "test/cctest/test-swiss-name-dictionary-shared-tests.h"
|
||||
|
||||
namespace v8 {
|
||||
namespace internal {
|
||||
@ -213,6 +214,14 @@ TEST(SizeFor) {
|
||||
CHECK_EQ(SwissNameDictionary::SizeFor(8), size_8);
|
||||
}
|
||||
|
||||
// Executes the tests defined in test-swiss-name-dictionary-shared-tests.h as if
|
||||
// they were defined in this file, using the RuntimeTestRunner. See comments in
|
||||
// test-swiss-name-dictionary-shared-tests.h and in
|
||||
// swiss-name-dictionary-infra.h for details.
|
||||
const char kRuntimeTestFileName[] = __FILE__;
|
||||
SharedSwissTableTests<RuntimeTestRunner, kRuntimeTestFileName>
|
||||
execute_shared_tests_runtime;
|
||||
|
||||
} // namespace test_swiss_hash_table
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
Loading…
Reference in New Issue
Block a user