From 8e6047e51d1fb4ef7d76bd62bdf24335b5ad8664 Mon Sep 17 00:00:00 2001 From: Frank Emrich Date: Fri, 19 Mar 2021 20:31:25 +0100 Subject: [PATCH] [dict-proto] C++ implementation of SwissNameDictionary, pt. 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Marja Hölttä Commit-Queue: Frank Emrich Cr-Commit-Position: refs/heads/master@{#73572} --- test/cctest/cctest.h | 13 + test/cctest/test-swiss-name-dictionary-csa.cc | 8 + .../test-swiss-name-dictionary-infra.cc | 24 +- .../cctest/test-swiss-name-dictionary-infra.h | 2 +- .../test-swiss-name-dictionary-shared-tests.h | 858 ++++++++++++++++++ test/cctest/test-swiss-name-dictionary.cc | 9 + 6 files changed, 908 insertions(+), 6 deletions(-) create mode 100644 test/cctest/test-swiss-name-dictionary-shared-tests.h diff --git a/test/cctest/cctest.h b/test/cctest/cctest.h index e2fe217fd1..862f347aa4 100644 --- a/test/cctest/cctest.h +++ b/test/cctest/cctest.h @@ -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") \ diff --git a/test/cctest/test-swiss-name-dictionary-csa.cc b/test/cctest/test-swiss-name-dictionary-csa.cc index 4e5fe33437..a760c91573 100644 --- a/test/cctest/test-swiss-name-dictionary-csa.cc +++ b/test/cctest/test-swiss-name-dictionary-csa.cc @@ -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 execute_shared_tests_csa; + } // namespace test_swiss_hash_table } // namespace internal } // namespace v8 diff --git a/test/cctest/test-swiss-name-dictionary-infra.cc b/test/cctest/test-swiss-name-dictionary-infra.cc index 0a8257bc3b..539d71c823 100644 --- a/test/cctest/test-swiss-name-dictionary-infra.cc +++ b/test/cctest/test-swiss-name-dictionary-infra.cc @@ -76,6 +76,13 @@ Handle 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 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 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 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; } diff --git a/test/cctest/test-swiss-name-dictionary-infra.h b/test/cctest/test-swiss-name-dictionary-infra.h index c4564527d3..198149d0e2 100644 --- a/test/cctest/test-swiss-name-dictionary-infra.h +++ b/test/cctest/test-swiss-name-dictionary-infra.h @@ -28,7 +28,7 @@ using IndexOpt = base::Optional; static const ValueOpt kNoValue; static const PropertyDetailsOpt kNoDetails; static const base::Optional kNoInt; -static const IndexOpt kNoIndex; +static const IndexOpt kIndexUnknown; static const std::vector interesting_initial_capacities = { 4, diff --git a/test/cctest/test-swiss-name-dictionary-shared-tests.h b/test/cctest/test-swiss-name-dictionary-shared-tests.h new file mode 100644 index 0000000000..6fabd479a6 --- /dev/null +++ b/test/cctest/test-swiss-name-dictionary-shared-tests.h @@ -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 +#include + +#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 +struct SharedSwissTableTests { + STATIC_ASSERT((std::is_same::value) || + (std::is_same::value)); + + SharedSwissTableTests() { + CHECK(kTestFileName == kRuntimeTestFileName || + kTestFileName == kCSATestFileName); + } + + using TS = TestSequence; + + // + // 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(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 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 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 key1 = s.isolate->factory()->InternalizeUtf8String("foo"); + Handle 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 key2 = s.isolate->factory()->NewSymbol(); + Handle 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 key1 = s.isolate->factory()->InternalizeUtf8String("foo"); + Handle value1 = + s.isolate->factory()->InternalizeUtf8String("bar"); + PropertyDetails details1 = + PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE, + PropertyCellType::kNoCell); + + s.Add(key1, value1, details1); + + Handle key2 = s.isolate->factory()->NewSymbol(); + Handle 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 value1_updated = handle(Smi::FromInt(456), s.isolate); + Handle 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 key1 = s.isolate->factory()->InternalizeUtf8String("foo"); + Handle value1 = + s.isolate->factory()->InternalizeUtf8String("bar"); + PropertyDetails details1 = + PropertyDetails(PropertyKind::kData, PropertyAttributes::DONT_DELETE, + PropertyCellType::kNoCell); + + s.Add(key1, value1, details1); + + Handle key2 = s.isolate->factory()->NewSymbol(); + Handle 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 boundary_indices = s.boundary_indices(capacity); + int size = static_cast(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 boundary_indices = s.boundary_indices(capacity); + int size = static_cast(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 boundary_indices = + TS::boundary_indices(s.initial_capacity); + int number_of_entries = static_cast(boundary_indices.size()); + DCHECK_GE(count, number_of_entries); + + if (i >= static_cast(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 boundary_indices = s.boundary_indices(capacity); + + std::vector keys, values; + std::vector 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(), 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 capacities_allowing_full_utilization; + for (int c = SwissNameDictionary::kInitialCapacity; + c <= static_cast(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 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 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_ diff --git a/test/cctest/test-swiss-name-dictionary.cc b/test/cctest/test-swiss-name-dictionary.cc index 5c5984817a..895445eee0 100644 --- a/test/cctest/test-swiss-name-dictionary.cc +++ b/test/cctest/test-swiss-name-dictionary.cc @@ -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 + execute_shared_tests_runtime; + } // namespace test_swiss_hash_table } // namespace internal } // namespace v8