From e3bbe9506d8b8b216b6dcab8f69b20665a72f802 Mon Sep 17 00:00:00 2001 From: Jiayu Wu Date: Thu, 27 Oct 2022 10:40:06 +0800 Subject: [PATCH 1/5] Merge volatile module, store persistent and volatile module in root dir (#349) Cherry-pick volatile branch, store persistent and volatile module to independent directories. Signed-off-by: Wu, Jiayu --- .github/workflows/ci.yml | 19 +- .gitmodules | 13 +- {pic => .pic}/kvdk-qrcode.png | Bin {pic => .pic}/kvdk_logo.png | Bin README.md | 115 +- persistent/.gitignore | 53 + CMakeLists.txt => persistent/CMakeLists.txt | 2 +- persistent/README.md | 126 + {benchmark => persistent/benchmark}/bench.cpp | 4 +- .../benchmark}/generator.hpp | 0 .../benchmark}/utils/rand64.hpp | 0 .../benchmark}/utils/range_iterator.hpp | 0 .../benchmark}/utils/zipf.hpp | 0 {cmake => persistent/cmake}/functions.cmake | 0 {doc => persistent/doc}/benchmark.md | 0 .../doc}/figs/pmem_allocator.png | Bin {doc => persistent/doc}/pmem_allocator.md | 0 {doc => persistent/doc}/user_doc.md | 4 +- {engine => persistent/engine}/alias.hpp | 2 +- {engine => persistent/engine}/allocator.hpp | 0 {engine => persistent/engine}/backup_log.hpp | 4 +- .../engine}/c/kvdk_basic_op.cpp | 0 .../engine}/c/kvdk_batch.cpp | 0 {engine => persistent/engine}/c/kvdk_c.hpp | 12 +- {engine => persistent/engine}/c/kvdk_hash.cpp | 0 {engine => persistent/engine}/c/kvdk_list.cpp | 0 .../engine}/c/kvdk_sorted.cpp | 0 .../engine}/c/kvdk_string.cpp | 0 .../engine}/c/kvdk_transaction.cpp | 0 {engine => persistent/engine}/collection.hpp | 0 {engine => persistent/engine}/configs.hpp | 2 +- {engine => persistent/engine}/data_record.cpp | 0 {engine => persistent/engine}/data_record.hpp | 2 +- {engine => persistent/engine}/dl_list.cpp | 0 {engine => persistent/engine}/dl_list.hpp | 2 +- .../engine}/dram_allocator.cpp | 0 .../engine}/dram_allocator.hpp | 2 +- persistent/engine/engine.cpp | 26 + .../engine}/hash_collection/hash_list.cpp | 0 .../engine}/hash_collection/hash_list.hpp | 2 +- .../engine}/hash_collection/iterator.hpp | 4 +- .../engine}/hash_collection/rebuilder.hpp | 0 {engine => persistent/engine}/hash_table.cpp | 0 {engine => persistent/engine}/hash_table.hpp | 2 +- {engine => persistent/engine}/kv_engine.cpp | 2 +- {engine => persistent/engine}/kv_engine.hpp | 2 +- .../engine}/kv_engine_cleaner.cpp | 0 .../engine}/kv_engine_cleaner.hpp | 0 .../engine}/kv_engine_hash.cpp | 0 .../engine}/kv_engine_list.cpp | 0 .../engine}/kv_engine_sorted.cpp | 0 .../engine}/kv_engine_string.cpp | 0 .../engine}/list_collection/iterator.hpp | 4 +- .../engine}/list_collection/list.cpp | 0 .../engine}/list_collection/list.hpp | 2 +- .../engine}/list_collection/rebuilder.hpp | 0 {engine => persistent/engine}/lock_table.hpp | 0 {engine => persistent/engine}/logger.cpp | 0 persistent/engine/logger.hpp | 39 + {engine => persistent/engine}/macros.hpp | 0 .../engine}/pmem_allocator/free_list.cpp | 0 .../engine}/pmem_allocator/free_list.hpp | 0 .../engine}/pmem_allocator/pmem_allocator.cpp | 0 .../engine}/pmem_allocator/pmem_allocator.hpp | 0 {engine => persistent/engine}/snapshot.hpp | 0 .../engine}/sorted_collection/iterator.hpp | 0 .../engine}/sorted_collection/rebuilder.cpp | 0 .../engine}/sorted_collection/rebuilder.hpp | 0 .../engine}/sorted_collection/skiplist.cpp | 0 .../engine}/sorted_collection/skiplist.hpp | 2 +- {engine => persistent/engine}/structures.hpp | 0 .../engine}/thread_manager.cpp | 0 persistent/engine/thread_manager.hpp | 48 + .../engine}/transaction_impl.cpp | 0 .../engine}/transaction_impl.hpp | 2 +- {engine => persistent/engine}/utils/codec.hpp | 0 .../engine}/utils/sync_impl.hpp | 0 .../engine}/utils/sync_point.cpp | 0 .../engine}/utils/sync_point.hpp | 0 {engine => persistent/engine}/utils/utils.cpp | 0 {engine => persistent/engine}/utils/utils.hpp | 0 .../engine}/version/old_records_cleaner.cpp | 0 .../engine}/version/old_records_cleaner.hpp | 2 +- .../engine}/version/version_controller.hpp | 2 +- .../engine}/write_batch_impl.cpp | 0 .../engine}/write_batch_impl.hpp | 2 +- .../examples}/graph_sim/CMakeLists.txt | 0 .../examples}/graph_sim/README.md | 0 .../examples}/graph_sim/bench/graph_bench.cpp | 0 .../graph_sim/scripts/build_rocksdb.sh | 0 .../examples}/graph_sim/src/coding.hpp | 0 .../graph_sim/src/graph_algorithm/top_n.hpp | 0 .../examples}/graph_sim/src/graph_impl.cpp | 0 .../examples}/graph_sim/src/graph_impl.hpp | 0 .../graph_sim/src/graph_impl_test.cpp | 0 .../graph_sim/src/kv_engines/KVEngine.hpp | 2 +- .../src/kv_engines/engine_factory.hpp | 0 .../graph_sim/src/kv_engines/kvdk.cpp | 0 .../graph_sim/src/kv_engines/rocksdb.cpp | 0 .../examples}/graph_sim/src/options.hpp | 0 .../kvredis/0001-redis-with-kvdk.patch | 0 .../examples}/kvredis/README.md | 0 .../examples}/kvredis/bench_redis_kvdk.sh | 0 .../examples}/kvredis/build_redis_kvdk.sh | 0 ...0001-use-KVDK-for-set-get-operations.patch | 0 .../examples}/kvrocks/README.md | 0 .../examples}/tutorial/CMakeLists.txt | 0 .../examples}/tutorial/c_api_tutorial.c | 2 +- .../examples}/tutorial/cpp_api_tutorial.cpp | 2 +- .../examples}/tutorial/zset.c | 2 +- {extern => persistent/extern}/gtest | 0 .../extern}/libpmemobj++/string_view.hpp | 0 {extern => persistent/extern}/xxhash.h | 0 .../include/kvdk/persistent}/comparator.hpp | 0 .../include/kvdk/persistent}/configs.hpp | 0 .../include/kvdk/persistent}/engine.h | 0 .../include/kvdk/persistent}/engine.hpp | 0 .../include/kvdk/persistent}/iterator.hpp | 0 .../include/kvdk/persistent}/snapshot.hpp | 0 .../include/kvdk/persistent}/transaction.hpp | 0 .../include/kvdk/persistent}/types.h | 0 .../include/kvdk/persistent}/types.hpp | 0 .../include/kvdk/persistent}/write_batch.hpp | 0 {java => persistent/java}/CMakeLists.txt | 0 {java => persistent/java}/README.md | 0 {java => persistent/java}/benchmark/pom.xml | 0 .../java}/benchmark/scripts/benchmark_all.sh | 0 .../io/pmem/kvdk/benchmark/KVDKBenchmark.java | 0 .../benchmark/util/ConstantLongGenerator.java | 0 .../kvdk/benchmark/util/LongGenerator.java | 0 .../benchmark/util/RandomLongGenerator.java | 0 .../benchmark/util/RangeLongGenerator.java | 0 {java => persistent/java}/examples/pom.xml | 0 .../io/pmem/kvdk/exmaples/KVDKExamples.java | 0 {java => persistent/java}/kvdkjni/configs.cc | 0 .../java}/kvdkjni/cplusplus_to_java_convert.h | 0 {java => persistent/java}/kvdkjni/engine.cc | 0 {java => persistent/java}/kvdkjni/iterator.cc | 0 {java => persistent/java}/kvdkjni/kvdkjni.h | 2 +- .../java}/kvdkjni/native_bytes_handle.cc | 0 .../java}/kvdkjni/write_batch.cc | 0 {java => persistent/java}/pom.xml | 0 .../io/pmem/kvdk/AbstractNativeReference.java | 0 .../src/main/java/io/pmem/kvdk/Configs.java | 0 .../src/main/java/io/pmem/kvdk/Engine.java | 0 .../src/main/java/io/pmem/kvdk/Iterator.java | 0 .../main/java/io/pmem/kvdk/KVDKException.java | 0 .../main/java/io/pmem/kvdk/KVDKObject.java | 0 .../java/io/pmem/kvdk/NativeBytesHandle.java | 0 .../io/pmem/kvdk/NativeLibraryLoader.java | 0 .../src/main/java/io/pmem/kvdk/Status.java | 0 .../main/java/io/pmem/kvdk/WriteBatch.java | 0 .../main/java/io/pmem/kvdk/WriteOptions.java | 0 .../java/src/main/resources/libatomic.so.1 | Bin 0 -> 26720 bytes .../java/src/main/resources/libstdc++.so.6 | Bin 0 -> 1594864 bytes .../test/java/io/pmem/kvdk/ConfigsTest.java | 0 .../test/java/io/pmem/kvdk/EngineTest.java | 0 .../java/io/pmem/kvdk/EngineTestBase.java | 0 .../test/java/io/pmem/kvdk/IteratorTest.java | 0 .../java/io/pmem/kvdk/WriteBatchTest.java | 0 kvdk.pc.in => persistent/kvdk.pc.in | 0 .../scripts}/benchmark_impl.py | 0 .../scripts}/clang_format.sh | 0 {scripts => persistent/scripts}/clang_tidy.sh | 0 {scripts => persistent/scripts}/cppstyle | 0 .../scripts}/init_devdax.sh | 0 .../scripts}/run_benchmark.py | 0 .../scripts}/test_coverage.sh | 0 {tests => persistent/tests}/CMakeLists.txt | 0 {tests => persistent/tests}/allocator.hpp | 0 {tests => persistent/tests}/c_api_test.hpp | 2 +- .../tests}/c_api_test_hash.cpp | 0 .../tests}/c_api_test_list.cpp | 0 .../tests}/pmem_allocator_bench.cpp | 2 +- {tests => persistent/tests}/stress_test.cpp | 4 +- .../tests}/test_pmem_allocator.cpp | 2 +- {tests => persistent/tests}/test_util.h | 0 {tests => persistent/tests}/tests.cpp | 2 +- volatile/.clang-format | 5 + volatile/.gitignore | 53 + volatile/CMakeLists.txt | 197 + volatile/LICENSE | 30 + volatile/README.md | 122 + volatile/benchmark/bench.cpp | 725 +++ volatile/benchmark/generator.hpp | 27 + volatile/benchmark/utils/rand64.hpp | 60 + volatile/benchmark/utils/range_iterator.hpp | 31 + volatile/benchmark/utils/zipf.hpp | 102 + volatile/cmake/functions.cmake | 75 + volatile/doc/benchmark.md | 152 + volatile/doc/user_doc.md | 437 ++ volatile/engine/alias.hpp | 19 + volatile/engine/allocator.hpp | 335 + volatile/engine/backup_log.hpp | 278 + volatile/engine/c/kvdk_basic_op.cpp | 135 + volatile/engine/c/kvdk_batch.cpp | 66 + volatile/engine/c/kvdk_c.hpp | 77 + volatile/engine/c/kvdk_hash.cpp | 147 + volatile/engine/c/kvdk_list.cpp | 250 + volatile/engine/c/kvdk_sorted.cpp | 137 + volatile/engine/c/kvdk_string.cpp | 58 + volatile/engine/collection.hpp | 84 + volatile/engine/data_record.cpp | 7 + volatile/engine/data_record.hpp | 318 + volatile/engine/dl_list.cpp | 178 + volatile/engine/dl_list.hpp | 344 ++ volatile/engine/dram_allocator.cpp | 299 + volatile/engine/dram_allocator.hpp | 253 + {engine => volatile/engine}/engine.cpp | 2 +- volatile/engine/hash_collection/hash_list.cpp | 424 ++ volatile/engine/hash_collection/hash_list.hpp | 200 + volatile/engine/hash_collection/iterator.hpp | 65 + volatile/engine/hash_table.cpp | 205 + volatile/engine/hash_table.hpp | 420 ++ volatile/engine/kv_engine.cpp | 1064 ++++ volatile/engine/kv_engine.hpp | 609 ++ volatile/engine/kv_engine_cleaner.cpp | 866 +++ volatile/engine/kv_engine_cleaner.hpp | 225 + volatile/engine/kv_engine_hash.cpp | 272 + volatile/engine/kv_engine_list.cpp | 607 ++ volatile/engine/kv_engine_sorted.cpp | 283 + volatile/engine/kv_engine_string.cpp | 291 + volatile/engine/list_collection/iterator.hpp | 92 + volatile/engine/list_collection/list.cpp | 464 ++ volatile/engine/list_collection/list.hpp | 171 + volatile/engine/lock_table.hpp | 89 + volatile/engine/logger.cpp | 56 + {engine => volatile/engine}/logger.hpp | 2 +- volatile/engine/macros.hpp | 34 + volatile/engine/snapshot.hpp | 11 + .../engine/sorted_collection/iterator.hpp | 60 + .../engine/sorted_collection/skiplist.cpp | 950 +++ .../engine/sorted_collection/skiplist.hpp | 582 ++ volatile/engine/structures.hpp | 93 + volatile/engine/thread_manager.cpp | 51 + .../engine}/thread_manager.hpp | 2 +- volatile/engine/utils/codec.hpp | 107 + volatile/engine/utils/sync_impl.hpp | 161 + volatile/engine/utils/sync_point.cpp | 57 + volatile/engine/utils/sync_point.hpp | 84 + volatile/engine/utils/utils.cpp | 53 + volatile/engine/utils/utils.hpp | 416 ++ .../engine/version/old_records_cleaner.cpp | 65 + .../engine/version/old_records_cleaner.hpp | 54 + .../engine/version/version_controller.hpp | 342 ++ volatile/engine/write_batch_impl.cpp | 116 + volatile/engine/write_batch_impl.hpp | 311 + volatile/examples/tutorial/CMakeLists.txt | 18 + volatile/examples/tutorial/c_api_tutorial.c | 766 +++ .../examples/tutorial/cpp_api_tutorial.cpp | 457 ++ volatile/examples/tutorial/zset.c | 275 + volatile/extern/gtest | 1 + volatile/extern/jemalloc | 1 + volatile/extern/jemalloc-cmake/CMakeLists.txt | 108 + .../internal/jemalloc_internal_defs.h.in | 435 ++ .../jemalloc/internal/jemalloc_preamble.h | 263 + .../jemalloc/internal/public_namespace.h | 25 + .../jemalloc/internal/public_unnamespace.h | 25 + .../include/jemalloc/jemalloc.h | 478 ++ .../include/jemalloc/jemalloc_defs.h | 56 + .../include/jemalloc/jemalloc_macros.h | 149 + .../include/jemalloc/jemalloc_mangle.h | 72 + .../include/jemalloc/jemalloc_mangle_jet.h | 72 + .../include/jemalloc/jemalloc_protos.h | 77 + .../include/jemalloc/jemalloc_protos_jet.h | 77 + .../include/jemalloc/jemalloc_rename.h | 32 + .../include/jemalloc/jemalloc_typedefs.h | 77 + volatile/extern/libpmemobj++/string_view.hpp | 1416 +++++ volatile/extern/numactl | 1 + volatile/extern/numactl-cmake/CMakeLists.txt | 38 + .../extern/numactl-cmake/include/config.h | 68 + volatile/extern/xxhash.h | 5448 +++++++++++++++++ volatile/include/kvdk/volatile/comparator.hpp | 48 + volatile/include/kvdk/volatile/configs.hpp | 110 + volatile/include/kvdk/volatile/engine.h | 308 + volatile/include/kvdk/volatile/engine.hpp | 514 ++ volatile/include/kvdk/volatile/iterator.hpp | 82 + volatile/include/kvdk/volatile/snapshot.hpp | 12 + volatile/include/kvdk/volatile/types.h | 76 + volatile/include/kvdk/volatile/types.hpp | 82 + .../include/kvdk/volatile/write_batch.hpp | 35 + volatile/java/CMakeLists.txt | 194 + volatile/java/README.md | 89 + volatile/java/benchmark/pom.xml | 102 + .../java/benchmark/scripts/benchmark_all.sh | 72 + .../io/pmem/kvdk/benchmark/KVDKBenchmark.java | 1033 ++++ .../benchmark/util/ConstantLongGenerator.java | 18 + .../kvdk/benchmark/util/LongGenerator.java | 9 + .../benchmark/util/RandomLongGenerator.java | 24 + .../benchmark/util/RangeLongGenerator.java | 28 + volatile/java/examples/pom.xml | 102 + .../io/pmem/kvdk/exmaples/KVDKExamples.java | 131 + volatile/java/kvdkjni/configs.cc | 90 + .../java/kvdkjni/cplusplus_to_java_convert.h | 8 + volatile/java/kvdkjni/engine.cc | 438 ++ volatile/java/kvdkjni/iterator.cc | 127 + volatile/java/kvdkjni/kvdkjni.h | 322 + volatile/java/kvdkjni/native_bytes_handle.cc | 39 + volatile/java/kvdkjni/write_batch.cc | 168 + volatile/java/pom.xml | 105 + .../io/pmem/kvdk/AbstractNativeReference.java | 40 + .../src/main/java/io/pmem/kvdk/Configs.java | 64 + .../src/main/java/io/pmem/kvdk/Engine.java | 426 ++ .../src/main/java/io/pmem/kvdk/Iterator.java | 79 + .../main/java/io/pmem/kvdk/KVDKException.java | 28 + .../main/java/io/pmem/kvdk/KVDKObject.java | 27 + .../java/io/pmem/kvdk/NativeBytesHandle.java | 38 + .../io/pmem/kvdk/NativeLibraryLoader.java | 116 + .../src/main/java/io/pmem/kvdk/Status.java | 94 + .../main/java/io/pmem/kvdk/WriteBatch.java | 139 + .../main/java/io/pmem/kvdk/WriteOptions.java | 28 + .../test/java/io/pmem/kvdk/ConfigsTest.java | 18 + .../test/java/io/pmem/kvdk/EngineTest.java | 149 + .../java/io/pmem/kvdk/EngineTestBase.java | 36 + .../test/java/io/pmem/kvdk/IteratorTest.java | 89 + .../java/io/pmem/kvdk/WriteBatchTest.java | 61 + volatile/kvdk.pc.in | 12 + volatile/scripts/benchmark_impl.py | 184 + volatile/scripts/clang_format.sh | 12 + volatile/scripts/clang_tidy.sh | 19 + volatile/scripts/cppstyle | 44 + volatile/scripts/run_benchmark.py | 64 + volatile/scripts/test_coverage.sh | 9 + volatile/tests/CMakeLists.txt | 16 + volatile/tests/c_api_test.hpp | 51 + volatile/tests/c_api_test_hash.cpp | 196 + volatile/tests/c_api_test_list.cpp | 367 ++ volatile/tests/stress_test.cpp | 983 +++ volatile/tests/test_util.h | 155 + volatile/tests/tests.cpp | 2573 ++++++++ 330 files changed, 36239 insertions(+), 161 deletions(-) rename {pic => .pic}/kvdk-qrcode.png (100%) rename {pic => .pic}/kvdk_logo.png (100%) create mode 100644 persistent/.gitignore rename CMakeLists.txt => persistent/CMakeLists.txt (99%) create mode 100644 persistent/README.md rename {benchmark => persistent/benchmark}/bench.cpp (99%) rename {benchmark => persistent/benchmark}/generator.hpp (100%) rename {benchmark => persistent/benchmark}/utils/rand64.hpp (100%) rename {benchmark => persistent/benchmark}/utils/range_iterator.hpp (100%) rename {benchmark => persistent/benchmark}/utils/zipf.hpp (100%) rename {cmake => persistent/cmake}/functions.cmake (100%) rename {doc => persistent/doc}/benchmark.md (100%) rename {doc => persistent/doc}/figs/pmem_allocator.png (100%) rename {doc => persistent/doc}/pmem_allocator.md (100%) rename {doc => persistent/doc}/user_doc.md (99%) rename {engine => persistent/engine}/alias.hpp (90%) rename {engine => persistent/engine}/allocator.hpp (100%) rename {engine => persistent/engine}/backup_log.hpp (99%) rename {engine => persistent/engine}/c/kvdk_basic_op.cpp (100%) rename {engine => persistent/engine}/c/kvdk_batch.cpp (100%) rename {engine => persistent/engine}/c/kvdk_c.hpp (84%) rename {engine => persistent/engine}/c/kvdk_hash.cpp (100%) rename {engine => persistent/engine}/c/kvdk_list.cpp (100%) rename {engine => persistent/engine}/c/kvdk_sorted.cpp (100%) rename {engine => persistent/engine}/c/kvdk_string.cpp (100%) rename {engine => persistent/engine}/c/kvdk_transaction.cpp (100%) rename {engine => persistent/engine}/collection.hpp (100%) rename {engine => persistent/engine}/configs.hpp (96%) rename {engine => persistent/engine}/data_record.cpp (100%) rename {engine => persistent/engine}/data_record.hpp (99%) rename {engine => persistent/engine}/dl_list.cpp (100%) rename {engine => persistent/engine}/dl_list.hpp (99%) rename {engine => persistent/engine}/dram_allocator.cpp (100%) rename {engine => persistent/engine}/dram_allocator.hpp (97%) create mode 100644 persistent/engine/engine.cpp rename {engine => persistent/engine}/hash_collection/hash_list.cpp (100%) rename {engine => persistent/engine}/hash_collection/hash_list.hpp (99%) rename {engine => persistent/engine}/hash_collection/iterator.hpp (95%) rename {engine => persistent/engine}/hash_collection/rebuilder.hpp (100%) rename {engine => persistent/engine}/hash_table.cpp (100%) rename {engine => persistent/engine}/hash_table.hpp (99%) rename {engine => persistent/engine}/kv_engine.cpp (99%) rename {engine => persistent/engine}/kv_engine.hpp (99%) rename {engine => persistent/engine}/kv_engine_cleaner.cpp (100%) rename {engine => persistent/engine}/kv_engine_cleaner.hpp (100%) rename {engine => persistent/engine}/kv_engine_hash.cpp (100%) rename {engine => persistent/engine}/kv_engine_list.cpp (100%) rename {engine => persistent/engine}/kv_engine_sorted.cpp (100%) rename {engine => persistent/engine}/kv_engine_string.cpp (100%) rename {engine => persistent/engine}/list_collection/iterator.hpp (95%) rename {engine => persistent/engine}/list_collection/list.cpp (100%) rename {engine => persistent/engine}/list_collection/list.hpp (99%) rename {engine => persistent/engine}/list_collection/rebuilder.hpp (100%) rename {engine => persistent/engine}/lock_table.hpp (100%) rename {engine => persistent/engine}/logger.cpp (100%) create mode 100644 persistent/engine/logger.hpp rename {engine => persistent/engine}/macros.hpp (100%) rename {engine => persistent/engine}/pmem_allocator/free_list.cpp (100%) rename {engine => persistent/engine}/pmem_allocator/free_list.hpp (100%) rename {engine => persistent/engine}/pmem_allocator/pmem_allocator.cpp (100%) rename {engine => persistent/engine}/pmem_allocator/pmem_allocator.hpp (100%) rename {engine => persistent/engine}/snapshot.hpp (100%) rename {engine => persistent/engine}/sorted_collection/iterator.hpp (100%) rename {engine => persistent/engine}/sorted_collection/rebuilder.cpp (100%) rename {engine => persistent/engine}/sorted_collection/rebuilder.hpp (100%) rename {engine => persistent/engine}/sorted_collection/skiplist.cpp (100%) rename {engine => persistent/engine}/sorted_collection/skiplist.hpp (99%) rename {engine => persistent/engine}/structures.hpp (100%) rename {engine => persistent/engine}/thread_manager.cpp (100%) create mode 100644 persistent/engine/thread_manager.hpp rename {engine => persistent/engine}/transaction_impl.cpp (100%) rename {engine => persistent/engine}/transaction_impl.hpp (99%) rename {engine => persistent/engine}/utils/codec.hpp (100%) rename {engine => persistent/engine}/utils/sync_impl.hpp (100%) rename {engine => persistent/engine}/utils/sync_point.cpp (100%) rename {engine => persistent/engine}/utils/sync_point.hpp (100%) rename {engine => persistent/engine}/utils/utils.cpp (100%) rename {engine => persistent/engine}/utils/utils.hpp (100%) rename {engine => persistent/engine}/version/old_records_cleaner.cpp (100%) rename {engine => persistent/engine}/version/old_records_cleaner.hpp (97%) rename {engine => persistent/engine}/version/version_controller.hpp (99%) rename {engine => persistent/engine}/write_batch_impl.cpp (100%) rename {engine => persistent/engine}/write_batch_impl.hpp (99%) rename {examples => persistent/examples}/graph_sim/CMakeLists.txt (100%) rename {examples => persistent/examples}/graph_sim/README.md (100%) rename {examples => persistent/examples}/graph_sim/bench/graph_bench.cpp (100%) rename {examples => persistent/examples}/graph_sim/scripts/build_rocksdb.sh (100%) rename {examples => persistent/examples}/graph_sim/src/coding.hpp (100%) rename {examples => persistent/examples}/graph_sim/src/graph_algorithm/top_n.hpp (100%) rename {examples => persistent/examples}/graph_sim/src/graph_impl.cpp (100%) rename {examples => persistent/examples}/graph_sim/src/graph_impl.hpp (100%) rename {examples => persistent/examples}/graph_sim/src/graph_impl_test.cpp (100%) rename {examples => persistent/examples}/graph_sim/src/kv_engines/KVEngine.hpp (99%) rename {examples => persistent/examples}/graph_sim/src/kv_engines/engine_factory.hpp (100%) rename {examples => persistent/examples}/graph_sim/src/kv_engines/kvdk.cpp (100%) rename {examples => persistent/examples}/graph_sim/src/kv_engines/rocksdb.cpp (100%) rename {examples => persistent/examples}/graph_sim/src/options.hpp (100%) rename {examples => persistent/examples}/kvredis/0001-redis-with-kvdk.patch (100%) rename {examples => persistent/examples}/kvredis/README.md (100%) rename {examples => persistent/examples}/kvredis/bench_redis_kvdk.sh (100%) rename {examples => persistent/examples}/kvredis/build_redis_kvdk.sh (100%) rename {examples => persistent/examples}/kvrocks/0001-use-KVDK-for-set-get-operations.patch (100%) rename {examples => persistent/examples}/kvrocks/README.md (100%) rename {examples => persistent/examples}/tutorial/CMakeLists.txt (100%) rename {examples => persistent/examples}/tutorial/c_api_tutorial.c (99%) rename {examples => persistent/examples}/tutorial/cpp_api_tutorial.cpp (99%) rename {examples => persistent/examples}/tutorial/zset.c (99%) rename {extern => persistent/extern}/gtest (100%) rename {extern => persistent/extern}/libpmemobj++/string_view.hpp (100%) rename {extern => persistent/extern}/xxhash.h (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/comparator.hpp (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/configs.hpp (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/engine.h (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/engine.hpp (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/iterator.hpp (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/snapshot.hpp (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/transaction.hpp (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/types.h (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/types.hpp (100%) rename {include/kvdk => persistent/include/kvdk/persistent}/write_batch.hpp (100%) rename {java => persistent/java}/CMakeLists.txt (100%) rename {java => persistent/java}/README.md (100%) rename {java => persistent/java}/benchmark/pom.xml (100%) rename {java => persistent/java}/benchmark/scripts/benchmark_all.sh (100%) rename {java => persistent/java}/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java (100%) rename {java => persistent/java}/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java (100%) rename {java => persistent/java}/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java (100%) rename {java => persistent/java}/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java (100%) rename {java => persistent/java}/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java (100%) rename {java => persistent/java}/examples/pom.xml (100%) rename {java => persistent/java}/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java (100%) rename {java => persistent/java}/kvdkjni/configs.cc (100%) rename {java => persistent/java}/kvdkjni/cplusplus_to_java_convert.h (100%) rename {java => persistent/java}/kvdkjni/engine.cc (100%) rename {java => persistent/java}/kvdkjni/iterator.cc (100%) rename {java => persistent/java}/kvdkjni/kvdkjni.h (99%) rename {java => persistent/java}/kvdkjni/native_bytes_handle.cc (100%) rename {java => persistent/java}/kvdkjni/write_batch.cc (100%) rename {java => persistent/java}/pom.xml (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/AbstractNativeReference.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/Configs.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/Engine.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/Iterator.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/KVDKException.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/KVDKObject.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/NativeBytesHandle.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/Status.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/WriteBatch.java (100%) rename {java => persistent/java}/src/main/java/io/pmem/kvdk/WriteOptions.java (100%) create mode 100644 persistent/java/src/main/resources/libatomic.so.1 create mode 100644 persistent/java/src/main/resources/libstdc++.so.6 rename {java => persistent/java}/src/test/java/io/pmem/kvdk/ConfigsTest.java (100%) rename {java => persistent/java}/src/test/java/io/pmem/kvdk/EngineTest.java (100%) rename {java => persistent/java}/src/test/java/io/pmem/kvdk/EngineTestBase.java (100%) rename {java => persistent/java}/src/test/java/io/pmem/kvdk/IteratorTest.java (100%) rename {java => persistent/java}/src/test/java/io/pmem/kvdk/WriteBatchTest.java (100%) rename kvdk.pc.in => persistent/kvdk.pc.in (100%) rename {scripts => persistent/scripts}/benchmark_impl.py (100%) rename {scripts => persistent/scripts}/clang_format.sh (100%) rename {scripts => persistent/scripts}/clang_tidy.sh (100%) rename {scripts => persistent/scripts}/cppstyle (100%) rename {scripts => persistent/scripts}/init_devdax.sh (100%) rename {scripts => persistent/scripts}/run_benchmark.py (100%) rename {scripts => persistent/scripts}/test_coverage.sh (100%) rename {tests => persistent/tests}/CMakeLists.txt (100%) rename {tests => persistent/tests}/allocator.hpp (100%) rename {tests => persistent/tests}/c_api_test.hpp (97%) rename {tests => persistent/tests}/c_api_test_hash.cpp (100%) rename {tests => persistent/tests}/c_api_test_list.cpp (100%) rename {tests => persistent/tests}/pmem_allocator_bench.cpp (99%) rename {tests => persistent/tests}/stress_test.cpp (99%) rename {tests => persistent/tests}/test_pmem_allocator.cpp (99%) rename {tests => persistent/tests}/test_util.h (100%) rename {tests => persistent/tests}/tests.cpp (99%) create mode 100644 volatile/.clang-format create mode 100644 volatile/.gitignore create mode 100644 volatile/CMakeLists.txt create mode 100644 volatile/LICENSE create mode 100644 volatile/README.md create mode 100644 volatile/benchmark/bench.cpp create mode 100644 volatile/benchmark/generator.hpp create mode 100644 volatile/benchmark/utils/rand64.hpp create mode 100644 volatile/benchmark/utils/range_iterator.hpp create mode 100644 volatile/benchmark/utils/zipf.hpp create mode 100644 volatile/cmake/functions.cmake create mode 100644 volatile/doc/benchmark.md create mode 100644 volatile/doc/user_doc.md create mode 100644 volatile/engine/alias.hpp create mode 100644 volatile/engine/allocator.hpp create mode 100644 volatile/engine/backup_log.hpp create mode 100644 volatile/engine/c/kvdk_basic_op.cpp create mode 100644 volatile/engine/c/kvdk_batch.cpp create mode 100644 volatile/engine/c/kvdk_c.hpp create mode 100644 volatile/engine/c/kvdk_hash.cpp create mode 100644 volatile/engine/c/kvdk_list.cpp create mode 100644 volatile/engine/c/kvdk_sorted.cpp create mode 100644 volatile/engine/c/kvdk_string.cpp create mode 100644 volatile/engine/collection.hpp create mode 100644 volatile/engine/data_record.cpp create mode 100644 volatile/engine/data_record.hpp create mode 100644 volatile/engine/dl_list.cpp create mode 100644 volatile/engine/dl_list.hpp create mode 100644 volatile/engine/dram_allocator.cpp create mode 100644 volatile/engine/dram_allocator.hpp rename {engine => volatile/engine}/engine.cpp (95%) create mode 100644 volatile/engine/hash_collection/hash_list.cpp create mode 100644 volatile/engine/hash_collection/hash_list.hpp create mode 100644 volatile/engine/hash_collection/iterator.hpp create mode 100644 volatile/engine/hash_table.cpp create mode 100644 volatile/engine/hash_table.hpp create mode 100644 volatile/engine/kv_engine.cpp create mode 100644 volatile/engine/kv_engine.hpp create mode 100644 volatile/engine/kv_engine_cleaner.cpp create mode 100644 volatile/engine/kv_engine_cleaner.hpp create mode 100644 volatile/engine/kv_engine_hash.cpp create mode 100644 volatile/engine/kv_engine_list.cpp create mode 100644 volatile/engine/kv_engine_sorted.cpp create mode 100644 volatile/engine/kv_engine_string.cpp create mode 100644 volatile/engine/list_collection/iterator.hpp create mode 100644 volatile/engine/list_collection/list.cpp create mode 100644 volatile/engine/list_collection/list.hpp create mode 100644 volatile/engine/lock_table.hpp create mode 100644 volatile/engine/logger.cpp rename {engine => volatile/engine}/logger.hpp (94%) create mode 100644 volatile/engine/macros.hpp create mode 100644 volatile/engine/snapshot.hpp create mode 100644 volatile/engine/sorted_collection/iterator.hpp create mode 100644 volatile/engine/sorted_collection/skiplist.cpp create mode 100644 volatile/engine/sorted_collection/skiplist.hpp create mode 100644 volatile/engine/structures.hpp create mode 100644 volatile/engine/thread_manager.cpp rename {engine => volatile/engine}/thread_manager.hpp (96%) create mode 100644 volatile/engine/utils/codec.hpp create mode 100644 volatile/engine/utils/sync_impl.hpp create mode 100644 volatile/engine/utils/sync_point.cpp create mode 100644 volatile/engine/utils/sync_point.hpp create mode 100644 volatile/engine/utils/utils.cpp create mode 100644 volatile/engine/utils/utils.hpp create mode 100644 volatile/engine/version/old_records_cleaner.cpp create mode 100644 volatile/engine/version/old_records_cleaner.hpp create mode 100644 volatile/engine/version/version_controller.hpp create mode 100644 volatile/engine/write_batch_impl.cpp create mode 100644 volatile/engine/write_batch_impl.hpp create mode 100644 volatile/examples/tutorial/CMakeLists.txt create mode 100644 volatile/examples/tutorial/c_api_tutorial.c create mode 100644 volatile/examples/tutorial/cpp_api_tutorial.cpp create mode 100644 volatile/examples/tutorial/zset.c create mode 160000 volatile/extern/gtest create mode 160000 volatile/extern/jemalloc create mode 100644 volatile/extern/jemalloc-cmake/CMakeLists.txt create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_internal_defs.h.in create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_preamble.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_namespace.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_unnamespace.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_defs.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_macros.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle_jet.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos_jet.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_rename.h create mode 100644 volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_typedefs.h create mode 100644 volatile/extern/libpmemobj++/string_view.hpp create mode 160000 volatile/extern/numactl create mode 100644 volatile/extern/numactl-cmake/CMakeLists.txt create mode 100644 volatile/extern/numactl-cmake/include/config.h create mode 100644 volatile/extern/xxhash.h create mode 100644 volatile/include/kvdk/volatile/comparator.hpp create mode 100644 volatile/include/kvdk/volatile/configs.hpp create mode 100644 volatile/include/kvdk/volatile/engine.h create mode 100644 volatile/include/kvdk/volatile/engine.hpp create mode 100644 volatile/include/kvdk/volatile/iterator.hpp create mode 100644 volatile/include/kvdk/volatile/snapshot.hpp create mode 100644 volatile/include/kvdk/volatile/types.h create mode 100644 volatile/include/kvdk/volatile/types.hpp create mode 100644 volatile/include/kvdk/volatile/write_batch.hpp create mode 100644 volatile/java/CMakeLists.txt create mode 100644 volatile/java/README.md create mode 100644 volatile/java/benchmark/pom.xml create mode 100755 volatile/java/benchmark/scripts/benchmark_all.sh create mode 100644 volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java create mode 100644 volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java create mode 100644 volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java create mode 100644 volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java create mode 100644 volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java create mode 100644 volatile/java/examples/pom.xml create mode 100644 volatile/java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java create mode 100644 volatile/java/kvdkjni/configs.cc create mode 100644 volatile/java/kvdkjni/cplusplus_to_java_convert.h create mode 100644 volatile/java/kvdkjni/engine.cc create mode 100644 volatile/java/kvdkjni/iterator.cc create mode 100644 volatile/java/kvdkjni/kvdkjni.h create mode 100644 volatile/java/kvdkjni/native_bytes_handle.cc create mode 100644 volatile/java/kvdkjni/write_batch.cc create mode 100644 volatile/java/pom.xml create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/Configs.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/Engine.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/Iterator.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/KVDKException.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/KVDKObject.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/Status.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/WriteBatch.java create mode 100644 volatile/java/src/main/java/io/pmem/kvdk/WriteOptions.java create mode 100644 volatile/java/src/test/java/io/pmem/kvdk/ConfigsTest.java create mode 100644 volatile/java/src/test/java/io/pmem/kvdk/EngineTest.java create mode 100644 volatile/java/src/test/java/io/pmem/kvdk/EngineTestBase.java create mode 100644 volatile/java/src/test/java/io/pmem/kvdk/IteratorTest.java create mode 100644 volatile/java/src/test/java/io/pmem/kvdk/WriteBatchTest.java create mode 100644 volatile/kvdk.pc.in create mode 100644 volatile/scripts/benchmark_impl.py create mode 100755 volatile/scripts/clang_format.sh create mode 100755 volatile/scripts/clang_tidy.sh create mode 100755 volatile/scripts/cppstyle create mode 100644 volatile/scripts/run_benchmark.py create mode 100755 volatile/scripts/test_coverage.sh create mode 100644 volatile/tests/CMakeLists.txt create mode 100644 volatile/tests/c_api_test.hpp create mode 100644 volatile/tests/c_api_test_hash.cpp create mode 100644 volatile/tests/c_api_test_list.cpp create mode 100644 volatile/tests/stress_test.cpp create mode 100644 volatile/tests/test_util.h create mode 100644 volatile/tests/tests.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36850fa0..11004e46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,14 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Check Java codestyle + - name: Check Persistent Java codestyle run: | - cd java + cd persistent/java + mvn spotless:check + + - name: Check Volatile Java codestyle + run: | + cd volatile/java mvn spotless:check - name: Get cmake @@ -36,8 +41,16 @@ jobs: - name: Init submodules run: git submodule update --init --recursive - - name: Check codestyle & Build + - name: Check Persistent codestyle & Build + run: | + cd persistent + mkdir -p build && cd build + cmake .. -DCMAKE_BUILD_TYPE=Release -DCHECK_CPP_STYLE=ON -DWITH_JNI=ON + make -j + + - name: Check Volatile codestyle & Build run: | + cd volatile mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DCHECK_CPP_STYLE=ON -DWITH_JNI=ON make -j diff --git a/.gitmodules b/.gitmodules index 667caa69..971d1998 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,12 @@ -[submodule "extern/gtest"] - path = extern/gtest +[submodule "volatile/extern/jemalloc"] + path = volatile/extern/jemalloc + url = https://github.com/jemalloc/jemalloc +[submodule "volatile/extern/numactl"] + path = volatile/extern/numactl + url = https://github.com/numactl/numactl +[submodule "persistent/extern/gtest"] + path = persistent/extern/gtest + url = https://github.com/google/googletest.git +[submodule "volatile/extern/gtest"] + path = volatile/extern/gtest url = https://github.com/google/googletest.git diff --git a/pic/kvdk-qrcode.png b/.pic/kvdk-qrcode.png similarity index 100% rename from pic/kvdk-qrcode.png rename to .pic/kvdk-qrcode.png diff --git a/pic/kvdk_logo.png b/.pic/kvdk_logo.png similarity index 100% rename from pic/kvdk_logo.png rename to .pic/kvdk_logo.png diff --git a/README.md b/README.md index decdead5..27e2f8c5 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,19 @@
-


+


-`KVDK` (Key-Value Development Kit) is a key-value store library implemented in C++ language. It is designed for supporting DRAM, Optane persistent memory and CXL memory pool. It also demonstrates several optimization methods for high performance with tiered memory. Besides providing the basic APIs of key-value store, it offers several advanced features, like read-modify-write, checkpoint, etc. +`KVDK` (Key-Value Development Kit) is a key-value store library implemented in C++ language. It is designed for supporting Optane persistent memory, DRAM and CXL memory pool. It also demonstrates several optimization methods for high performance with tiered memory. Besides providing the basic APIs of key-value store, it offers several advanced features, like read-modify-write, checkpoint, etc. -## Features -* Rich data types - * string, sorted, hash, list, hash -* Basic KV operations - * get/put/update/delete/scan -* Read-Modify-Write -* Support TTL -* Atomic Batch Write -* Snapshot based Scan -* Consistent Dump & Restore to/from storage -* Consistent Checkpoint -* Transaction -* C/C++/Java APIs - -# Limitations -* The maximum supported key-value size is 64KB-4GB. -* No approach to key-value compression. -* Users can't expand the persistent memory space on the fly. +**Notice: The DRAM engine and CXL memory pool are in development, you can checkout to v1.0 for a release version of PMEM based persistent engine.** ## Getting the Source ```bash git clone --recurse-submodules https://github.com/pmem/kvdk.git ``` -## Building -### Install dependent tools and libraries on Ubuntu 18.04 -```bash -sudo apt install make clang-format-9 pkg-config g++ autoconf libtool asciidoctor libkmod-dev libudev-dev uuid-dev libjson-c-dev libkeyutils-dev pandoc libhwloc-dev libgflags-dev libtext-diff-perl bash-completion systemd wget git curl - -git clone https://github.com/pmem/ndctl.git -cd ndctl -git checkout v70.1 -./autogen.sh -./configure CFLAGS='-g -O2' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib -make -sudo make install - -git clone https://github.com/pmem/pmdk.git -cd pmdk -git checkout 1.11.1 -make -sudo make install - -wget https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4.tar.gz -tar vzxf cmake-3.12.4.tar.gz -cd cmake-3.12.4 -./bootstrap -make -sudo make install - -``` - -### Install dependent tools and libraries on CentOS 8 -```bash -yum config-manager --add-repo /etc/yum.repos.d/CentOS-Linux-PowerTools.repo -yum config-manager --set-enabled PowerTools - -yum install -y git gcc gcc-c++ autoconf automake asciidoc bash-completion xmlto libtool pkgconfig glib2 glib2-devel libfabric libfabric-devel doxygen graphviz pandoc ncurses kmod kmod-devel libudev-devel libuuid-devel json-c-devel keyutils-libs-devel gem make cmake libarchive clang-tools-extra hwloc-devel perl-Text-Diff gflags-devel curl - -git clone https://github.com/pmem/ndctl.git -cd ndctl -git checkout v70.1 -./autogen.sh -./configure CFLAGS='-g -O2' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib -make -sudo make install - -git clone https://github.com/pmem/pmdk.git -cd pmdk -git checkout 1.11.1 -make -sudo make install - -wget https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4.tar.gz -tar vzxf cmake-3.12.4.tar.gz -cd cmake-3.12.4 -./bootstrap -make -sudo make install -``` - -### Compile KVDK -```bash -mkdir -p build && cd build -cmake .. -DCMAKE_BUILD_TYPE=Release -DCHECK_CPP_STYLE=ON && make -j -``` - -### How to test it on a system without PMEM -```bash -# set the correct path for pmdk library -export LD_LIBRARY_PATH=/usr/local/lib64 - -# setup a tmpfs for test -mkdir /mnt/pmem0 -mount -t tmpfs -o size=2G tmpfs /mnt/pmem0 - -# force the program work on non-pmem directory -export PMEM_IS_PMEM_FORCE=1 - -cd kvdk/build/examples -# Note: this requires CPU supporting AVX512 -./cpp_api_tutorial - -``` - -## Benchmarks -[Here](./doc/benchmark.md) are the examples of how to benchmark the performance of KVDK on your systems. - -## Documentations - -### User Guide - -Please refer to [User guide](./doc/user_doc.md) for API introductions of KVDK. +## User guide -### Architecture +KVDK of different storage type are independently modules, please refer to [persistent](./persistent/README.md) and [volatile](./volatile/README.md) for building, test and docs. # Support Welcome to join the wechat group or slack channel for KVDK tech discussion. diff --git a/persistent/.gitignore b/persistent/.gitignore new file mode 100644 index 00000000..47a99d9c --- /dev/null +++ b/persistent/.gitignore @@ -0,0 +1,53 @@ +# IDE related +.idea/ +cmake-build-* +.vscode/ +.vs/ + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +build/ +.cache/ + +scripts/result* +scripts/__pycache__ + +java/out +java/target +java/benchmark/target +java/examples/target +java/test-libs +java/*.log +java/include/io_pmem_*.h +java/src/main/resources diff --git a/CMakeLists.txt b/persistent/CMakeLists.txt similarity index 99% rename from CMakeLists.txt rename to persistent/CMakeLists.txt index b8f194c8..8cf1b68f 100644 --- a/CMakeLists.txt +++ b/persistent/CMakeLists.txt @@ -156,7 +156,7 @@ add_cppstyle(src ${CMAKE_CURRENT_SOURCE_DIR}/engine/*.c* ${CMAKE_CURRENT_SOURCE_DIR}/engine/utils/*.h* ${CMAKE_CURRENT_SOURCE_DIR}/examples/tutorial/*.c* ${CMAKE_CURRENT_SOURCE_DIR}/benchmark/*.c* - ${CMAKE_CURRENT_SOURCE_DIR}/include/kvdk/*.h* + ${CMAKE_CURRENT_SOURCE_DIR}/include/kvdk/persistent/*.h* ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.c* ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.h*) diff --git a/persistent/README.md b/persistent/README.md new file mode 100644 index 00000000..51c00fa6 --- /dev/null +++ b/persistent/README.md @@ -0,0 +1,126 @@ +
+


+
+ +This is the persistent module of `kvdk`, based on Intel Optane Persistent Memory. + +## Features +* Rich data types + * string, sorted, hash, list, hash +* Basic KV operations + * get/put/update/delete/scan +* Read-Modify-Write +* Support TTL +* Atomic Batch Write +* Snapshot based Scan +* Consistent Dump & Restore to/from storage +* Consistent Checkpoint +* Transaction +* C/C++/Java APIs + +# Limitations +* The maximum supported key-value size is 64KB-4GB. +* No approach to key-value compression. +* Users can't expand the persistent memory space on the fly. + +## Getting the Source +```bash +git clone --recurse-submodules https://github.com/pmem/kvdk.git +``` + +## Building +### Install dependent tools and libraries on Ubuntu 18.04 +```bash +sudo apt install make clang-format-9 pkg-config g++ autoconf libtool asciidoctor libkmod-dev libudev-dev uuid-dev libjson-c-dev libkeyutils-dev pandoc libhwloc-dev libgflags-dev libtext-diff-perl bash-completion systemd wget git curl + +git clone https://github.com/pmem/ndctl.git +cd ndctl +git checkout v70.1 +./autogen.sh +./configure CFLAGS='-g -O2' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib +make +sudo make install + +git clone https://github.com/pmem/pmdk.git +cd pmdk +git checkout 1.11.1 +make +sudo make install + +wget https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4.tar.gz +tar vzxf cmake-3.12.4.tar.gz +cd cmake-3.12.4 +./bootstrap +make +sudo make install + +``` + +### Install dependent tools and libraries on CentOS 8 +```bash +yum config-manager --add-repo /etc/yum.repos.d/CentOS-Linux-PowerTools.repo +yum config-manager --set-enabled PowerTools + +yum install -y git gcc gcc-c++ autoconf automake asciidoc bash-completion xmlto libtool pkgconfig glib2 glib2-devel libfabric libfabric-devel doxygen graphviz pandoc ncurses kmod kmod-devel libudev-devel libuuid-devel json-c-devel keyutils-libs-devel gem make cmake libarchive clang-tools-extra hwloc-devel perl-Text-Diff gflags-devel curl + +git clone https://github.com/pmem/ndctl.git +cd ndctl +git checkout v70.1 +./autogen.sh +./configure CFLAGS='-g -O2' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib +make +sudo make install + +git clone https://github.com/pmem/pmdk.git +cd pmdk +git checkout 1.11.1 +make +sudo make install + +wget https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4.tar.gz +tar vzxf cmake-3.12.4.tar.gz +cd cmake-3.12.4 +./bootstrap +make +sudo make install +``` + +### Compile KVDK +```bash +mkdir -p build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DCHECK_CPP_STYLE=ON && make -j +``` + +### How to test it on a system without PMEM +```bash +# set the correct path for pmdk library +export LD_LIBRARY_PATH=/usr/local/lib64 + +# setup a tmpfs for test +mkdir /mnt/pmem0 +mount -t tmpfs -o size=2G tmpfs /mnt/pmem0 + +# force the program work on non-pmem directory +export PMEM_IS_PMEM_FORCE=1 + +cd kvdk/build/examples +# Note: this requires CPU supporting AVX512 +./cpp_api_tutorial + +``` + +## Benchmarks +[Here](./doc/benchmark.md) are the examples of how to benchmark the performance of KVDK on your systems. + +## Documentations + +### User Guide + +Please refer to [User guide](./doc/user_doc.md) for API introductions of KVDK. + +### Architecture + +# Support +Welcome to join the wechat group or slack channel for KVDK tech discussion. +- [Wechat](https://github.com/pmem/kvdk/issues/143) +- [Slack Channel](https://join.slack.com/t/kvdksupportcommunity/shared_invite/zt-12b66vg1c-4FGb~Ri4w8K2_msau6v86Q) diff --git a/benchmark/bench.cpp b/persistent/benchmark/bench.cpp similarity index 99% rename from benchmark/bench.cpp rename to persistent/benchmark/bench.cpp index 6593bdb6..c1e00124 100644 --- a/benchmark/bench.cpp +++ b/persistent/benchmark/bench.cpp @@ -11,8 +11,8 @@ #include #include "generator.hpp" -#include "kvdk/engine.hpp" -#include "kvdk/types.hpp" +#include "kvdk/persistent/engine.hpp" +#include "kvdk/persistent/types.hpp" using namespace google; using namespace KVDK_NAMESPACE; diff --git a/benchmark/generator.hpp b/persistent/benchmark/generator.hpp similarity index 100% rename from benchmark/generator.hpp rename to persistent/benchmark/generator.hpp diff --git a/benchmark/utils/rand64.hpp b/persistent/benchmark/utils/rand64.hpp similarity index 100% rename from benchmark/utils/rand64.hpp rename to persistent/benchmark/utils/rand64.hpp diff --git a/benchmark/utils/range_iterator.hpp b/persistent/benchmark/utils/range_iterator.hpp similarity index 100% rename from benchmark/utils/range_iterator.hpp rename to persistent/benchmark/utils/range_iterator.hpp diff --git a/benchmark/utils/zipf.hpp b/persistent/benchmark/utils/zipf.hpp similarity index 100% rename from benchmark/utils/zipf.hpp rename to persistent/benchmark/utils/zipf.hpp diff --git a/cmake/functions.cmake b/persistent/cmake/functions.cmake similarity index 100% rename from cmake/functions.cmake rename to persistent/cmake/functions.cmake diff --git a/doc/benchmark.md b/persistent/doc/benchmark.md similarity index 100% rename from doc/benchmark.md rename to persistent/doc/benchmark.md diff --git a/doc/figs/pmem_allocator.png b/persistent/doc/figs/pmem_allocator.png similarity index 100% rename from doc/figs/pmem_allocator.png rename to persistent/doc/figs/pmem_allocator.png diff --git a/doc/pmem_allocator.md b/persistent/doc/pmem_allocator.md similarity index 100% rename from doc/pmem_allocator.md rename to persistent/doc/pmem_allocator.md diff --git a/doc/user_doc.md b/persistent/doc/user_doc.md similarity index 99% rename from doc/user_doc.md rename to persistent/doc/user_doc.md index c982c499..c573f288 100644 --- a/doc/user_doc.md +++ b/persistent/doc/user_doc.md @@ -18,8 +18,8 @@ or how to reopen an existing KVDK instance at the path supplied. (In the following example, PMem is mounted as /mnt/pmem0/ and the KVDK instance is named tutorial_kvdk_example.) ```c++ -#include "kvdk/engine.hpp" -#include "kvdk/namespace.hpp" +#include "kvdk/persistent/engine.hpp" +#include "kvdk/persistent/namespace.hpp" #include #include #include diff --git a/engine/alias.hpp b/persistent/engine/alias.hpp similarity index 90% rename from engine/alias.hpp rename to persistent/engine/alias.hpp index ce93123f..3540e5dd 100644 --- a/engine/alias.hpp +++ b/persistent/engine/alias.hpp @@ -6,7 +6,7 @@ #include -#include "kvdk/types.hpp" +#include "kvdk/persistent/types.hpp" namespace KVDK_NAMESPACE { enum class WriteOp { Put, Delete }; diff --git a/engine/allocator.hpp b/persistent/engine/allocator.hpp similarity index 100% rename from engine/allocator.hpp rename to persistent/engine/allocator.hpp diff --git a/engine/backup_log.hpp b/persistent/engine/backup_log.hpp similarity index 99% rename from engine/backup_log.hpp rename to persistent/engine/backup_log.hpp index bea59fd3..e0acc8fd 100644 --- a/engine/backup_log.hpp +++ b/persistent/engine/backup_log.hpp @@ -10,8 +10,8 @@ #include "configs.hpp" #include "data_record.hpp" -#include "kvdk/configs.hpp" -#include "kvdk/types.hpp" +#include "kvdk/persistent/configs.hpp" +#include "kvdk/persistent/types.hpp" #include "logger.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/c/kvdk_basic_op.cpp b/persistent/engine/c/kvdk_basic_op.cpp similarity index 100% rename from engine/c/kvdk_basic_op.cpp rename to persistent/engine/c/kvdk_basic_op.cpp diff --git a/engine/c/kvdk_batch.cpp b/persistent/engine/c/kvdk_batch.cpp similarity index 100% rename from engine/c/kvdk_batch.cpp rename to persistent/engine/c/kvdk_batch.cpp diff --git a/engine/c/kvdk_c.hpp b/persistent/engine/c/kvdk_c.hpp similarity index 84% rename from engine/c/kvdk_c.hpp rename to persistent/engine/c/kvdk_c.hpp index 318cb623..2686c990 100644 --- a/engine/c/kvdk_c.hpp +++ b/persistent/engine/c/kvdk_c.hpp @@ -8,12 +8,12 @@ #include #include "../alias.hpp" -#include "kvdk/configs.hpp" -#include "kvdk/engine.h" -#include "kvdk/engine.hpp" -#include "kvdk/iterator.hpp" -#include "kvdk/transaction.hpp" -#include "kvdk/write_batch.hpp" +#include "kvdk/persistent/configs.hpp" +#include "kvdk/persistent/engine.h" +#include "kvdk/persistent/engine.hpp" +#include "kvdk/persistent/iterator.hpp" +#include "kvdk/persistent/transaction.hpp" +#include "kvdk/persistent/write_batch.hpp" using kvdk::StringView; diff --git a/engine/c/kvdk_hash.cpp b/persistent/engine/c/kvdk_hash.cpp similarity index 100% rename from engine/c/kvdk_hash.cpp rename to persistent/engine/c/kvdk_hash.cpp diff --git a/engine/c/kvdk_list.cpp b/persistent/engine/c/kvdk_list.cpp similarity index 100% rename from engine/c/kvdk_list.cpp rename to persistent/engine/c/kvdk_list.cpp diff --git a/engine/c/kvdk_sorted.cpp b/persistent/engine/c/kvdk_sorted.cpp similarity index 100% rename from engine/c/kvdk_sorted.cpp rename to persistent/engine/c/kvdk_sorted.cpp diff --git a/engine/c/kvdk_string.cpp b/persistent/engine/c/kvdk_string.cpp similarity index 100% rename from engine/c/kvdk_string.cpp rename to persistent/engine/c/kvdk_string.cpp diff --git a/engine/c/kvdk_transaction.cpp b/persistent/engine/c/kvdk_transaction.cpp similarity index 100% rename from engine/c/kvdk_transaction.cpp rename to persistent/engine/c/kvdk_transaction.cpp diff --git a/engine/collection.hpp b/persistent/engine/collection.hpp similarity index 100% rename from engine/collection.hpp rename to persistent/engine/collection.hpp diff --git a/engine/configs.hpp b/persistent/engine/configs.hpp similarity index 96% rename from engine/configs.hpp rename to persistent/engine/configs.hpp index fa32d081..5b00fc19 100644 --- a/engine/configs.hpp +++ b/persistent/engine/configs.hpp @@ -5,7 +5,7 @@ #pragma once #include "alias.hpp" -#include "kvdk/configs.hpp" +#include "kvdk/persistent/configs.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/data_record.cpp b/persistent/engine/data_record.cpp similarity index 100% rename from engine/data_record.cpp rename to persistent/engine/data_record.cpp diff --git a/engine/data_record.hpp b/persistent/engine/data_record.hpp similarity index 99% rename from engine/data_record.hpp rename to persistent/engine/data_record.hpp index b8ea2283..036d4554 100644 --- a/engine/data_record.hpp +++ b/persistent/engine/data_record.hpp @@ -9,7 +9,7 @@ #include #include "alias.hpp" -#include "kvdk/configs.hpp" +#include "kvdk/persistent/configs.hpp" #include "utils/utils.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/dl_list.cpp b/persistent/engine/dl_list.cpp similarity index 100% rename from engine/dl_list.cpp rename to persistent/engine/dl_list.cpp diff --git a/engine/dl_list.hpp b/persistent/engine/dl_list.hpp similarity index 99% rename from engine/dl_list.hpp rename to persistent/engine/dl_list.hpp index f450fe35..0179d8e9 100644 --- a/engine/dl_list.hpp +++ b/persistent/engine/dl_list.hpp @@ -6,7 +6,7 @@ #include "collection.hpp" #include "data_record.hpp" -#include "kvdk/types.hpp" +#include "kvdk/persistent/types.hpp" #include "lock_table.hpp" #include "pmem_allocator/pmem_allocator.hpp" #include "utils/sync_point.hpp" diff --git a/engine/dram_allocator.cpp b/persistent/engine/dram_allocator.cpp similarity index 100% rename from engine/dram_allocator.cpp rename to persistent/engine/dram_allocator.cpp diff --git a/engine/dram_allocator.hpp b/persistent/engine/dram_allocator.hpp similarity index 97% rename from engine/dram_allocator.hpp rename to persistent/engine/dram_allocator.hpp index 46458f7b..88237767 100644 --- a/engine/dram_allocator.hpp +++ b/persistent/engine/dram_allocator.hpp @@ -12,7 +12,7 @@ #include "alias.hpp" #include "allocator.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "logger.hpp" #include "structures.hpp" diff --git a/persistent/engine/engine.cpp b/persistent/engine/engine.cpp new file mode 100644 index 00000000..f27dbd8d --- /dev/null +++ b/persistent/engine/engine.cpp @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "kvdk/persistent/engine.hpp" + +#include "kv_engine.hpp" + +namespace KVDK_NAMESPACE { +Status Engine::Open(const StringView name, Engine** engine_ptr, + const Configs& configs, FILE* log_file) { + GlobalLogger.Init(log_file, configs.log_level); + Status s = KVEngine::Open(name, engine_ptr, configs); + return s; +} + +Status Engine::Restore(const StringView engine_path, + const StringView backup_file, Engine** engine_ptr, + const Configs& configs, FILE* log_file) { + GlobalLogger.Init(log_file, configs.log_level); + Status s = KVEngine::Restore(engine_path, backup_file, engine_ptr, configs); + return s; +} + +Engine::~Engine() {} +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/engine/hash_collection/hash_list.cpp b/persistent/engine/hash_collection/hash_list.cpp similarity index 100% rename from engine/hash_collection/hash_list.cpp rename to persistent/engine/hash_collection/hash_list.cpp diff --git a/engine/hash_collection/hash_list.hpp b/persistent/engine/hash_collection/hash_list.hpp similarity index 99% rename from engine/hash_collection/hash_list.hpp rename to persistent/engine/hash_collection/hash_list.hpp index 033bf691..f96a1581 100644 --- a/engine/hash_collection/hash_list.hpp +++ b/persistent/engine/hash_collection/hash_list.hpp @@ -2,7 +2,7 @@ #include "../dl_list.hpp" #include "../hash_table.hpp" -#include "kvdk/types.hpp" +#include "kvdk/persistent/types.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/hash_collection/iterator.hpp b/persistent/engine/hash_collection/iterator.hpp similarity index 95% rename from engine/hash_collection/iterator.hpp rename to persistent/engine/hash_collection/iterator.hpp index 39d57fcf..eb1b06b0 100644 --- a/engine/hash_collection/iterator.hpp +++ b/persistent/engine/hash_collection/iterator.hpp @@ -2,8 +2,8 @@ #include "../version/version_controller.hpp" #include "hash_list.hpp" -#include "kvdk/engine.hpp" -#include "kvdk/iterator.hpp" +#include "kvdk/persistent/engine.hpp" +#include "kvdk/persistent/iterator.hpp" namespace KVDK_NAMESPACE { class KVEngine; diff --git a/engine/hash_collection/rebuilder.hpp b/persistent/engine/hash_collection/rebuilder.hpp similarity index 100% rename from engine/hash_collection/rebuilder.hpp rename to persistent/engine/hash_collection/rebuilder.hpp diff --git a/engine/hash_table.cpp b/persistent/engine/hash_table.cpp similarity index 100% rename from engine/hash_table.cpp rename to persistent/engine/hash_table.cpp diff --git a/engine/hash_table.hpp b/persistent/engine/hash_table.hpp similarity index 99% rename from engine/hash_table.hpp rename to persistent/engine/hash_table.hpp index a0d6d6fd..ff5a27cb 100644 --- a/engine/hash_table.hpp +++ b/persistent/engine/hash_table.hpp @@ -12,7 +12,7 @@ #include "alias.hpp" #include "data_record.hpp" #include "dram_allocator.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "pmem_allocator/pmem_allocator.hpp" #include "structures.hpp" diff --git a/engine/kv_engine.cpp b/persistent/engine/kv_engine.cpp similarity index 99% rename from engine/kv_engine.cpp rename to persistent/engine/kv_engine.cpp index cb7cc794..effa20b2 100644 --- a/engine/kv_engine.cpp +++ b/persistent/engine/kv_engine.cpp @@ -21,7 +21,7 @@ #include "configs.hpp" #include "dram_allocator.hpp" #include "hash_collection/iterator.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "list_collection/iterator.hpp" #include "sorted_collection/iterator.hpp" #include "structures.hpp" diff --git a/engine/kv_engine.hpp b/persistent/engine/kv_engine.hpp similarity index 99% rename from engine/kv_engine.hpp rename to persistent/engine/kv_engine.hpp index f577e96b..fe68596f 100644 --- a/engine/kv_engine.hpp +++ b/persistent/engine/kv_engine.hpp @@ -25,7 +25,7 @@ #include "hash_collection/hash_list.hpp" #include "hash_collection/rebuilder.hpp" #include "hash_table.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "list_collection/list.hpp" #include "list_collection/rebuilder.hpp" #include "lock_table.hpp" diff --git a/engine/kv_engine_cleaner.cpp b/persistent/engine/kv_engine_cleaner.cpp similarity index 100% rename from engine/kv_engine_cleaner.cpp rename to persistent/engine/kv_engine_cleaner.cpp diff --git a/engine/kv_engine_cleaner.hpp b/persistent/engine/kv_engine_cleaner.hpp similarity index 100% rename from engine/kv_engine_cleaner.hpp rename to persistent/engine/kv_engine_cleaner.hpp diff --git a/engine/kv_engine_hash.cpp b/persistent/engine/kv_engine_hash.cpp similarity index 100% rename from engine/kv_engine_hash.cpp rename to persistent/engine/kv_engine_hash.cpp diff --git a/engine/kv_engine_list.cpp b/persistent/engine/kv_engine_list.cpp similarity index 100% rename from engine/kv_engine_list.cpp rename to persistent/engine/kv_engine_list.cpp diff --git a/engine/kv_engine_sorted.cpp b/persistent/engine/kv_engine_sorted.cpp similarity index 100% rename from engine/kv_engine_sorted.cpp rename to persistent/engine/kv_engine_sorted.cpp diff --git a/engine/kv_engine_string.cpp b/persistent/engine/kv_engine_string.cpp similarity index 100% rename from engine/kv_engine_string.cpp rename to persistent/engine/kv_engine_string.cpp diff --git a/engine/list_collection/iterator.hpp b/persistent/engine/list_collection/iterator.hpp similarity index 95% rename from engine/list_collection/iterator.hpp rename to persistent/engine/list_collection/iterator.hpp index 09d77499..bbb224c5 100644 --- a/engine/list_collection/iterator.hpp +++ b/persistent/engine/list_collection/iterator.hpp @@ -1,8 +1,8 @@ #pragma once #include "../version/version_controller.hpp" -#include "kvdk/engine.hpp" -#include "kvdk/iterator.hpp" +#include "kvdk/persistent/engine.hpp" +#include "kvdk/persistent/iterator.hpp" #include "list.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/list_collection/list.cpp b/persistent/engine/list_collection/list.cpp similarity index 100% rename from engine/list_collection/list.cpp rename to persistent/engine/list_collection/list.cpp diff --git a/engine/list_collection/list.hpp b/persistent/engine/list_collection/list.hpp similarity index 99% rename from engine/list_collection/list.hpp rename to persistent/engine/list_collection/list.hpp index 999c660b..4212e1de 100644 --- a/engine/list_collection/list.hpp +++ b/persistent/engine/list_collection/list.hpp @@ -5,7 +5,7 @@ #pragma once #include "../dl_list.hpp" -#include "kvdk/types.hpp" +#include "kvdk/persistent/types.hpp" namespace KVDK_NAMESPACE { class ListIteratorImpl; diff --git a/engine/list_collection/rebuilder.hpp b/persistent/engine/list_collection/rebuilder.hpp similarity index 100% rename from engine/list_collection/rebuilder.hpp rename to persistent/engine/list_collection/rebuilder.hpp diff --git a/engine/lock_table.hpp b/persistent/engine/lock_table.hpp similarity index 100% rename from engine/lock_table.hpp rename to persistent/engine/lock_table.hpp diff --git a/engine/logger.cpp b/persistent/engine/logger.cpp similarity index 100% rename from engine/logger.cpp rename to persistent/engine/logger.cpp diff --git a/persistent/engine/logger.hpp b/persistent/engine/logger.hpp new file mode 100644 index 00000000..6713928b --- /dev/null +++ b/persistent/engine/logger.hpp @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include + +#include +#include +#include + +#include "alias.hpp" +#include "kvdk/persistent/configs.hpp" + +#define DO_LOG 1 + +namespace KVDK_NAMESPACE { + +class Logger { + public: + void Info(const char* format, ...); + void Error(const char* format, ...); + void Debug(const char* format, ...); + void Init(FILE* fp, LogLevel level); + + private: + void Log(const char* log_type, const char* format, va_list& args); + + FILE* log_file_ = NULL; + LogLevel level_; + std::mutex mut_; + + std::chrono::time_point start_ts_; +}; + +extern Logger GlobalLogger; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/engine/macros.hpp b/persistent/engine/macros.hpp similarity index 100% rename from engine/macros.hpp rename to persistent/engine/macros.hpp diff --git a/engine/pmem_allocator/free_list.cpp b/persistent/engine/pmem_allocator/free_list.cpp similarity index 100% rename from engine/pmem_allocator/free_list.cpp rename to persistent/engine/pmem_allocator/free_list.cpp diff --git a/engine/pmem_allocator/free_list.hpp b/persistent/engine/pmem_allocator/free_list.hpp similarity index 100% rename from engine/pmem_allocator/free_list.hpp rename to persistent/engine/pmem_allocator/free_list.hpp diff --git a/engine/pmem_allocator/pmem_allocator.cpp b/persistent/engine/pmem_allocator/pmem_allocator.cpp similarity index 100% rename from engine/pmem_allocator/pmem_allocator.cpp rename to persistent/engine/pmem_allocator/pmem_allocator.cpp diff --git a/engine/pmem_allocator/pmem_allocator.hpp b/persistent/engine/pmem_allocator/pmem_allocator.hpp similarity index 100% rename from engine/pmem_allocator/pmem_allocator.hpp rename to persistent/engine/pmem_allocator/pmem_allocator.hpp diff --git a/engine/snapshot.hpp b/persistent/engine/snapshot.hpp similarity index 100% rename from engine/snapshot.hpp rename to persistent/engine/snapshot.hpp diff --git a/engine/sorted_collection/iterator.hpp b/persistent/engine/sorted_collection/iterator.hpp similarity index 100% rename from engine/sorted_collection/iterator.hpp rename to persistent/engine/sorted_collection/iterator.hpp diff --git a/engine/sorted_collection/rebuilder.cpp b/persistent/engine/sorted_collection/rebuilder.cpp similarity index 100% rename from engine/sorted_collection/rebuilder.cpp rename to persistent/engine/sorted_collection/rebuilder.cpp diff --git a/engine/sorted_collection/rebuilder.hpp b/persistent/engine/sorted_collection/rebuilder.hpp similarity index 100% rename from engine/sorted_collection/rebuilder.hpp rename to persistent/engine/sorted_collection/rebuilder.hpp diff --git a/engine/sorted_collection/skiplist.cpp b/persistent/engine/sorted_collection/skiplist.cpp similarity index 100% rename from engine/sorted_collection/skiplist.cpp rename to persistent/engine/sorted_collection/skiplist.cpp diff --git a/engine/sorted_collection/skiplist.hpp b/persistent/engine/sorted_collection/skiplist.hpp similarity index 99% rename from engine/sorted_collection/skiplist.hpp rename to persistent/engine/sorted_collection/skiplist.hpp index a069ca48..9afa1472 100644 --- a/engine/sorted_collection/skiplist.hpp +++ b/persistent/engine/sorted_collection/skiplist.hpp @@ -19,7 +19,7 @@ #include "../structures.hpp" #include "../utils/utils.hpp" #include "../write_batch_impl.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "rebuilder.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/structures.hpp b/persistent/engine/structures.hpp similarity index 100% rename from engine/structures.hpp rename to persistent/engine/structures.hpp diff --git a/engine/thread_manager.cpp b/persistent/engine/thread_manager.cpp similarity index 100% rename from engine/thread_manager.cpp rename to persistent/engine/thread_manager.cpp diff --git a/persistent/engine/thread_manager.hpp b/persistent/engine/thread_manager.hpp new file mode 100644 index 00000000..418bb4ac --- /dev/null +++ b/persistent/engine/thread_manager.hpp @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include + +#include "alias.hpp" +#include "kvdk/persistent/engine.hpp" +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { + +class ThreadManager; + +struct Thread { + public: + Thread() : id(-1), manager(nullptr) {} + int64_t id; + std::shared_ptr manager; + + ~Thread(); +}; + +extern thread_local Thread this_thread; + +class ThreadManager : public std::enable_shared_from_this { + public: + static ThreadManager* Get() { return manager_.get(); } + static int64_t ThreadID() { + Get()->MaybeInitThread(this_thread); + return this_thread.id; + } + void MaybeInitThread(Thread& t); + void Release(Thread& t); + + private: + ThreadManager() : ids_(0), recycle_id_(), spin_() {} + + static std::shared_ptr manager_; + std::atomic ids_; + std::unordered_set recycle_id_; + SpinMutex spin_; +}; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/engine/transaction_impl.cpp b/persistent/engine/transaction_impl.cpp similarity index 100% rename from engine/transaction_impl.cpp rename to persistent/engine/transaction_impl.cpp diff --git a/engine/transaction_impl.hpp b/persistent/engine/transaction_impl.hpp similarity index 99% rename from engine/transaction_impl.hpp rename to persistent/engine/transaction_impl.hpp index 00ef3bf6..8b646e03 100644 --- a/engine/transaction_impl.hpp +++ b/persistent/engine/transaction_impl.hpp @@ -9,7 +9,7 @@ #include #include "alias.hpp" -#include "kvdk/transaction.hpp" +#include "kvdk/persistent/transaction.hpp" #include "write_batch_impl.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/utils/codec.hpp b/persistent/engine/utils/codec.hpp similarity index 100% rename from engine/utils/codec.hpp rename to persistent/engine/utils/codec.hpp diff --git a/engine/utils/sync_impl.hpp b/persistent/engine/utils/sync_impl.hpp similarity index 100% rename from engine/utils/sync_impl.hpp rename to persistent/engine/utils/sync_impl.hpp diff --git a/engine/utils/sync_point.cpp b/persistent/engine/utils/sync_point.cpp similarity index 100% rename from engine/utils/sync_point.cpp rename to persistent/engine/utils/sync_point.cpp diff --git a/engine/utils/sync_point.hpp b/persistent/engine/utils/sync_point.hpp similarity index 100% rename from engine/utils/sync_point.hpp rename to persistent/engine/utils/sync_point.hpp diff --git a/engine/utils/utils.cpp b/persistent/engine/utils/utils.cpp similarity index 100% rename from engine/utils/utils.cpp rename to persistent/engine/utils/utils.cpp diff --git a/engine/utils/utils.hpp b/persistent/engine/utils/utils.hpp similarity index 100% rename from engine/utils/utils.hpp rename to persistent/engine/utils/utils.hpp diff --git a/engine/version/old_records_cleaner.cpp b/persistent/engine/version/old_records_cleaner.cpp similarity index 100% rename from engine/version/old_records_cleaner.cpp rename to persistent/engine/version/old_records_cleaner.cpp diff --git a/engine/version/old_records_cleaner.hpp b/persistent/engine/version/old_records_cleaner.hpp similarity index 97% rename from engine/version/old_records_cleaner.hpp rename to persistent/engine/version/old_records_cleaner.hpp index bb75edc9..a92cb1fb 100644 --- a/engine/version/old_records_cleaner.hpp +++ b/persistent/engine/version/old_records_cleaner.hpp @@ -13,7 +13,7 @@ #include "../kv_engine_cleaner.hpp" #include "../thread_manager.hpp" #include "../utils/utils.hpp" -#include "kvdk/configs.hpp" +#include "kvdk/persistent/configs.hpp" #include "version_controller.hpp" namespace KVDK_NAMESPACE { diff --git a/engine/version/version_controller.hpp b/persistent/engine/version/version_controller.hpp similarity index 99% rename from engine/version/version_controller.hpp rename to persistent/engine/version/version_controller.hpp index 97f1b50f..0b39c44c 100644 --- a/engine/version/version_controller.hpp +++ b/persistent/engine/version/version_controller.hpp @@ -6,7 +6,7 @@ #include "../alias.hpp" #include "../thread_manager.hpp" #include "../utils/utils.hpp" -#include "kvdk/configs.hpp" +#include "kvdk/persistent/configs.hpp" namespace KVDK_NAMESPACE { constexpr TimestampType kMaxTimestamp = UINT64_MAX; diff --git a/engine/write_batch_impl.cpp b/persistent/engine/write_batch_impl.cpp similarity index 100% rename from engine/write_batch_impl.cpp rename to persistent/engine/write_batch_impl.cpp diff --git a/engine/write_batch_impl.hpp b/persistent/engine/write_batch_impl.hpp similarity index 99% rename from engine/write_batch_impl.hpp rename to persistent/engine/write_batch_impl.hpp index 7ce49045..b19ae9ad 100644 --- a/engine/write_batch_impl.hpp +++ b/persistent/engine/write_batch_impl.hpp @@ -8,7 +8,7 @@ #include "alias.hpp" #include "hash_table.hpp" -#include "kvdk/write_batch.hpp" +#include "kvdk/persistent/write_batch.hpp" #include "utils/codec.hpp" #include "utils/utils.hpp" diff --git a/examples/graph_sim/CMakeLists.txt b/persistent/examples/graph_sim/CMakeLists.txt similarity index 100% rename from examples/graph_sim/CMakeLists.txt rename to persistent/examples/graph_sim/CMakeLists.txt diff --git a/examples/graph_sim/README.md b/persistent/examples/graph_sim/README.md similarity index 100% rename from examples/graph_sim/README.md rename to persistent/examples/graph_sim/README.md diff --git a/examples/graph_sim/bench/graph_bench.cpp b/persistent/examples/graph_sim/bench/graph_bench.cpp similarity index 100% rename from examples/graph_sim/bench/graph_bench.cpp rename to persistent/examples/graph_sim/bench/graph_bench.cpp diff --git a/examples/graph_sim/scripts/build_rocksdb.sh b/persistent/examples/graph_sim/scripts/build_rocksdb.sh similarity index 100% rename from examples/graph_sim/scripts/build_rocksdb.sh rename to persistent/examples/graph_sim/scripts/build_rocksdb.sh diff --git a/examples/graph_sim/src/coding.hpp b/persistent/examples/graph_sim/src/coding.hpp similarity index 100% rename from examples/graph_sim/src/coding.hpp rename to persistent/examples/graph_sim/src/coding.hpp diff --git a/examples/graph_sim/src/graph_algorithm/top_n.hpp b/persistent/examples/graph_sim/src/graph_algorithm/top_n.hpp similarity index 100% rename from examples/graph_sim/src/graph_algorithm/top_n.hpp rename to persistent/examples/graph_sim/src/graph_algorithm/top_n.hpp diff --git a/examples/graph_sim/src/graph_impl.cpp b/persistent/examples/graph_sim/src/graph_impl.cpp similarity index 100% rename from examples/graph_sim/src/graph_impl.cpp rename to persistent/examples/graph_sim/src/graph_impl.cpp diff --git a/examples/graph_sim/src/graph_impl.hpp b/persistent/examples/graph_sim/src/graph_impl.hpp similarity index 100% rename from examples/graph_sim/src/graph_impl.hpp rename to persistent/examples/graph_sim/src/graph_impl.hpp diff --git a/examples/graph_sim/src/graph_impl_test.cpp b/persistent/examples/graph_sim/src/graph_impl_test.cpp similarity index 100% rename from examples/graph_sim/src/graph_impl_test.cpp rename to persistent/examples/graph_sim/src/graph_impl_test.cpp diff --git a/examples/graph_sim/src/kv_engines/KVEngine.hpp b/persistent/examples/graph_sim/src/kv_engines/KVEngine.hpp similarity index 99% rename from examples/graph_sim/src/kv_engines/KVEngine.hpp rename to persistent/examples/graph_sim/src/kv_engines/KVEngine.hpp index 9e5d2935..f3168501 100644 --- a/examples/graph_sim/src/kv_engines/KVEngine.hpp +++ b/persistent/examples/graph_sim/src/kv_engines/KVEngine.hpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include #include diff --git a/examples/graph_sim/src/kv_engines/engine_factory.hpp b/persistent/examples/graph_sim/src/kv_engines/engine_factory.hpp similarity index 100% rename from examples/graph_sim/src/kv_engines/engine_factory.hpp rename to persistent/examples/graph_sim/src/kv_engines/engine_factory.hpp diff --git a/examples/graph_sim/src/kv_engines/kvdk.cpp b/persistent/examples/graph_sim/src/kv_engines/kvdk.cpp similarity index 100% rename from examples/graph_sim/src/kv_engines/kvdk.cpp rename to persistent/examples/graph_sim/src/kv_engines/kvdk.cpp diff --git a/examples/graph_sim/src/kv_engines/rocksdb.cpp b/persistent/examples/graph_sim/src/kv_engines/rocksdb.cpp similarity index 100% rename from examples/graph_sim/src/kv_engines/rocksdb.cpp rename to persistent/examples/graph_sim/src/kv_engines/rocksdb.cpp diff --git a/examples/graph_sim/src/options.hpp b/persistent/examples/graph_sim/src/options.hpp similarity index 100% rename from examples/graph_sim/src/options.hpp rename to persistent/examples/graph_sim/src/options.hpp diff --git a/examples/kvredis/0001-redis-with-kvdk.patch b/persistent/examples/kvredis/0001-redis-with-kvdk.patch similarity index 100% rename from examples/kvredis/0001-redis-with-kvdk.patch rename to persistent/examples/kvredis/0001-redis-with-kvdk.patch diff --git a/examples/kvredis/README.md b/persistent/examples/kvredis/README.md similarity index 100% rename from examples/kvredis/README.md rename to persistent/examples/kvredis/README.md diff --git a/examples/kvredis/bench_redis_kvdk.sh b/persistent/examples/kvredis/bench_redis_kvdk.sh similarity index 100% rename from examples/kvredis/bench_redis_kvdk.sh rename to persistent/examples/kvredis/bench_redis_kvdk.sh diff --git a/examples/kvredis/build_redis_kvdk.sh b/persistent/examples/kvredis/build_redis_kvdk.sh similarity index 100% rename from examples/kvredis/build_redis_kvdk.sh rename to persistent/examples/kvredis/build_redis_kvdk.sh diff --git a/examples/kvrocks/0001-use-KVDK-for-set-get-operations.patch b/persistent/examples/kvrocks/0001-use-KVDK-for-set-get-operations.patch similarity index 100% rename from examples/kvrocks/0001-use-KVDK-for-set-get-operations.patch rename to persistent/examples/kvrocks/0001-use-KVDK-for-set-get-operations.patch diff --git a/examples/kvrocks/README.md b/persistent/examples/kvrocks/README.md similarity index 100% rename from examples/kvrocks/README.md rename to persistent/examples/kvrocks/README.md diff --git a/examples/tutorial/CMakeLists.txt b/persistent/examples/tutorial/CMakeLists.txt similarity index 100% rename from examples/tutorial/CMakeLists.txt rename to persistent/examples/tutorial/CMakeLists.txt diff --git a/examples/tutorial/c_api_tutorial.c b/persistent/examples/tutorial/c_api_tutorial.c similarity index 99% rename from examples/tutorial/c_api_tutorial.c rename to persistent/examples/tutorial/c_api_tutorial.c index df5d7fc5..8b792b2e 100644 --- a/examples/tutorial/c_api_tutorial.c +++ b/persistent/examples/tutorial/c_api_tutorial.c @@ -7,7 +7,7 @@ #include #include -#include "kvdk/engine.h" +#include "kvdk/persistent/engine.h" // The KVDK instance is mounted as a directory // /mnt/pmem0/tutorial_kvdk_example. diff --git a/examples/tutorial/cpp_api_tutorial.cpp b/persistent/examples/tutorial/cpp_api_tutorial.cpp similarity index 99% rename from examples/tutorial/cpp_api_tutorial.cpp rename to persistent/examples/tutorial/cpp_api_tutorial.cpp index 63522f7d..caae6a17 100644 --- a/examples/tutorial/cpp_api_tutorial.cpp +++ b/persistent/examples/tutorial/cpp_api_tutorial.cpp @@ -11,7 +11,7 @@ #include #include -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #define DEBUG // For assert diff --git a/examples/tutorial/zset.c b/persistent/examples/tutorial/zset.c similarity index 99% rename from examples/tutorial/zset.c rename to persistent/examples/tutorial/zset.c index 646e408f..2e1935f7 100644 --- a/examples/tutorial/zset.c +++ b/persistent/examples/tutorial/zset.c @@ -5,7 +5,7 @@ // This is a example of single-thread redis zset implementation with kvdk c api #include "assert.h" -#include "kvdk/engine.h" +#include "kvdk/persistent/engine.h" #include "malloc.h" #include "string.h" diff --git a/extern/gtest b/persistent/extern/gtest similarity index 100% rename from extern/gtest rename to persistent/extern/gtest diff --git a/extern/libpmemobj++/string_view.hpp b/persistent/extern/libpmemobj++/string_view.hpp similarity index 100% rename from extern/libpmemobj++/string_view.hpp rename to persistent/extern/libpmemobj++/string_view.hpp diff --git a/extern/xxhash.h b/persistent/extern/xxhash.h similarity index 100% rename from extern/xxhash.h rename to persistent/extern/xxhash.h diff --git a/include/kvdk/comparator.hpp b/persistent/include/kvdk/persistent/comparator.hpp similarity index 100% rename from include/kvdk/comparator.hpp rename to persistent/include/kvdk/persistent/comparator.hpp diff --git a/include/kvdk/configs.hpp b/persistent/include/kvdk/persistent/configs.hpp similarity index 100% rename from include/kvdk/configs.hpp rename to persistent/include/kvdk/persistent/configs.hpp diff --git a/include/kvdk/engine.h b/persistent/include/kvdk/persistent/engine.h similarity index 100% rename from include/kvdk/engine.h rename to persistent/include/kvdk/persistent/engine.h diff --git a/include/kvdk/engine.hpp b/persistent/include/kvdk/persistent/engine.hpp similarity index 100% rename from include/kvdk/engine.hpp rename to persistent/include/kvdk/persistent/engine.hpp diff --git a/include/kvdk/iterator.hpp b/persistent/include/kvdk/persistent/iterator.hpp similarity index 100% rename from include/kvdk/iterator.hpp rename to persistent/include/kvdk/persistent/iterator.hpp diff --git a/include/kvdk/snapshot.hpp b/persistent/include/kvdk/persistent/snapshot.hpp similarity index 100% rename from include/kvdk/snapshot.hpp rename to persistent/include/kvdk/persistent/snapshot.hpp diff --git a/include/kvdk/transaction.hpp b/persistent/include/kvdk/persistent/transaction.hpp similarity index 100% rename from include/kvdk/transaction.hpp rename to persistent/include/kvdk/persistent/transaction.hpp diff --git a/include/kvdk/types.h b/persistent/include/kvdk/persistent/types.h similarity index 100% rename from include/kvdk/types.h rename to persistent/include/kvdk/persistent/types.h diff --git a/include/kvdk/types.hpp b/persistent/include/kvdk/persistent/types.hpp similarity index 100% rename from include/kvdk/types.hpp rename to persistent/include/kvdk/persistent/types.hpp diff --git a/include/kvdk/write_batch.hpp b/persistent/include/kvdk/persistent/write_batch.hpp similarity index 100% rename from include/kvdk/write_batch.hpp rename to persistent/include/kvdk/persistent/write_batch.hpp diff --git a/java/CMakeLists.txt b/persistent/java/CMakeLists.txt similarity index 100% rename from java/CMakeLists.txt rename to persistent/java/CMakeLists.txt diff --git a/java/README.md b/persistent/java/README.md similarity index 100% rename from java/README.md rename to persistent/java/README.md diff --git a/java/benchmark/pom.xml b/persistent/java/benchmark/pom.xml similarity index 100% rename from java/benchmark/pom.xml rename to persistent/java/benchmark/pom.xml diff --git a/java/benchmark/scripts/benchmark_all.sh b/persistent/java/benchmark/scripts/benchmark_all.sh similarity index 100% rename from java/benchmark/scripts/benchmark_all.sh rename to persistent/java/benchmark/scripts/benchmark_all.sh diff --git a/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java b/persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java similarity index 100% rename from java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java rename to persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java diff --git a/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java b/persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java similarity index 100% rename from java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java rename to persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java diff --git a/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java b/persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java similarity index 100% rename from java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java rename to persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java diff --git a/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java b/persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java similarity index 100% rename from java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java rename to persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java diff --git a/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java b/persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java similarity index 100% rename from java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java rename to persistent/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java diff --git a/java/examples/pom.xml b/persistent/java/examples/pom.xml similarity index 100% rename from java/examples/pom.xml rename to persistent/java/examples/pom.xml diff --git a/java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java b/persistent/java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java similarity index 100% rename from java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java rename to persistent/java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java diff --git a/java/kvdkjni/configs.cc b/persistent/java/kvdkjni/configs.cc similarity index 100% rename from java/kvdkjni/configs.cc rename to persistent/java/kvdkjni/configs.cc diff --git a/java/kvdkjni/cplusplus_to_java_convert.h b/persistent/java/kvdkjni/cplusplus_to_java_convert.h similarity index 100% rename from java/kvdkjni/cplusplus_to_java_convert.h rename to persistent/java/kvdkjni/cplusplus_to_java_convert.h diff --git a/java/kvdkjni/engine.cc b/persistent/java/kvdkjni/engine.cc similarity index 100% rename from java/kvdkjni/engine.cc rename to persistent/java/kvdkjni/engine.cc diff --git a/java/kvdkjni/iterator.cc b/persistent/java/kvdkjni/iterator.cc similarity index 100% rename from java/kvdkjni/iterator.cc rename to persistent/java/kvdkjni/iterator.cc diff --git a/java/kvdkjni/kvdkjni.h b/persistent/java/kvdkjni/kvdkjni.h similarity index 99% rename from java/kvdkjni/kvdkjni.h rename to persistent/java/kvdkjni/kvdkjni.h index fa6b9ee7..9424193b 100644 --- a/java/kvdkjni/kvdkjni.h +++ b/persistent/java/kvdkjni/kvdkjni.h @@ -10,7 +10,7 @@ #include -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "kvdkjni/cplusplus_to_java_convert.h" namespace KVDK_NAMESPACE { diff --git a/java/kvdkjni/native_bytes_handle.cc b/persistent/java/kvdkjni/native_bytes_handle.cc similarity index 100% rename from java/kvdkjni/native_bytes_handle.cc rename to persistent/java/kvdkjni/native_bytes_handle.cc diff --git a/java/kvdkjni/write_batch.cc b/persistent/java/kvdkjni/write_batch.cc similarity index 100% rename from java/kvdkjni/write_batch.cc rename to persistent/java/kvdkjni/write_batch.cc diff --git a/java/pom.xml b/persistent/java/pom.xml similarity index 100% rename from java/pom.xml rename to persistent/java/pom.xml diff --git a/java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java b/persistent/java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java rename to persistent/java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java diff --git a/java/src/main/java/io/pmem/kvdk/Configs.java b/persistent/java/src/main/java/io/pmem/kvdk/Configs.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/Configs.java rename to persistent/java/src/main/java/io/pmem/kvdk/Configs.java diff --git a/java/src/main/java/io/pmem/kvdk/Engine.java b/persistent/java/src/main/java/io/pmem/kvdk/Engine.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/Engine.java rename to persistent/java/src/main/java/io/pmem/kvdk/Engine.java diff --git a/java/src/main/java/io/pmem/kvdk/Iterator.java b/persistent/java/src/main/java/io/pmem/kvdk/Iterator.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/Iterator.java rename to persistent/java/src/main/java/io/pmem/kvdk/Iterator.java diff --git a/java/src/main/java/io/pmem/kvdk/KVDKException.java b/persistent/java/src/main/java/io/pmem/kvdk/KVDKException.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/KVDKException.java rename to persistent/java/src/main/java/io/pmem/kvdk/KVDKException.java diff --git a/java/src/main/java/io/pmem/kvdk/KVDKObject.java b/persistent/java/src/main/java/io/pmem/kvdk/KVDKObject.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/KVDKObject.java rename to persistent/java/src/main/java/io/pmem/kvdk/KVDKObject.java diff --git a/java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java b/persistent/java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java rename to persistent/java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java diff --git a/java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java b/persistent/java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java rename to persistent/java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java diff --git a/java/src/main/java/io/pmem/kvdk/Status.java b/persistent/java/src/main/java/io/pmem/kvdk/Status.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/Status.java rename to persistent/java/src/main/java/io/pmem/kvdk/Status.java diff --git a/java/src/main/java/io/pmem/kvdk/WriteBatch.java b/persistent/java/src/main/java/io/pmem/kvdk/WriteBatch.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/WriteBatch.java rename to persistent/java/src/main/java/io/pmem/kvdk/WriteBatch.java diff --git a/java/src/main/java/io/pmem/kvdk/WriteOptions.java b/persistent/java/src/main/java/io/pmem/kvdk/WriteOptions.java similarity index 100% rename from java/src/main/java/io/pmem/kvdk/WriteOptions.java rename to persistent/java/src/main/java/io/pmem/kvdk/WriteOptions.java diff --git a/persistent/java/src/main/resources/libatomic.so.1 b/persistent/java/src/main/resources/libatomic.so.1 new file mode 100644 index 0000000000000000000000000000000000000000..15f244d7554dc1a2316b9a84d877d0f0c30ad719 GIT binary patch literal 26720 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 Px+2~>$szn{?dyL7C7xV3 literal 0 HcmV?d00001 diff --git a/persistent/java/src/main/resources/libstdc++.so.6 b/persistent/java/src/main/resources/libstdc++.so.6 new file mode 100644 index 0000000000000000000000000000000000000000..a82db502a8e52282f5d148f0de8e53304c298eb8 GIT binary patch literal 1594864 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�LI&n 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

n`K1Y8QQbiyHG0`R8!ALuNmk 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

JSxBO(h>ui3&l{G7!xlGS;LGT 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¼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 M-%kWWzlc};KY(?&TL1t6 literal 0 HcmV?d00001 diff --git a/java/src/test/java/io/pmem/kvdk/ConfigsTest.java b/persistent/java/src/test/java/io/pmem/kvdk/ConfigsTest.java similarity index 100% rename from java/src/test/java/io/pmem/kvdk/ConfigsTest.java rename to persistent/java/src/test/java/io/pmem/kvdk/ConfigsTest.java diff --git a/java/src/test/java/io/pmem/kvdk/EngineTest.java b/persistent/java/src/test/java/io/pmem/kvdk/EngineTest.java similarity index 100% rename from java/src/test/java/io/pmem/kvdk/EngineTest.java rename to persistent/java/src/test/java/io/pmem/kvdk/EngineTest.java diff --git a/java/src/test/java/io/pmem/kvdk/EngineTestBase.java b/persistent/java/src/test/java/io/pmem/kvdk/EngineTestBase.java similarity index 100% rename from java/src/test/java/io/pmem/kvdk/EngineTestBase.java rename to persistent/java/src/test/java/io/pmem/kvdk/EngineTestBase.java diff --git a/java/src/test/java/io/pmem/kvdk/IteratorTest.java b/persistent/java/src/test/java/io/pmem/kvdk/IteratorTest.java similarity index 100% rename from java/src/test/java/io/pmem/kvdk/IteratorTest.java rename to persistent/java/src/test/java/io/pmem/kvdk/IteratorTest.java diff --git a/java/src/test/java/io/pmem/kvdk/WriteBatchTest.java b/persistent/java/src/test/java/io/pmem/kvdk/WriteBatchTest.java similarity index 100% rename from java/src/test/java/io/pmem/kvdk/WriteBatchTest.java rename to persistent/java/src/test/java/io/pmem/kvdk/WriteBatchTest.java diff --git a/kvdk.pc.in b/persistent/kvdk.pc.in similarity index 100% rename from kvdk.pc.in rename to persistent/kvdk.pc.in diff --git a/scripts/benchmark_impl.py b/persistent/scripts/benchmark_impl.py similarity index 100% rename from scripts/benchmark_impl.py rename to persistent/scripts/benchmark_impl.py diff --git a/scripts/clang_format.sh b/persistent/scripts/clang_format.sh similarity index 100% rename from scripts/clang_format.sh rename to persistent/scripts/clang_format.sh diff --git a/scripts/clang_tidy.sh b/persistent/scripts/clang_tidy.sh similarity index 100% rename from scripts/clang_tidy.sh rename to persistent/scripts/clang_tidy.sh diff --git a/scripts/cppstyle b/persistent/scripts/cppstyle similarity index 100% rename from scripts/cppstyle rename to persistent/scripts/cppstyle diff --git a/scripts/init_devdax.sh b/persistent/scripts/init_devdax.sh similarity index 100% rename from scripts/init_devdax.sh rename to persistent/scripts/init_devdax.sh diff --git a/scripts/run_benchmark.py b/persistent/scripts/run_benchmark.py similarity index 100% rename from scripts/run_benchmark.py rename to persistent/scripts/run_benchmark.py diff --git a/scripts/test_coverage.sh b/persistent/scripts/test_coverage.sh similarity index 100% rename from scripts/test_coverage.sh rename to persistent/scripts/test_coverage.sh diff --git a/tests/CMakeLists.txt b/persistent/tests/CMakeLists.txt similarity index 100% rename from tests/CMakeLists.txt rename to persistent/tests/CMakeLists.txt diff --git a/tests/allocator.hpp b/persistent/tests/allocator.hpp similarity index 100% rename from tests/allocator.hpp rename to persistent/tests/allocator.hpp diff --git a/tests/c_api_test.hpp b/persistent/tests/c_api_test.hpp similarity index 97% rename from tests/c_api_test.hpp rename to persistent/tests/c_api_test.hpp index 569d1982..6035a534 100644 --- a/tests/c_api_test.hpp +++ b/persistent/tests/c_api_test.hpp @@ -10,7 +10,7 @@ #include #include "gtest/gtest.h" -#include "kvdk/engine.h" +#include "kvdk/persistent/engine.h" #include "test_util.h" class EngineCAPITestBase : public testing::Test { diff --git a/tests/c_api_test_hash.cpp b/persistent/tests/c_api_test_hash.cpp similarity index 100% rename from tests/c_api_test_hash.cpp rename to persistent/tests/c_api_test_hash.cpp diff --git a/tests/c_api_test_list.cpp b/persistent/tests/c_api_test_list.cpp similarity index 100% rename from tests/c_api_test_list.cpp rename to persistent/tests/c_api_test_list.cpp diff --git a/tests/pmem_allocator_bench.cpp b/persistent/tests/pmem_allocator_bench.cpp similarity index 99% rename from tests/pmem_allocator_bench.cpp rename to persistent/tests/pmem_allocator_bench.cpp index 4457eecd..2ca8e101 100644 --- a/tests/pmem_allocator_bench.cpp +++ b/persistent/tests/pmem_allocator_bench.cpp @@ -16,7 +16,7 @@ #include "../engine/kv_engine.hpp" #include "../engine/thread_manager.hpp" #include "allocator.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "test_util.h" using namespace KVDK_NAMESPACE; diff --git a/tests/stress_test.cpp b/persistent/tests/stress_test.cpp similarity index 99% rename from tests/stress_test.cpp rename to persistent/tests/stress_test.cpp index 391ad68f..38819130 100644 --- a/tests/stress_test.cpp +++ b/persistent/tests/stress_test.cpp @@ -17,8 +17,8 @@ #include #include -#include "kvdk/engine.hpp" -#include "kvdk/types.hpp" +#include "kvdk/persistent/engine.hpp" +#include "kvdk/persistent/types.hpp" #include "test_util.h" DEFINE_bool( diff --git a/tests/test_pmem_allocator.cpp b/persistent/tests/test_pmem_allocator.cpp similarity index 99% rename from tests/test_pmem_allocator.cpp rename to persistent/tests/test_pmem_allocator.cpp index 4f644c09..bef57d40 100644 --- a/tests/test_pmem_allocator.cpp +++ b/persistent/tests/test_pmem_allocator.cpp @@ -16,7 +16,7 @@ #include "../engine/logger.hpp" #include "../engine/thread_manager.hpp" #include "gtest/gtest.h" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "pmem_allocator/free_list.hpp" #include "pmem_allocator/pmem_allocator.hpp" #include "test_util.h" diff --git a/tests/test_util.h b/persistent/tests/test_util.h similarity index 100% rename from tests/test_util.h rename to persistent/tests/test_util.h diff --git a/tests/tests.cpp b/persistent/tests/tests.cpp similarity index 99% rename from tests/tests.cpp rename to persistent/tests/tests.cpp index 7f25c086..94856785 100644 --- a/tests/tests.cpp +++ b/persistent/tests/tests.cpp @@ -14,7 +14,7 @@ #include "../engine/kv_engine.hpp" #include "../engine/pmem_allocator/pmem_allocator.hpp" #include "../engine/utils/sync_point.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/persistent/engine.hpp" #include "test_util.h" DEFINE_string(path, "/mnt/pmem0/kvdk_unit_test", diff --git a/volatile/.clang-format b/volatile/.clang-format new file mode 100644 index 00000000..45dafec9 --- /dev/null +++ b/volatile/.clang-format @@ -0,0 +1,5 @@ +# Run manually to reformat a file: +# clang-format-9 -i --style=file +# find . -iname '*.cpp' -o -iname '*.hpp' | xargs clang-format-9 -i --style=file +BasedOnStyle: Google +DerivePointerAlignment: false diff --git a/volatile/.gitignore b/volatile/.gitignore new file mode 100644 index 00000000..47a99d9c --- /dev/null +++ b/volatile/.gitignore @@ -0,0 +1,53 @@ +# IDE related +.idea/ +cmake-build-* +.vscode/ +.vs/ + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +build/ +.cache/ + +scripts/result* +scripts/__pycache__ + +java/out +java/target +java/benchmark/target +java/examples/target +java/test-libs +java/*.log +java/include/io_pmem_*.h +java/src/main/resources diff --git a/volatile/CMakeLists.txt b/volatile/CMakeLists.txt new file mode 100644 index 00000000..39546299 --- /dev/null +++ b/volatile/CMakeLists.txt @@ -0,0 +1,197 @@ +cmake_minimum_required(VERSION 3.12.4) + +project(KVDK VERSION 1.0 + DESCRIPTION "A fast persistent KV engine for Persistent Memory" + LANGUAGES CXX C) + +set(KVDK_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +include(${KVDK_ROOT_DIR}/cmake/functions.cmake) +include(GNUInstallDirs) + +set(CMAKE_CXX_STANDARD 11) + +option(COVERAGE "code coverage" OFF) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx512f -mrdseed -mrdrnd -mclwb -mclflushopt") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + +if (CMAKE_BUILD_TYPE STREQUAL "Release") +elseif (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") +elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") +elseif (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -fsanitize=address -fsanitize-address-use-after-scope -fno-omit-frame-pointer") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -fsanitize=address -fsanitize-address-use-after-scope -fno-omit-frame-pointer") +elseif (CMAKE_BUILD_TYPE STREQUAL "FastDebug") + set(CMAKE_CXX_FLAGS "-Wall -Wextra -g -O2 -march=native") + set(CMAKE_C_FLAGS "-Wall -Wextra -g -O2 -march=native") +else () + message(FATAL_ERROR "Invalid build type!") +endif () + +if (CMAKE_BUILD_TYPE STREQUAL "Release") + add_compile_definitions(KVDK_DEBUG_LEVEL=0) +else () + add_compile_definitions(KVDK_DEBUG_LEVEL=1) + add_compile_definitions(KVDK_ENABLE_CRASHPOINT) +endif () + +# code coverage +if (COVERAGE) + if(NOT ${CMAKE_BUILD_TYPE} MATCHES Debug) + message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading" ) + endif() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage -fprofile-arcs -ftest-coverage") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -fprofile-arcs -ftest-coverage") +endif() + +set (KVDK_SHARED_LIB_LINK_LIBS pthread gflags hwloc atomic) +set (KVDK_STATIC_LIB_LINK_LIBS "") + +# jemalloc +option(WITH_JEMALLOC "Build with jemalloc to support memory management with jemalloc" ON) +if (WITH_JEMALLOC) + message(STATUS "jemalloc support is enabled") + # libnuma static library, to support memory binding on numa nodes + add_subdirectory(extern/numactl-cmake) + + list(APPEND KVDK_SHARED_LIB_LINK_LIBS kvdk_extern_lib::numa) + list(APPEND KVDK_STATIC_LIB_LINK_LIBS kvdk_extern_lib::numa) + + # jemalloc + add_subdirectory(extern/jemalloc-cmake) + add_compile_definitions(KVDK_WITH_JEMALLOC) + + list(APPEND KVDK_SHARED_LIB_LINK_LIBS kvdk_extern_lib::jemalloc) + list(APPEND KVDK_STATIC_LIB_LINK_LIBS kvdk_extern_lib::jemalloc) +else () + message(STATUS "jemalloc support is disabled") +endif() + +# source files +set(SOURCES + engine/c/kvdk_basic_op.cpp + engine/c/kvdk_batch.cpp + engine/c/kvdk_hash.cpp + engine/c/kvdk_list.cpp + engine/c/kvdk_sorted.cpp + engine/c/kvdk_string.cpp + engine/utils/utils.cpp + engine/utils/sync_point.cpp + engine/engine.cpp + engine/kv_engine.cpp + engine/kv_engine_cleaner.cpp + engine/kv_engine_hash.cpp + engine/kv_engine_list.cpp + engine/kv_engine_sorted.cpp + engine/kv_engine_string.cpp + engine/logger.cpp + engine/hash_table.cpp + engine/sorted_collection/skiplist.cpp + engine/hash_collection/hash_list.cpp + engine/list_collection/list.cpp + engine/write_batch_impl.cpp + engine/dram_allocator.cpp + engine/thread_manager.cpp + engine/data_record.cpp + engine/dl_list.cpp + engine/version/old_records_cleaner.cpp + ) + +# .so library +add_library(engine SHARED ${SOURCES}) +target_include_directories(engine PUBLIC ./include ./extern) +target_link_libraries(engine PUBLIC ${KVDK_SHARED_LIB_LINK_LIBS}) + +configure_file(kvdk.pc.in kvdk.pc @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kvdk.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +set(KVDK_PUBLIC_HEADERS + ${PROJECT_SOURCE_DIR}/include/kvdk/comparator.hpp + ${PROJECT_SOURCE_DIR}/include/kvdk/configs.hpp + ${PROJECT_SOURCE_DIR}/include/kvdk/engine.h + ${PROJECT_SOURCE_DIR}/include/kvdk/engine.hpp + ${PROJECT_SOURCE_DIR}/include/kvdk/iterator.hpp + ${PROJECT_SOURCE_DIR}/include/kvdk/write_batch.hpp + ${PROJECT_SOURCE_DIR}/extern/libpmemobj++/string_view.hpp +) + +set_target_properties(engine PROPERTIES PUBLIC_HEADER "${KVDK_PUBLIC_HEADERS}") + +install(TARGETS engine + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/kvdk + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# executables +add_executable(bench benchmark/bench.cpp) +target_link_libraries(bench PUBLIC engine) +target_include_directories(bench PUBLIC ./include ./extern ./) + +option(BUILD_TESTING "Build the tests" ON) +if (BUILD_TESTING) + add_subdirectory(${CMAKE_SOURCE_DIR}/extern/gtest) + add_subdirectory(tests) +endif () + + +option(BUILD_TUTORIAL "Build the tutorial" ON) +add_subdirectory(examples/tutorial) + +option(BUILD_GRAPH_SIMULATOR "Build the graph simulator" ON) + +add_custom_target(checkers ALL) +add_custom_target(cppstyle) +add_custom_target(cppformat) + +find_program(CLANG_FORMAT NAMES clang-format-9 clang-format-9.0 clang-format) +set(CLANG_FORMAT_MINIMUM_REQUIRED "9.0") +if (CLANG_FORMAT) + get_program_version_major_minor(${CLANG_FORMAT} CLANG_FORMAT_VERSION) + message(STATUS "Found clang-format: ${CLANG_FORMAT} (version: ${CLANG_FORMAT_VERSION})") +endif () + +if (CHECK_CPP_STYLE) + if (CLANG_FORMAT) + if (NOT (CLANG_FORMAT_VERSION VERSION_GREATER_EQUAL CLANG_FORMAT_MINIMUM_REQUIRED)) + message(FATAL_ERROR "minimum required clang-format version is ${CCLANG_FORMAT_MINIMUM_REQUIRED}") + endif () + else () + message(FATAL_ERROR "CHECK_CPP_STYLE=ON, but clang-format not found (required version: ${CLANG_FORMAT_MINIMUM_REQUIRED})") + endif () + + add_dependencies(checkers cppstyle) +endif () + +add_cppstyle(src ${CMAKE_CURRENT_SOURCE_DIR}/engine/*.c* + ${CMAKE_CURRENT_SOURCE_DIR}/engine/*.h* + ${CMAKE_CURRENT_SOURCE_DIR}/engine/utils/*.c* + ${CMAKE_CURRENT_SOURCE_DIR}/engine/utils/*.h* + ${CMAKE_CURRENT_SOURCE_DIR}/examples/tutorial/*.c* + ${CMAKE_CURRENT_SOURCE_DIR}/benchmark/*.c* + ${CMAKE_CURRENT_SOURCE_DIR}/include/kvdk/volatile/*.h* + ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.c* + ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.h*) + +option(WITH_JNI "Build with JNI" OFF) +if(WITH_JNI OR JNI) + message(STATUS "JNI library is enabled") + set(KVDK_SHARED_LIB engine) + set(KVDK_STATIC_LIB engine-static) + + include(FindPackageHandleStandardArgs) + + add_library(${KVDK_STATIC_LIB} STATIC ${SOURCES}) + target_include_directories(${KVDK_STATIC_LIB} PUBLIC ./include ./extern) + target_link_libraries(${KVDK_STATIC_LIB} PRIVATE ${KVDK_STATIC_LIB_LINK_LIBS} ${KVDK_SHARED_LIB_LINK_LIBS}) + + set_property(TARGET ${KVDK_STATIC_LIB} PROPERTY POSITION_INDEPENDENT_CODE ON) + + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/java) + + add_cppstyle(jni_src + ${CMAKE_CURRENT_SOURCE_DIR}/java/kvdkjni/*.c* + ${CMAKE_CURRENT_SOURCE_DIR}/java/kvdkjni/*.h*) +else() + message(STATUS "JNI library is disabled") +endif() diff --git a/volatile/LICENSE b/volatile/LICENSE new file mode 100644 index 00000000..e2335064 --- /dev/null +++ b/volatile/LICENSE @@ -0,0 +1,30 @@ +BSD 3-Clause License +Copyright 2021, Intel Corporation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/volatile/README.md b/volatile/README.md new file mode 100644 index 00000000..bc67cb87 --- /dev/null +++ b/volatile/README.md @@ -0,0 +1,122 @@ +

+ +This is the volatile module of `kvdk`, data stored on local DRAM. **This module is in development**. + +## Features +* Rich data types + * string, sorted, hash, list, hash +* Basic KV operations + * get/put/update/delete/scan +* Read-Modify-Write +* Support TTL +* Snapshot based Scan +* Consistent Dump & Restore to/from storage +* C/C++/Java APIs + +# Limitations +* The maximum supported key-value size is 64KB-4GB. +* No approach to key-value compression. + +## Getting the Source +```bash +git clone --recurse-submodules https://github.com/pmem/kvdk.git +``` + +## Building +### Install dependent tools and libraries on Ubuntu 18.04 +```bash +sudo apt install make clang-format-9 pkg-config g++ autoconf libtool asciidoctor libkmod-dev libudev-dev uuid-dev libjson-c-dev libkeyutils-dev pandoc libhwloc-dev libgflags-dev libtext-diff-perl bash-completion systemd wget git curl + +git clone https://github.com/pmem/ndctl.git +cd ndctl +git checkout v70.1 +./autogen.sh +./configure CFLAGS='-g -O2' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib +make +sudo make install + +git clone https://github.com/pmem/pmdk.git +cd pmdk +git checkout 1.11.1 +make +sudo make install + +wget https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4.tar.gz +tar vzxf cmake-3.12.4.tar.gz +cd cmake-3.12.4 +./bootstrap +make +sudo make install + +``` + +### Install dependent tools and libraries on CentOS 8 +```bash +yum config-manager --add-repo /etc/yum.repos.d/CentOS-Linux-PowerTools.repo +yum config-manager --set-enabled PowerTools + +yum install -y git gcc gcc-c++ autoconf automake asciidoc bash-completion xmlto libtool pkgconfig glib2 glib2-devel libfabric libfabric-devel doxygen graphviz pandoc ncurses kmod kmod-devel libudev-devel libuuid-devel json-c-devel keyutils-libs-devel gem make cmake libarchive clang-tools-extra hwloc-devel perl-Text-Diff gflags-devel curl + +git clone https://github.com/pmem/ndctl.git +cd ndctl +git checkout v70.1 +./autogen.sh +./configure CFLAGS='-g -O2' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib +make +sudo make install + +git clone https://github.com/pmem/pmdk.git +cd pmdk +git checkout 1.11.1 +make +sudo make install + +wget https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4.tar.gz +tar vzxf cmake-3.12.4.tar.gz +cd cmake-3.12.4 +./bootstrap +make +sudo make install +``` + +### Compile KVDK +```bash +mkdir -p build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DCHECK_CPP_STYLE=ON && make -j +``` + +### How to test it on a system without PMEM +```bash +# set the correct path for pmdk library +export LD_LIBRARY_PATH=/usr/local/lib64 + +# setup a tmpfs for test +mkdir /mnt/pmem0 +mount -t tmpfs -o size=2G tmpfs /mnt/pmem0 + +# force the program work on non-pmem directory +export PMEM_IS_PMEM_FORCE=1 + +cd kvdk/build/examples +# Note: this requires CPU supporting AVX512 +./cpp_api_tutorial + +``` + +## Benchmarks +[Here](./doc/benchmark.md) are the examples of how to benchmark the performance of KVDK on your systems. + +## Documentations + +### User Guide + +Please refer to [User guide](./doc/user_doc.md) for API introductions of KVDK. + +### Architecture + +# Support +Welcome to join the wechat group or slack channel for KVDK tech discussion. +- [Wechat](https://github.com/pmem/kvdk/issues/143) +- [Slack Channel](https://join.slack.com/t/kvdksupportcommunity/shared_invite/zt-12b66vg1c-4FGb~Ri4w8K2_msau6v86Q) diff --git a/volatile/benchmark/bench.cpp b/volatile/benchmark/bench.cpp new file mode 100644 index 00000000..34f55e6e --- /dev/null +++ b/volatile/benchmark/bench.cpp @@ -0,0 +1,725 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "generator.hpp" +#include "kvdk/volatile/engine.hpp" +#include "kvdk/volatile/types.hpp" + +using namespace google; +using namespace KVDK_NAMESPACE; + +#define MAX_LAT (10000000) + +// Benchmark configs +DEFINE_string(path, "/mnt/pmem0/kvdk", "Instance path"); + +DEFINE_uint64(num_kv, (1 << 30), "Number of KVs to place"); + +DEFINE_uint64(num_operations, (1 << 30), + "Number of total operations. Asserted to be equal to num_kv if " + "(fill == true)."); + +DEFINE_bool(fill, false, "Fill num_kv uniform kv pairs to a new instance"); + +DEFINE_int64(timeout, 30, + "Time to benchmark, this is valid only if fill=false"); + +DEFINE_uint64(value_size, 120, "Value size of KV"); + +DEFINE_string(value_size_distribution, "constant", + "Distribution of value size to write, can be constant/random, " + "default is constant. If set to random, the max value size " + "will be FLAGS_value_size."); + +DEFINE_uint64(threads, 10, "Number of concurrent threads to run benchmark"); + +DEFINE_double(read_ratio, 0, "Read threads = threads * read_ratio"); + +DEFINE_double( + existing_keys_ratio, 1, + "Ratio of keys to read / write that existed in the filled instance, for " + "example, if set to " + "1, all writes will be updates, and all read keys will be existed"); + +DEFINE_bool(latency, false, "Stat operation latencies"); + +DEFINE_string(type, "string", + "Storage engine to benchmark, can be string, sorted, hash, list " + "or blackhole"); + +DEFINE_bool(scan, false, + "If set true, read threads will do scan operations, this is valid " + "only if we benchmark sorted or hash engine"); + +DEFINE_uint64(num_collection, 1, + "Number of collections in the instance to benchmark"); + +DEFINE_uint64( + batch_size, 0, + "Size of write batch. If batch>0, write string type kv with atomic batch " + "write, this is valid only if we benchmark string engine"); + +DEFINE_string(key_distribution, "random", + "Distribution of benchmark keys, if fill is true, this para will " + "be ignored and only uniform distribution will be used"); + +// Engine configs +DEFINE_bool( + populate, false, + "Populate pmem space while creating a new instance. This can improve write " + "performance in runtime, but will take long time to init the instance"); + +DEFINE_uint64(max_access_threads, 64, "Max access threads of the instance"); + +DEFINE_uint64(space, (256ULL << 30), "Max usable PMem space of the instance"); + +DEFINE_bool(opt_large_sorted_collection_restore, true, + " Optional optimization strategy which Multi-thread recovery a " + "skiplist. When having few large skiplists, the optimization can " + "get better performance"); + +DEFINE_bool(use_devdax_mode, false, "Use devdax device for kvdk"); + +class Timer { + public: + void Start() { clock_gettime(CLOCK_REALTIME, &start); } + + std::uint64_t End() { + struct timespec end; + clock_gettime(CLOCK_REALTIME, &end); + return (end.tv_sec - start.tv_sec) * 1000000000 + + (end.tv_nsec - start.tv_nsec); + } + + private: + struct timespec start; +}; + +std::atomic_uint64_t read_ops{0}; +std::atomic_uint64_t write_ops{0}; +std::atomic_uint64_t read_not_found{0}; +std::atomic_uint64_t read_cnt{UINT64_MAX}; +std::vector collections; +Engine* engine{nullptr}; +std::string value_pool; +size_t operations_per_thread; +bool has_timed_out; +std::vector has_finished; // std::vector is a trap! +// Record operation latencies of access threads. Latencies of write threads +// stored in first part of the vector, latencies of read threads stored in +// second part +std::vector> latencies; + +std::vector random_engines; +std::vector ranges; + +enum class DataType { String, Sorted, Hashes, List, Blackhole } bench_data_type; + +enum class KeyDistribution { Range, Uniform, Zipf } key_dist; + +enum class ValueSizeDistribution { Constant, Uniform } vsz_dist; + +std::uint64_t generate_key(size_t tid) { + static std::uint64_t max_key = FLAGS_existing_keys_ratio == 0 + ? UINT64_MAX + : FLAGS_num_kv / FLAGS_existing_keys_ratio; + static extd::zipfian_distribution zipf{max_key, 0.99}; + static std::uniform_int_distribution uniform{0, max_key}; + switch (key_dist) { + case KeyDistribution::Range: { + return ranges[tid].gen(); + } + case KeyDistribution::Uniform: { + return uniform(random_engines[tid].gen); + } + case KeyDistribution::Zipf: { + return zipf(random_engines[tid].gen); + } + default: { + throw; + } + } +} + +size_t generate_value_size(size_t tid) { + switch (vsz_dist) { + case ValueSizeDistribution::Constant: { + return FLAGS_value_size; + } + case ValueSizeDistribution::Uniform: { + return random_engines[tid].gen() % FLAGS_value_size + 1; + } + default: { + throw; + } + } +} + +void DBWrite(int tid) { + std::string key(8, ' '); + std::unique_ptr batch; + if (engine != nullptr) { + batch = engine->WriteBatchCreate(); + } + + for (size_t operations = 0; operations < operations_per_thread; + ++operations) { + if (has_timed_out) { + break; + } + + // generate key + std::uint64_t num = generate_key(tid); + std::uint64_t cid = num % FLAGS_num_collection; + memcpy(&key[0], &num, 8); + StringView value = StringView(value_pool.data(), generate_value_size(tid)); + + Timer timer; + if (FLAGS_latency) timer.Start(); + + Status s; + switch (bench_data_type) { + case DataType::String: { + if (FLAGS_batch_size == 0) { + s = engine->Put(key, value, WriteOptions()); + } else { + batch->StringPut(key, std::string{value.data(), value.size()}); + if (operations % FLAGS_batch_size == 0) { + s = engine->BatchWrite(batch); + batch->Clear(); + } + } + break; + } + case DataType::Sorted: { + if (FLAGS_batch_size == 0) { + s = engine->SortedPut(collections[cid], key, value); + } else { + batch->SortedPut(collections[cid], key, + std::string{value.data(), value.size()}); + if (operations % FLAGS_batch_size == 0) { + s = engine->BatchWrite(batch); + batch->Clear(); + } + } + break; + } + case DataType::Hashes: { + if (FLAGS_batch_size == 0) { + s = engine->HashPut(collections[cid], key, value); + } else { + batch->HashPut(collections[cid], key, + std::string{value.data(), value.size()}); + if (operations % FLAGS_batch_size == 0) { + s = engine->BatchWrite(batch); + batch->Clear(); + } + } + break; + } + case DataType::List: { + s = engine->ListPushFront(collections[cid], value); + break; + } + case DataType::Blackhole: { + s = Status::Ok; + break; + } + default: { + throw std::runtime_error{"Unsupported data type!"}; + } + } + + if (FLAGS_latency) { + std::uint64_t lat = timer.End(); + if (lat / 100 >= MAX_LAT) { + throw std::runtime_error{"Write latency overflow"}; + } + latencies[tid][lat / 100]++; + } + + if (s != Status::Ok) { + throw std::runtime_error{"Put error"}; + } + + if ((operations + 1) % 1000 == 0) { + write_ops.fetch_add(1000); + }; + } + has_finished[tid] = 1; + return; +} + +void DBScan(int tid) { + std::string key(8, ' '); + std::string value_sink; + + for (size_t operations = 0, operations_counted = 0; + operations < operations_per_thread;) { + if (has_timed_out) { + break; + } + + std::uint64_t num = generate_key(tid); + std::uint64_t cid = num % FLAGS_num_collection; + memcpy(&key[0], &num, 8); + + switch (bench_data_type) { + case DataType::Sorted: { + size_t const scan_length = 100; + auto iter = engine->SortedIteratorCreate(collections[cid]); + if (iter) { + iter->Seek(key); + for (size_t i = 0; (i < scan_length) && (iter->Valid()); + i++, iter->Next()) { + key = iter->Key(); + value_sink = iter->Value(); + ++operations; + if (operations > operations_counted + 1000) { + read_ops.fetch_add(operations - operations_counted); + operations_counted = operations; + } + } + } else { + throw std::runtime_error{"Error creating SortedIterator"}; + } + engine->SortedIteratorRelease(iter); + break; + } + case DataType::Hashes: { + auto iter = engine->HashIteratorCreate(collections[cid]); + if (iter) { + for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { + key = iter->Key(); + value_sink = iter->Value(); + ++operations; + if (operations > operations_counted + 1000) { + read_ops += (operations - operations_counted); + operations_counted = operations; + } + } + } else { + throw std::runtime_error{"Error creating HashIterator"}; + } + engine->HashIteratorRelease(iter); + break; + } + case DataType::Blackhole: { + operations += 1024; + read_ops.fetch_add(1024); + break; + } + case DataType::String: + case DataType::List: + default: { + throw std::runtime_error{"Unsupported data type!"}; + } + } + } + + has_finished[tid] = 1; + return; +} + +void DBRead(int tid) { + std::string key(8, ' '); + std::string value_sink; + + std::uint64_t not_found = 0; + for (size_t operations = 0; operations < operations_per_thread; + ++operations) { + if (has_timed_out) { + break; + } + + std::uint64_t num = generate_key(tid); + std::uint64_t cid = num % FLAGS_num_collection; + memcpy(&key[0], &num, 8); + + Timer timer; + if (FLAGS_latency) timer.Start(); + + Status s; + switch (bench_data_type) { + case DataType::String: { + s = engine->Get(key, &value_sink); + break; + } + case DataType::Sorted: { + s = engine->SortedGet(collections[cid], key, &value_sink); + break; + } + case DataType::Hashes: { + s = engine->HashGet(collections[cid], key, &value_sink); + break; + } + case DataType::List: { + s = engine->ListPopBack(collections[cid], &value_sink); + break; + } + case DataType::Blackhole: { + s = Status::Ok; + break; + } + default: { + throw std::runtime_error{"Unsupported!"}; + } + } + + if (FLAGS_latency) { + auto lat = timer.End(); + if (lat / 100 >= MAX_LAT) { + fprintf(stderr, "Read latency overflow: %ld us\n", lat / 100); + std::abort(); + } + latencies[tid][lat / 100]++; + } + + if (s != Status::Ok) { + if (s != Status::NotFound) { + throw std::runtime_error{"Fail to Read"}; + } else { + if (++not_found % 1000 == 0) { + read_not_found.fetch_add(1000); + } + } + } + + if ((operations + 1) % 1000 == 0) { + read_ops.fetch_add(1000); + } + } + + has_finished[tid] = 1; + return; +} + +void ProcessBenchmarkConfigs() { + if (FLAGS_type == "sorted") { + bench_data_type = DataType::Sorted; + } else if (FLAGS_type == "string") { + bench_data_type = DataType::String; + } else if (FLAGS_type == "hash") { + bench_data_type = DataType::Hashes; + } else if (FLAGS_type == "list") { + bench_data_type = DataType::List; + } else if (FLAGS_type == "blackhole") { + bench_data_type = DataType::Blackhole; + } else { + throw std::invalid_argument{"Unsupported data type"}; + } + // Initialize collections and batch parameters + switch (bench_data_type) { + case DataType::String: + case DataType::Blackhole: { + break; + } + case DataType::Hashes: + case DataType::List: + case DataType::Sorted: { + collections.resize(FLAGS_num_collection); + for (size_t i = 0; i < FLAGS_num_collection; i++) { + collections[i] = "Collection_" + std::to_string(i); + } + break; + } + } + + if (FLAGS_batch_size > 0 && (bench_data_type == DataType::List)) { + throw std::invalid_argument{R"(List does not support batch write.)"}; + } + + // Check for scan flag + switch (bench_data_type) { + case DataType::String: + case DataType::List: { + if (FLAGS_scan) { + throw std::invalid_argument{ + R"(Scan is not supported for "String" and "List" type data.)"}; + } + } + default: { + break; + } + } + + if (FLAGS_value_size > 102400) { + throw std::invalid_argument{"value size too large"}; + } + + random_engines.resize(FLAGS_threads); + if (FLAGS_fill) { + assert(FLAGS_read_ratio == 0); + key_dist = KeyDistribution::Range; + operations_per_thread = FLAGS_num_kv / FLAGS_threads + 1; + for (size_t i = 0; i < FLAGS_threads; i++) { + ranges.emplace_back(i * operations_per_thread, + (i + 1) * operations_per_thread); + } + } else { + operations_per_thread = FLAGS_num_operations / FLAGS_threads; + if (FLAGS_key_distribution == "random") { + key_dist = KeyDistribution::Uniform; + } else if (FLAGS_key_distribution == "zipf") { + key_dist = KeyDistribution::Zipf; + } else { + throw std::invalid_argument{"Invalid key distribution"}; + } + } + + if (FLAGS_value_size_distribution == "constant") { + vsz_dist = ValueSizeDistribution::Constant; + } else if (FLAGS_value_size_distribution == "random") { + vsz_dist = ValueSizeDistribution::Uniform; + } else { + throw std::runtime_error{"Invalid value size distribution"}; + } +} + +int main(int argc, char** argv) { + ParseCommandLineFlags(&argc, &argv, true); + ProcessBenchmarkConfigs(); + + if (bench_data_type != DataType::Blackhole) { + Configs configs; + configs.max_access_threads = FLAGS_max_access_threads; + configs.opt_large_sorted_collection_recovery = + FLAGS_opt_large_sorted_collection_restore; + Status s = Engine::Open(FLAGS_path, &engine, configs, stdout); + if (s != Status::Ok) { + throw std::runtime_error{ + std::string{"Fail to open KVDK instance. Status: "} + + KVDKStatusStrings[static_cast(s)]}; + } + } + + { + value_pool.clear(); + value_pool.reserve(FLAGS_value_size); + std::default_random_engine rand_engine{42}; + for (size_t i = 0; i < FLAGS_value_size; i++) { + value_pool.push_back('a' + rand_engine() % 26); + } + } + + size_t write_threads = + FLAGS_fill ? FLAGS_threads + : FLAGS_threads - FLAGS_read_ratio * 100 * FLAGS_threads / 100; + int read_threads = FLAGS_threads - write_threads; + std::vector ts; + + if (FLAGS_latency) { + printf("calculate latencies\n"); + latencies.resize(FLAGS_threads, std::vector(MAX_LAT, 0)); + } + + switch (bench_data_type) { + case DataType::Sorted: { + printf("Create %ld Sorted Collections\n", FLAGS_num_collection); + for (auto col : collections) { + SortedCollectionConfigs s_configs; + Status s = engine->SortedCreate(col, s_configs); + if (s != Status::Ok && s != Status::Existed) { + throw std::runtime_error{"Fail to create Sorted collection"}; + } + } + break; + } + case DataType::Hashes: { + for (auto col : collections) { + Status s = engine->HashCreate(col); + if (s != Status::Ok && s != Status::Existed) { + throw std::runtime_error{"Fail to create Hashset"}; + } + } + break; + } + case DataType::List: { + for (auto col : collections) { + Status s = engine->ListCreate(col); + if (s != Status::Ok && s != Status::Existed) { + throw std::runtime_error{"Fail to create List"}; + } + } + break; + } + default: { + break; + } + } + + has_finished.resize(FLAGS_threads, 0); + + std::cout << "Init " << read_threads << " readers " + << "and " << write_threads << " writers." << std::endl; + + for (size_t i = 0; i < write_threads; i++) { + ts.emplace_back(DBWrite, i); + } + for (size_t i = write_threads; i < FLAGS_threads; i++) { + ts.emplace_back(FLAGS_scan ? DBScan : DBRead, i); + } + + size_t const field_width = 15; + std::cout << "----------------------------------------------------------\n" + << std::setw(field_width) << "Time(ms)" << std::setw(field_width) + << "Read Ops" << std::setw(field_width) << "Write Ops" + << std::setw(field_width) << "Not Found" << std::setw(field_width) + << "Total Read" << std::setw(field_width) << "Total Write" + << std::endl; + + std::vector read_cnt{0}; + std::vector write_cnt{0}; + std::vector notfound_cnt{0}; + size_t last_effective_idx = read_cnt.size(); + auto start_ts = std::chrono::system_clock::now(); + while (true) { + std::this_thread::sleep_for(std::chrono::seconds{1}); + auto duration = std::chrono::duration_cast( + std::chrono::system_clock::now() - start_ts); + + read_cnt.push_back(read_ops.load()); + write_cnt.push_back(write_ops.load()); + notfound_cnt.push_back(read_not_found.load()); + + size_t idx = read_cnt.size() - 1; + std::cout << std::setw(field_width) << duration.count() + << std::setw(field_width) << read_cnt[idx] - read_cnt[idx - 1] + << std::setw(field_width) << write_cnt[idx] - write_cnt[idx - 1] + << std::setw(field_width) + << notfound_cnt[idx] - notfound_cnt[idx - 1] + << std::setw(field_width) << read_cnt[idx] + << std::setw(field_width) << write_cnt[idx] << std::endl; + + size_t num_finished = + std::accumulate(has_finished.begin(), has_finished.end(), 0UL); + + if (num_finished == 0 || idx < 2) { + last_effective_idx = idx; + } + if (num_finished == FLAGS_threads) { + break; + } + if (!FLAGS_fill && (duration.count() >= FLAGS_timeout * 1000)) { + // Signal a timeout for read, scan, update and insert + // Fill will never timeout + has_timed_out = true; + break; + } + } + + std::cout << "Benchmark finished." << std::endl; + printf("finish bench\n"); + + for (size_t i = 0; i < ts.size(); i++) { + ts[i].join(); + } + + size_t time_elapsed; + size_t total_effective_read; + size_t total_effective_write; + size_t const warmup_time = 2; + if (last_effective_idx <= warmup_time) { + time_elapsed = last_effective_idx; + total_effective_read = read_cnt[last_effective_idx]; + total_effective_write = write_cnt[last_effective_idx]; + } else { + time_elapsed = last_effective_idx - warmup_time; + total_effective_read = read_cnt[last_effective_idx] - read_cnt[warmup_time]; + total_effective_write = + write_cnt[last_effective_idx] - write_cnt[warmup_time]; + } + + std::cout << "----------------------------------------------------------\n" + << "Average Read Ops:\t" << total_effective_read / time_elapsed + << ". " + << "Average Write Ops:\t" << total_effective_write / time_elapsed + << std::endl; + + if (FLAGS_latency) { + auto ro = read_ops.load(); + if (ro > 0 && read_threads > 0) { + double total = 0; + double avg = 0; + double cur = 0; + double l50 = 0; + double l99 = 0; + double l995 = 0; + double l999 = 0; + double l9999 = 0; + for (std::uint64_t i = 1; i <= MAX_LAT; i++) { + for (auto j = 0; j < read_threads; j++) { + cur += latencies[write_threads + j][i]; + total += latencies[write_threads + j][i] * i; + if (l50 == 0 && (double)cur / ro > 0.5) { + l50 = (double)i / 10; + } else if (l99 == 0 && (double)cur / ro > 0.99) { + l99 = (double)i / 10; + } else if (l995 == 0 && (double)cur / ro > 0.995) { + l995 = (double)i / 10; + } else if (l999 == 0 && (double)cur / ro > 0.999) { + l999 = (double)i / 10; + } else if (l9999 == 0 && (double)cur / ro > 0.9999) { + l9999 = (double)i / 10; + } + } + } + avg = total / ro / 10; + + printf( + "read lantencies (us): Avg: %.2f, P50: %.2f, P99: %.2f, P99.5: " + "%.2f, " + "P99.9: %.2f, P99.99: %.2f\n", + avg, l50, l99, l995, l999, l9999); + } + + auto wo = write_ops.load(); + if (wo > 0 && write_threads > 0) { + double total = 0; + double avg = 0; + double cur = 0; + double l50 = 0; + double l99 = 0; + double l995 = 0; + double l999 = 0; + double l9999 = 0; + for (std::uint64_t i = 1; i <= MAX_LAT; i++) { + for (size_t j = 0; j < write_threads; j++) { + cur += latencies[j][i]; + total += latencies[j][i] * i; + if (l50 == 0 && (double)cur / wo > 0.5) { + l50 = (double)i / 10; + } else if (l99 == 0 && (double)cur / wo > 0.99) { + l99 = (double)i / 10; + } else if (l995 == 0 && (double)cur / wo > 0.995) { + l995 = (double)i / 10; + } else if (l999 == 0 && (double)cur / wo > 0.999) { + l999 = (double)i / 10; + } else if (l9999 == 0 && (double)cur / wo > 0.9999) { + l9999 = (double)i / 10; + } + } + } + avg = total / wo / 10; + + printf( + "write lantencies (us): Avg: %.2f, P50: %.2f, P99: %.2f, P99.5: " + "%.2f, " + "P99.9: %.2f, P99.99: %.2f\n", + avg, l50, l99, l995, l999, l9999); + } + } + + if (bench_data_type != DataType::Blackhole) delete engine; + + return 0; +} diff --git a/volatile/benchmark/generator.hpp b/volatile/benchmark/generator.hpp new file mode 100644 index 00000000..4db8c06a --- /dev/null +++ b/volatile/benchmark/generator.hpp @@ -0,0 +1,27 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils/rand64.hpp" +#include "utils/range_iterator.hpp" +#include "utils/zipf.hpp" + +struct PaddedEngine { + extd::xorshift_engine gen; + char padding[64]; +}; + +struct PaddedRangeIterators { + extd::range_iterator gen; + PaddedRangeIterators(std::uint64_t lo, std::uint64_t hi) : gen{lo, hi} {} + char padding[64]; +}; diff --git a/volatile/benchmark/utils/rand64.hpp b/volatile/benchmark/utils/rand64.hpp new file mode 100644 index 00000000..5b922f89 --- /dev/null +++ b/volatile/benchmark/utils/rand64.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace extd { +class xorshift_engine { + public: + using result_type = std::uint64_t; + + inline explicit xorshift_engine() { reseed(); } + + inline explicit xorshift_engine(std::uint64_t seed) : s{seed} { + assert(s != 0); + } + + inline result_type operator()() { + update_state(); + return s * magic; + } + + constexpr static result_type min() { + return std::numeric_limits::min(); + } + + constexpr static result_type max() { + return std::numeric_limits::max(); + } + + private: + void reseed() { + size_t failures = 0; + unsigned long long sink; + while (_rdseed64_step(&sink) != 1 || sink == 0) { + ++failures; + if (failures > 10000) throw std::runtime_error{"Fail to seed the engine"}; + } + s = sink; + } + + void update_state() { + if (s == 0) { + reseed(); + } + s ^= (s >> 12); + s ^= (s << 25); + s ^= (s >> 27); + } + + private: + result_type s; + static constexpr result_type magic = 0x2545F4914F6CDD1D; +}; + +} // namespace extd diff --git a/volatile/benchmark/utils/range_iterator.hpp b/volatile/benchmark/utils/range_iterator.hpp new file mode 100644 index 00000000..91ce5170 --- /dev/null +++ b/volatile/benchmark/utils/range_iterator.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace extd { +template ::value, + bool>::type = true> +// Yield Number in range [lower, upper), by step. +class range_iterator { + public: + range_iterator(IntType lo, IntType hi, IntType s = 1) + : lower{lo}, upper{hi}, step{s}, curr{lower} {} + + IntType operator()() { + IntType old = curr; + curr += step; + assert(old < upper); + return old; + } + + private: + IntType const lower; + IntType const upper; + IntType const step; + IntType curr; +}; + +} // namespace extd diff --git a/volatile/benchmark/utils/zipf.hpp b/volatile/benchmark/utils/zipf.hpp new file mode 100644 index 00000000..e041dea0 --- /dev/null +++ b/volatile/benchmark/utils/zipf.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include + +namespace extd { + +// Param: s, N +// P(X=k) = k^{-s}/\sum_{i=1}^N i^{-s} = k^{-s}/zeta(N;s) +template +class zipfian_distribution { + public: + using result_type = IntType; + using float_type = double; + + static constexpr float_type default_s = 0.99; + static constexpr size_t precalc_n = 8; + static constexpr float_type half = 0.5; + + private: + float_type zeta_k[precalc_n]{}; + + result_type const N; + float_type const s; + + float_type const r = 1 - s; + float_type const inv_r = 1 / r; + float_type const a = precalc_n + 0.5; + float_type const a_exp_r = std::pow(a, r); + float_type const zeta_n{zeta(N)}; // Last to initialize! + + public: + inline zipfian_distribution(result_type n, float_type e = default_s) + : N{n}, s{e} { + for (size_t i = 0; i < precalc_n; i++) { + zeta_k[i] = zeta(i + 1); + } + } + + template + inline result_type operator()(UniformBitRandomGenerator& g) const { + using GeneratorIntType = typename UniformBitRandomGenerator::result_type; + static constexpr GeneratorIntType min = UniformBitRandomGenerator::min(); + static constexpr GeneratorIntType max = UniformBitRandomGenerator::max(); + GeneratorIntType r = g(); + float_type sum = (r - min) * zeta_n / (max - min); + assert(0 <= sum && sum <= zeta_n); + return inv_zeta(sum); + } + + inline float_type probability(result_type k) const { + if (k < 1 || k > N) { + return 0.0; + } + return std::pow(k, -s) / zeta_n; + } + + private: + // zeta(k) = sum_{i=1}^{k} i^{-s} + inline constexpr float_type zeta(result_type k) const { + constexpr result_type acc_n = 100; + if (k <= acc_n) { + float_type sum = 0; + for (result_type i = 1; i <= k; i++) { + sum += std::pow(i, -s); + } + return sum; + } else { + float_type a = acc_n + half; + float_type b = k + half; + if (s != 1.0) { + return zeta(acc_n) + (std::pow(b, r) - std::pow(a, r)) / r; + } else { + return zeta(acc_n) + (std::log(b) - std::log(a)); + } + } + } + + // (k = inv_zeta(sum)) <=> (zeta(k) <= sum < zeta(k+1)) + inline result_type inv_zeta(float_type sum) const { + for (size_t i = 0; i < precalc_n; i++) { + if (sum <= zeta_k[i]) { + return i + 1; + } + } + // integral == \int_a^b zeta(x)dx + float_type integral = sum - zeta_k[precalc_n - 1]; + assert(integral > 0); + + if (s != 1.0) { + float_type b = std::pow(integral * r + a_exp_r, inv_r); + assert(a < b && b <= N + half); + return std::floor(b + half); + } else { + float_type b = a * std::exp(integral); + assert(a < b && b <= N + half); + return std::floor(b + half); + } + } +}; + +} // namespace extd diff --git a/volatile/cmake/functions.cmake b/volatile/cmake/functions.cmake new file mode 100644 index 00000000..4849dfc0 --- /dev/null +++ b/volatile/cmake/functions.cmake @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2018-2021, Intel Corporation + +# +# functions.cmake - helper functions for top-level CMakeLists.txt +# + +# Sets ${ret} to version of program specified by ${name} in major.minor format +function(get_program_version_major_minor name ret) + execute_process(COMMAND ${name} --version + OUTPUT_VARIABLE cmd_ret + ERROR_QUIET) + STRING(REGEX MATCH "([0-9]+.)([0-9]+)" VERSION ${cmd_ret}) + SET(${ret} ${VERSION} PARENT_SCOPE) +endfunction() + +# Generates cppstyle-$name and cppformat-$name targets and attaches them +# as dependencies of global "cppformat" target. +# cppstyle-$name target verifies C++ style of files in current source dir. +# cppformat-$name target reformats files in current source dir. +# If more arguments are used, then they are used as files to be checked +# instead. +# ${name} must be unique. +function(add_cppstyle name) + if(NOT CLANG_FORMAT OR NOT (CLANG_FORMAT_VERSION VERSION_GREATER_EQUAL CLANG_FORMAT_MINIMUM_REQUIRED)) + return() + endif() + + if(${ARGC} EQUAL 1) + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/cppstyle-${name}-status + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp + COMMAND ${PERL_EXECUTABLE} + ${KVDK_ROOT_DIR}/scripts/cppstyle + ${CLANG_FORMAT} + check + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp + COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/cppstyle-${name}-status + ) + + add_custom_target(cppformat-${name} + COMMAND ${PERL_EXECUTABLE} + ${KVDK_ROOT_DIR}/scripts/cppstyle + ${CLANG_FORMAT} + format + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp + ) + else() + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/cppstyle-${name}-status + DEPENDS ${ARGN} + COMMAND ${PERL_EXECUTABLE} + ${KVDK_ROOT_DIR}/scripts/cppstyle + ${CLANG_FORMAT} + check + ${ARGN} + COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/cppstyle-${name}-status + ) + + add_custom_target(cppformat-${name} + COMMAND ${PERL_EXECUTABLE} + ${KVDK_ROOT_DIR}/scripts/cppstyle + ${CLANG_FORMAT} + format + ${ARGN} + ) + endif() + + add_custom_target(cppstyle-${name} + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/cppstyle-${name}-status) + + add_dependencies(cppstyle cppstyle-${name}) + add_dependencies(cppformat cppformat-${name}) +endfunction() diff --git a/volatile/doc/benchmark.md b/volatile/doc/benchmark.md new file mode 100644 index 00000000..34311427 --- /dev/null +++ b/volatile/doc/benchmark.md @@ -0,0 +1,152 @@ +# Benchmark tool + +To test performance of KVDK, you can run our benchmark tool "bench", the tool is auto-built along with KVDK library in the build dir. + +You can manually run individual benchmark follow the examples as shown bellow, or simply run our basic benchmark script "scripts/run_benchmark.py" to test all the basic read/write performance. + +To run the script, you shoulf first build kvdk, then run: + +``` +scripts/run_benchmark.py [data_type] [key distribution] +``` + +data_type: Which data type to benchmark, it can be string/sorted/hash/list/blackhole/all + +key distribution: Distribution of key of the benchmark workloads, it can be random/zipf/all +## Fill data to new instance + +To test performance, we need to first fill key-value pairs to the KVDK instance. Since KVDK did not support cross-socket access yet, we need to bind bench program to a numa node: + + numactl --cpunodebind=0 --membind=0 ./bench -fill=1 -value_size=120 -threads=64 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string -populate=1 + +This command will fill 83886088 uniform distributed string-type key-value pairs to the KVDK instance that located at /mnt/pmem0/kvdk. + +Explanation of arguments: + + -fill: Indicates filling data to a new instance. + + -threads: Number of threads of benchmark. + + -space: PMem space that allocate to the KVDK instance. + + -max_access_threads: Max concurrent access threads in the KVDK instance, set it to the number of the hyper-threads for performance consideration. You can call KVDK API with any number of threads, but if your parallel threads more than max_access_threads, the performance will be degraded due to synchronization cost + + -type: Type of key-value pairs to benchmark, it can be "string", "hash" or "sorted". + + -populate: Populate pmem space while creating new KVDK instance for best write performance in runtime, see "include/kvdk/configs.hpp" for explanation. + +## Test read/write performance + +### Read performance + +After fill the instance, we can test read performance with the command below: + + numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=1 -existing_keys_ratio=1 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string + +This will read key-value pairs from the KVDK instance with 48 threads in 10 seconds. + +Explanation of arguments: + + -read_ratio: Ratio of read threads among benchmark threads, for example, if set it to 0.5, then there will be 24 write threads and 24 read threads. + + -existing_keys_ratio: Ratio of keys among key-value pairs to read that already filled in the instance. For example, if set it to 0.5, then 50% read operations will return NotFound. + +Benchmark tool will print performance stats to stdout, include throughput in each second and average ops: + + $numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=1 -existing_keys_ratio=1 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string + + [LOG] time 0 ms: Initializing PMem size 274877906944 in file /mnt/pmem0/kvdk/data + [LOG] time 1864 ms: Map pmem space done + [LOG] time 9033 ms: In restoring: iterated 840882543 records + init 0 write threads + init 64 read threads + ------- ops in seconds ----------- + time (ms), read ops, not found, write ops, total read, total write + 1000 73691000 0 0 73691000 0 + 2001 73613000 0 0 147304000 0 + 3002 73643000 0 0 220947000 0 + 4003 73656000 0 0 294603000 0 + 5004 73675000 0 0 368278000 0 + 6005 73667000 0 0 441945000 0 + 7006 73699000 0 0 515644000 0 + 8007 73647000 0 0 589291000 0 + 9008 73634000 0 0 662925000 0 + 10009 73677000 0 0 736602000 0 + finish bench + ------------ statistics ------------ + read ops 73660400, write ops 0 + [LOG] time 19051 ms: instance closed + + + +### Write performance + +Similarily, to test write performance, we can simply modify "read_ratio": + + numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=0 -existing_keys_ratio=0 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string + +This command will insert new key-value pairs to the KVDK instance in 10 seconds. Likely wise, by modify "existing_keys_ratio", we can control how many write operations are updates. + + $numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=0 -existing_keys_ratio=0 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string + + [LOG] time 0 ms: Initializing PMem size 274877906944 in file /mnt/pmem0/kvdk/data + [LOG] time 1865 ms: Map pmem space done + [LOG] time 9015 ms: In restoring: iterated 840882543 records + init 64 write threads + init 0 read threads + ------- ops in seconds ----------- + time (ms), read ops, not found, write ops, total read, total write + 1000 0 0 50610000 0 50610000 + 2007 0 0 50053000 0 100663000 + 3016 0 0 49669000 0 150332000 + 4017 0 0 49048000 0 199380000 + 5018 0 0 48540000 0 247920000 + 6022 0 0 48210000 0 296130000 + 7023 0 0 47725000 0 343855000 + 8024 0 0 47354000 0 391209000 + 9027 0 0 47080000 0 438289000 + 10028 0 0 46544000 0 484833000 + finish bench + ------------ statistics ------------ + read ops 0, write ops 48483400 + [LOG] time 19055 ms: instance closed + + +### Stat latencies + +We can also stat latency information by add "-latency=1" to the benchmark command. + + $ numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=0.5 -existing_keys_ratio=1 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string -latency=1 + + [LOG] time 0 ms: Initializing PMem size 274877906944 in file /mnt/pmem0/kvdk/data + [LOG] time 1869 ms: Map pmem space done + [LOG] time 14963 ms: In restoring: iterated 1323729106 records + calculate latencies + init 6 write threads + init 58 read threads + ------- ops in seconds ----------- + time (ms), read ops, not found, write ops, total read, total write + 1000 62763000 0 3933000 62763000 3933000 + 2001 62297000 0 4303000 125060000 8236000 + 3002 62190000 0 4530000 187250000 12766000 + 4003 62194000 0 4530000 249444000 17296000 + 5004 62206000 0 4531000 311650000 21827000 + 6005 62172000 0 4527000 373822000 26354000 + 7006 62194000 0 4530000 436016000 30884000 + 8007 62227000 0 4535000 498243000 35419000 + 9008 62196000 0 4529000 560439000 39948000 + 10009 62190000 0 4527000 622629000 44475000 + finish bench + ------------ statistics ------------ + read ops 62263100, write ops 4447500 + read lantencies (us): Avg: 0.89, P50: 0.83, P99: 1.54, P99.5: 1.67, P99.9: 2.77, P99.99: 4.20 + write lantencies (us): Avg: 0.09, P50: 1.22, P99: 2.64, P99.5: 3.25, P99.9: 4.22, P99.99: 5.35 + [LOG] time 28382 ms: instance closed + +## More configurations + +For more configurations of the benchmark tool, please reference to "benchmark/bench.cpp" and "scripts/basic_benchmarks.py". + + + + diff --git a/volatile/doc/user_doc.md b/volatile/doc/user_doc.md new file mode 100644 index 00000000..84f70fe3 --- /dev/null +++ b/volatile/doc/user_doc.md @@ -0,0 +1,437 @@ +KVDK +======= + +KVDK(Key-Value Development Kit) is a Key-Value store for Persistent Memory (PMem). + +KVDK supports basic read and write operations on both sorted and unsorted KV-Pairs, it also support some advanced features, such as **backup**, **checkpoint**, **expire key**, **atomic batch write** and **transactions**. + +Code snippets in this user documents are from `./examples/tutorial/cpp_api_tutorial.cpp`, which is built as `./build/examples/tutorial/cpp_api_tutorial`. + +## Open a KVDK instance + +A KVDK instance is associated with a PMem directory mounted under linux system. Keys and values +are stored in PMem under this path and are indexed by HashTable which is stored in DRAM. +HashTable is not persisted on PMem and is reconstructed with information on PMem. + +The following code shows how to create a KVDK instance if none exist +or how to reopen an existing KVDK instance at the path supplied. +(In the following example, PMem is mounted as /mnt/pmem0/ and the KVDK instance is named tutorial_kvdk_example.) + +```c++ +#include "kvdk/volatile/engine.hpp" +#include "kvdk/volatile/namespace.hpp" +#include +#include +#include +#include +#include +#include + +#define Debug // For assert + +int main() +{ + kvdk::Status status; + kvdk::Engine *engine = nullptr; + + // Initialize a KVDK instance. + { + kvdk::Configs engine_configs; + { + // Configure for a tiny KVDK instance. + // Please refer to "Configuration" section in user documentation for + // details. + engine_configs.hash_bucket_num = (1ull << 10); + } + // The KVDK instance is mounted as a directory + // /mnt/pmem0/tutorial_kvdk_example. + // Modify this path if necessary. + std::string engine_path{"/mnt/pmem0/tutorial_kvdk_example"}; + + // Purge old KVDK instance + int sink = system(std::string{"rm -rf " + engine_path + "\n"}.c_str()); + + status = kvdk::Engine::Open(engine_path, &engine, engine_configs, stdout); + assert(status == kvdk::Status::Ok); + printf("Successfully opened a KVDK instance.\n"); + } + + ... Do something with KVDK instance ... + + ... Close KVDK instance and exit ... +} +``` + +## Status + +`kvdk::Status` indicates status of KVDK function calls. +Functions return `kvdk::Status::Ok` if such a function call is a success. +If exceptions are raised during function calls, other `kvdk::Status` is returned, +such as `kvdk::Status::MemoryOverflow` while no enough memory to allocate. + +## Close a KVDK instance + +To close a KVDK instance, just delete the instance. + +When a KVDK instance is closed, Key-Value pairs are still persisted on PMem. +KV-Pair indexing stored on DRAM are purged. +Follow "Open a KVDK instance" to reopen the KVDK instance will reconstruct indexing on DRAM with information persisted on PMem. + +To purge contents on PMem, just delete its directory under mounted PMem. + +```c++ +int main() +{ + ... Open a KVDK instance as described in "Open a KVDK instance" ... + ... Do something with KVDK instance ... + + // Close KVDK instance. + delete engine; + + // Remove persisted contents on PMem + return system(std::string{"rm -rf " + engine_path + "\n"}.c_str()); +} +``` + +## Data types +KVDK currently supports raw string, sorted collection, hash collection and list data type. + +### Raw String + +All keys and values in a KVDK instance are strings. You can directly store or read key-value pairs in global namespace, which is accessible via Get, Put, Delete and Modify operations, we call them string type data in kvdk. + +Keys are limited to have a maximum size of 64KB. + +A value can be at max 64MB in length by default. The maximum length can be configured when initializing a KVDK instance. + +### Collections + +Instead of raw string, you can organize key-value pairs to a collection, each collection has its own namespace. + +Currently we have three types of collection: + +#### Sorted Collection + +KV pairs are stored with some kind of order (lexicographical order by default) in Sorted Collection, they can be iterated forward or backward starting from an arbitrary point(at a key or between two keys) by an iterator. They can also be directly accessed via SortedGet, SortedPut, SortedDelete operations. + +#### Hash Collection + +Hash Collection is like Raw String with a name space, you can access KV pairs via HashGet, HashPut, HashDelete and HashModify operations. + +In current version, performance of operations on hash collection is similar to sorted collection, which much slower than raw-string, so we recomend use raw-string or sorted collection as high priority. + +#### List + +List is a list of string elements, you can access elems at the front or back via ListPushFront, ListPushBack, ListPopFron, ListPopBack, or operation elems with index via ListInsertAt, ListInsertBefore, ListInsertAfter and ListErase. Notice that operation with index take O(n) time, while operation on front and back only takes O(1). + +### Namespace + +Each collection has its own namespace, so you can store same key in every collection. Howevery, collection name and raw string key are in a same namespace, so you can't assign same name for a collection and a string key, otherwise a error status (Status::WrongType) will be returned. + +## API Examples + +### Reads and Writes with String type + +A KVDK instance provides Get, Put, Delete methods to query/modify/delete raw string kvs. + +The following code performs a series of Get, Put and Delete operations. + +```c++ +int main() +{ + ... Open a KVDK instance as described in "Open a KVDK instance" ... + + // Reads and Writes String KV + { + std::string key1{"key1"}; + std::string key2{"key2"}; + std::string value1{"value1"}; + std::string value2{"value2"}; + std::string v; + + // Insert key1-value1 + status = engine->Put(key1, value1); + assert(status == kvdk::Status::Ok); + + // Get value1 by key1 + status = engine->Get(key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value1); + + // Update key1-value1 to key1-value2 + status = engine->Put(key1, value2); + assert(status == kvdk::Status::Ok); + + // Get value2 by key1 + status = engine->Get(key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value2); + + // Insert key2-value2 + status = engine->Put(key2, value2); + assert(status == kvdk::Status::Ok); + + // Delete key1-value2 + status = engine->Delete(key1); + assert(status == kvdk::Status::Ok); + + // Delete key2-value2 + status = engine->Delete(key2); + assert(status == kvdk::Status::Ok); + + printf("Successfully performed Get, Put, Delete operations on anonymous " + "global collection.\n"); + } + + ... Do something else with KVDK instance ... + + ... Close KVDK instance and exit ... +} +``` + +### Reads and Writes in a Sorted Collection + +A KVDK instance provides SortedGet, SortedPut, SortedDelete methods to query/modify/delete sorted entries. + +The following code performs a series of SortedGet, SortedPut and SortedDelete operations on a sorted collection. + +```c++ +int main() +{ + ... Open a KVDK instance as described in "Open a KVDK instance" ... + + // Reads and Writes on Named Collection + { + std::string collection1{"my_collection_1"}; + std::string collection2{"my_collection_2"}; + std::string key1{"key1"}; + std::string key2{"key2"}; + std::string value1{"value1"}; + std::string value2{"value2"}; + std::string v; + + // You must create sorted collections before you do any operations on them + status = engine->SortedCreate(collection1); + assert(status == kvdk::Status::Ok); + status = engine->SortedCreate(collection2); + assert(status == kvdk::Status::Ok); + + // Insert key1-value1 into "my_collection_1". + status = engine->SortedPut(collection1, key1, value1); + assert(status == kvdk::Status::Ok); + + // Get value1 by key1 in collection "my_collection_1" + status = engine->SortedGet(collection1, key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value1); + + // Insert key1-value2 into "my_collection_2". + status = engine->SortedPut(collection2, key1, value2); + assert(status == kvdk::Status::Ok); + + // Get value2 by key1 in collection "my_collection_2" + status = engine->SortedGet(collection2, key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value2); + + // Get value1 by key1 in collection "my_collection_1" + // key1-value2 is stored in "my_collection_2" + // Thus key1-value1 stored in "my_collection_1" is unaffected by operation + // engine->SortedPut(collection2, key1, value2). + status = engine->SortedGet(collection1, key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value1); + + // Insert key2-value2 into collection "my_collection_2" + // Collection "my_collection_2" already exists and no implicit collection + // creation occurs. + status = engine->SortedPut(collection2, key2, value2); + assert(status == kvdk::Status::Ok); + + // Delete key1-value1 in collection "my_collection_1" + // Although "my_collection_1" has no elements now, the collection itself is + // not deleted though. + status = engine->SortedDelete(collection1, key1); + assert(status == kvdk::Status::Ok); + + // Destroy sorted collections + status = engine->SortedDestroy(collection1); + assert(status == kvdk::Status::Ok); + status = engine->SrotedDestroy(collection2); + assert(status == kvdk::Status::Ok); + + printf("Successfully performed SortedGet, SortedPut, SortedDelete operations.\n"); + } + + ... Do something else with KVDK instance ... + + ... Close KVDK instance and exit ... +} +``` + +### Iterating a Sorted Collection +The following example demonstrates how to iterate through a sorted collection at a consistent view of data. It also demonstrates how to iterate through a range defined by Key. + +```c++ +int main() +{ + ... Open a KVDK instance as described in "Open a KVDK instance" ... + + // Iterating a Sorted Sorted Collection + { + std::string sorted_collection{"my_sorted_collection"}; + engine->SortedCreate(sorted_collection); + // Create toy keys and values. + std::vector> kv_pairs; + for (int i = 0; i < 10; ++i) { + kv_pairs.emplace_back(std::make_pair("key" + std::to_string(i), + "value" + std::to_string(i))); + } + std::shuffle(kv_pairs.begin(), kv_pairs.end(), std::mt19937{42}); + // Print out kv_pairs to check if they are really shuffled. + printf("The shuffled kv-pairs are:\n"); + for (const auto &kv : kv_pairs) + printf("%s\t%s\n", kv.first.c_str(), kv.second.c_str()); + + // Populate collection "my_sorted_collection" with keys and values. + // kv_pairs are not necessarily sorted, but kv-pairs in collection + // "my_sorted_collection" are sorted. + for (int i = 0; i < 10; ++i) { + // Collection "my_sorted_collection" is implicitly created in first + // iteration + status = engine->SortedPut(sorted_collection, kv_pairs[i].first, + kv_pairs[i].second); + assert(status == kvdk::Status::Ok); + } + // Sort kv_pairs for checking the order of "my_sorted_collection". + std::sort(kv_pairs.begin(), kv_pairs.end()); + + // Iterate through collection "my_sorted_collection", the iter is + // created on a consistent view while you create it, e.g. all + // modifications after you create the iter won't be observed + auto iter = engine->SortedIteratorCreate(sorted_collection); + iter->SeekToFirst(); + { + int i = 0; + while (iter->Valid()) { + assert(iter->Key() == kv_pairs[i].first); + assert(iter->Value() == kv_pairs[i].second); + iter->Next(); + ++i; + } + } + + // Iterate through range ["key1", "key8"). + std::string beg{"key1"}; + std::string end{"key8"}; + { + int i = 1; + iter->Seek(beg); + for (iter->Seek(beg); iter->Valid() && iter->Key() < end; iter->Next()) { + assert(iter->Key() == kv_pairs[i].first); + assert(iter->Value() == kv_pairs[i].second); + ++i; + } + } + + // Reversely iterate through range ["key8", "key1"). + beg = "key8"; + end = "key1"; + { + int i = 8; + for (iter->Seek(beg); iter->Valid() && iter->Key() > end; iter->Prev()) { + assert(iter->Key() == kv_pairs[i].first); + assert(iter->Value() == kv_pairs[i].second); + --i; + } + } + + printf("Successfully iterated through a sorted collections.\n"); + engine->SortedIteratorRelease(iter); + } + + ... Do something else with KVDK instance ... + + ... Close KVDK instance and exit ... +} +``` + +### Atomic Updates +KVDK supports organizing a series of Put, Delete operations into a `kvdk::WriteBatch` object as an atomic operation. If KVDK fail to apply the `kvdk::WriteBatch` object as a whole, i.e. the system shuts down during applying the batch, it will roll back to the status right before applying the `kvdk::WriteBatch`. + +```c++ +int main() +{ + ... Open a KVDK instance as described in "Open a KVDK instance" ... + + // BatchWrite on Anonymous Global Collection + { + std::string key1{"key1"}; + std::string key2{"key2"}; + std::string value1{"value1"}; + std::string value2{"value2"}; + std::string v; + + kvdk::WriteBatch batch; + batch.Put(key1, value1); + batch.Put(key1, value2); + batch.Put(key2, value2); + batch.Delete(key2); + + // If the batch is successfully written, there should be only key1-value2 in + // anonymous global collection. + status = engine->BatchWrite(batch); + assert(status == kvdk::Status::Ok); + + // Get value2 by key1 + status = engine->Get(key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value2); + + // Get value2 by key1 + status = engine->Get(key2, &v); + assert(status == kvdk::Status::NotFound); + // v is unchanged, but it is invalid. Always Check kvdk::Status before + // perform further operations! + assert(v == value2); + + printf( + "Successfully performed BatchWrite on anonymous global collection.\n"); + } + + ... Do something else with KVDK instance ... + + ... Close KVDK instance and exit ... +} +``` + +## Concurrency +A KVDK instance can be accessed by multiple read and write threads safely. Synchronization is handled by KVDK implementation. + +## Configuration + +Users can configure KVDK to adapt to their system environment by setting up a `kvdk::Configs` object and passing it to 'kvdk::Engine::Open' when initializing a KVDK instance. + +### Max Access Threads +Maximum number of internal access threads in kvdk is specified by `kvdk::Configs::max_access_threads`. Defaulted to 64. It's recommended to set this number to the number of threads provided by CPU. + +You can call KVDK API with any number of threads, but if your parallel threads more than max_access_threads, the performance will be degraded due to synchronization cost + +### Clean Threads +KVDK reclaim space of updated/deleted data in background with dynamic number of clean threads, you can specify max clean thread number with `kvdk::Configs::clean_threads`. Defaulted to 8, you can config more clean threads in delete intensive workloads to avoid space be exhausted. + +**This parameter is immutable after initialization of the KVDK instance.** + +### HashBucket Size +Specified by `kvdk::Configs::hash_bucket_size`. Defaulted to 128(Bytes). +Larger HashBucket Size will slightly improve performance but will occupy larger space. Please read Architecture Documentation for details before tuning this parameter. + +### Number of HashBucket Chains +Specified by `kvdk::Configs::hash_bucket_num`. Greater number will improve performance by reducing hash conflicts at the cost of greater DRAM space. Please read Architecture Documentation for details before tuning this parameter. + +### Buckets per Slot +Specified by `kvdk::Configs::num_buckets_per_slot`. Smaller number will improve performance by reducing lock contentions and improving caching at the cost of greater DRAM space. Please read Architecture Documentation for details before tuning this parameter. + +## Advanced features and more API + +Please read examples/tutorial for more API and advanced features in KVDK. diff --git a/volatile/engine/alias.hpp b/volatile/engine/alias.hpp new file mode 100644 index 00000000..968de154 --- /dev/null +++ b/volatile/engine/alias.hpp @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include + +#include "kvdk/volatile/types.hpp" + +namespace KVDK_NAMESPACE { +enum class WriteOp { Put, Delete }; + +// Internal types +using MemoryOffsetType = std::uint64_t; +using TimestampType = std::uint64_t; + +const uint64_t kMaxCachedOldRecords = 1024; +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/allocator.hpp b/volatile/engine/allocator.hpp new file mode 100644 index 00000000..bcf13c67 --- /dev/null +++ b/volatile/engine/allocator.hpp @@ -0,0 +1,335 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include + +#include "alias.hpp" +#include "logger.hpp" +#include "macros.hpp" +#include "thread_manager.hpp" + +namespace KVDK_NAMESPACE { + +struct DLRecord; +struct StringRecord; + +constexpr MemoryOffsetType kNullMemoryOffset = UINT64_MAX; + +struct SpaceEntry { + class SpaceCmp { + public: + bool operator()(const SpaceEntry& s1, const SpaceEntry& s2) const { + if (s1.size > s2.size) return true; + if (s1.size == s2.size && s1.offset < s2.offset) return true; + return false; + } + }; + SpaceEntry() = default; + + SpaceEntry(uint64_t _offset, uint64_t _size) : offset(_offset), size(_size) {} + uint64_t offset; + uint64_t size = 0; +}; + +class Allocator { + public: + virtual SpaceEntry Allocate(uint64_t size) = 0; + virtual SpaceEntry AllocateAligned(size_t alignment, uint64_t size) = 0; + virtual void Free(const SpaceEntry& entry) = 0; + virtual std::string AllocatorName() = 0; + + virtual void BatchFree(const std::vector& entries) { + for (const SpaceEntry& entry : entries) { + this->Free(entry); + } + } + + // Set up this allocator for multi-threads access. + // Resource initialization may be involved. + virtual bool SetMaxAccessThreads(uint32_t max_access_threads) { + auto ul = AcquireLock(); + return EnableThreadLocalCounters(max_access_threads); + } + + // Set the backend memory nodes for this allocator. + // Some allocators may not support this feature. + virtual void SetDestMemoryNodes(std::string) {} + + Allocator(char* base_addr, uint64_t max_offset) + : base_addr_(base_addr), max_offset_(max_offset) {} + + virtual ~Allocator() {} + + // Purge a kvdk data record and free it + template + void PurgeAndFree(T* data_record) { + static_assert(std::is_same::value || + std::is_same::value, + ""); + data_record->Destroy(); + Free(SpaceEntry(addr2offset_checked(data_record), + data_record->GetRecordSize())); + } + + inline void LogAllocation(int tid, size_t sz) { + if (KVDK_UNLIKELY(tid == -1 || !thread_local_counter_enabled_)) { + global_allocated_size_.fetch_add(sz); + } else { + assert(tid >= 0); + assert(allocator_thread_cache_.size() > 0); + allocator_thread_cache_[tid % allocator_thread_cache_.size()] + .allocated_size += sz; + } + } + + inline void LogDeallocation(int tid, size_t sz) { + if (KVDK_UNLIKELY(tid == -1 || !thread_local_counter_enabled_)) { + global_allocated_size_.fetch_sub(sz); + } else { + assert(tid >= 0); + assert(allocator_thread_cache_.size() > 0); + allocator_thread_cache_[tid % allocator_thread_cache_.size()] + .allocated_size -= sz; + } + } + + std::int64_t BytesAllocated() const { + std::int64_t total = 0; + for (auto& tc : allocator_thread_cache_) { + total += tc.allocated_size; + } + total += global_allocated_size_.load(); + return total; + } + + std::unique_lock AcquireLock() { + return std::unique_lock{allocator_spin_}; + } + + // translate offset of allocated space to address + inline void* offset2addr_checked(MemoryOffsetType offset) const { + assert(validate_offset(offset) && "Trying to access invalid offset"); + return base_addr_ + offset; + } + + inline void* offset2addr(MemoryOffsetType offset) const { + if (validate_offset(offset)) { + return base_addr_ + offset; + } + return nullptr; + } + + template + inline T* offset2addr_checked(MemoryOffsetType offset) const { + return static_cast(offset2addr_checked(offset)); + } + + template + inline T* offset2addr(MemoryOffsetType offset) const { + return static_cast(offset2addr(offset)); + } + + // translate address of allocated space to offset + inline MemoryOffsetType addr2offset_checked(const void* addr) const { + assert((char*)addr >= base_addr_); + MemoryOffsetType offset = (char*)addr - base_addr_; + assert(validate_offset(offset) && "Trying to create invalid offset"); + return offset; + } + + inline MemoryOffsetType addr2offset(const void* addr) const { + if (addr) { + MemoryOffsetType offset = (char*)addr - base_addr_; + if (validate_offset(offset)) { + return offset; + } + } + return kNullMemoryOffset; + } + + inline bool validate_offset(uint64_t offset) const { + return offset < max_offset_; + } + + protected: + /** + * @brief Enable thread local memory usage counters. + * This helps to avoid possible bottleneck when multiple threads + * access the std::atomic `global_allocated_size_`. + * + * @param max_access_threads + * @return true Success + * @return false Failure + */ + bool EnableThreadLocalCounters(uint32_t max_access_threads) { + if (max_access_threads < 1) { + GlobalLogger.Error( + "Invalid number of thread local counters for Allocator: %u.\n", + max_access_threads); + return false; + } + + thread_local_counter_enabled_ = true; + allocator_thread_cache_.resize(max_access_threads); + + return true; + } + + private: + struct alignas(64) AllocatorThreadCache { + int64_t allocated_size; + // align to cache line to avoid cache conherence + char padding[64 - sizeof(int64_t)]; + }; + static_assert(sizeof(AllocatorThreadCache) % 64 == 0); + + char* base_addr_; + uint64_t max_offset_; + + bool thread_local_counter_enabled_{}; + std::vector allocator_thread_cache_; + std::atomic global_allocated_size_{0}; + + SpinMutex allocator_spin_; +}; + +// Global default memory allocator +Allocator* global_memory_allocator(); + +/// Caution: AlignedPoolAllocator is not thread-safe +template +class AlignedPoolAllocator { + static_assert(alignof(T) <= 1024, + "Alignment greater than 1024B not supported"); + + public: + using value_type = T; + + explicit inline AlignedPoolAllocator() : pools_{}, pos_{TrunkSize} {} + + inline AlignedPoolAllocator(AlignedPoolAllocator const&) + : AlignedPoolAllocator{} {} + AlignedPoolAllocator(AlignedPoolAllocator&&) = delete; + ~AlignedPoolAllocator() { + for (auto p : pools_) { + free(p); + } + } + + inline T* allocate(size_t n) { + if (pools_.capacity() < 64) { + pools_.reserve(64); + } + + if (pos_ + n <= TrunkSize) { + size_t old_pos = pos_; + pos_ += n; + return &pools_.back()[old_pos]; + } else if (n <= TrunkSize) { + allocate_trunk(); + pos_ = n; + return &pools_.back()[0]; + } else { + allocate_trunk(n); + pos_ = TrunkSize; + return &pools_.back()[0]; + } + } + + inline void deallocate(T*, size_t) noexcept { return; } + + private: + inline void allocate_trunk(size_t sz = TrunkSize) { + pools_.emplace_back( + static_cast(aligned_alloc(alignof(T), sizeof(T) * sz))); + if (pools_.back() == nullptr || alignof(pools_.back()[0]) != alignof(T)) { + throw std::bad_alloc{}; + } + } + + static constexpr size_t TrunkSize = 1024; + std::vector pools_; + size_t pos_; +}; + +// Thread safety guaranteed by aligned_alloc +template +class AlignedAllocator { + public: + using value_type = T; + + inline T* allocate(size_t n) { + static_assert(sizeof(T) % alignof(T) == 0); + T* p = static_cast(aligned_alloc(alignof(T), n * sizeof(T))); + if (p == nullptr) { + throw std::bad_alloc{}; + } + return p; + } + + inline void deallocate(T* p, size_t) noexcept { free(p); } +}; + +template +class Array { + public: + template + explicit Array(uint64_t size, Args&&... args) : size_(size) { + static_assert(sizeof(T) % alignof(T) == 0); + allocated = alloc_->AllocateAligned(alignof(T), size * sizeof(T)); + if (allocated.size == 0) { + throw std::bad_alloc{}; + } + + data_ = alloc_->offset2addr(allocated.offset); + for (uint64_t i = 0; i < size; i++) { + new (data_ + i) T{std::forward(args)...}; + } + } + + Array(const Array&) = delete; + Array& operator=(const Array&) = delete; + Array(Array&&) = delete; + + Array() : size_(0), data_(nullptr){}; + + ~Array() { + if (data_ != nullptr) { + for (uint64_t i = 0; i < size_; i++) { + data_[i].~T(); + } + alloc_->Free(allocated); + } + } + + T& back() { + assert(size_ > 0); + return data_[size_ - 1]; + } + + T& front() { + assert(size_ > 0); + return data_[0]; + } + + T& operator[](uint64_t index) { + if (index >= size_) { + throw std::out_of_range("array out of range"); + } + return data_[index]; + } + + uint64_t size() { return size_; } + + private: + uint64_t const size_; + T* data_{nullptr}; + Allocator* alloc_ = global_memory_allocator(); + SpaceEntry allocated; +}; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/backup_log.hpp b/volatile/engine/backup_log.hpp new file mode 100644 index 00000000..6013aa82 --- /dev/null +++ b/volatile/engine/backup_log.hpp @@ -0,0 +1,278 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include +#include +#include + +#include + +#include "data_record.hpp" +#include "kvdk/volatile/configs.hpp" +#include "kvdk/volatile/types.hpp" +#include "logger.hpp" + +namespace KVDK_NAMESPACE { + +constexpr size_t kMaxBackupLogBufferSize = 128 << 20; + +enum class BackupStage { + NotFinished = 0, + Finished, +}; + +// Persist/Read backup of a kvdk instance as log-like manner +// Format: +// stage | string record 1 | string record 2 | sorted header record 1 | sorted +// elems of header 1 | ... sorted header n | sorted elems of header n | ... | +// string record n| ... +class BackupLog { + public: + struct LogRecord { + RecordType type; + std::string key; + std::string val; + ExpireTimeType expire_time; + + bool DecodeFrom(StringView* str) { + return FetchUint32(str, (uint32_t*)&type) && + FetchFixedString(str, &key) & FetchFixedString(str, &val) && + FetchUint64(str, (uint64_t*)&expire_time); + } + + void EncodeTo(std::string* str) { + assert(str != nullptr); + AppendUint32(str, type); + AppendFixedString(str, key); + AppendFixedString(str, val); + AppendUint64(str, expire_time); + } + }; + + // Iterate log records on a backup log + class LogIterator { + public: + LogIterator(StringView records_on_file) + : records_on_file_(records_on_file) { + valid_ = curr_.DecodeFrom(&records_on_file_); + } + + const LogRecord& Record() { return curr_; } + void Next() { + if (Valid()) { + valid_ = curr_.DecodeFrom(&records_on_file_); + } + } + + bool Valid() { return valid_; } + + private: + StringView records_on_file_; + LogRecord curr_; + bool valid_ = true; + }; + + ~BackupLog() { Close(); } + + BackupLog() = default; + BackupLog(const BackupLog&) = delete; + + // Init a new backup log + Status Init(const std::string& backup_log) { + Status s = Status::Ok; + if (file_exist(backup_log)) { + GlobalLogger.Error("Init backup log %s error: file already exist\n", + backup_log.c_str()); + s = Status::Abort; + } + if (s == Status::Ok) { + file_name_ = backup_log; + fd_ = open(backup_log.c_str(), O_CREAT | O_RDWR, 0666); + if (fd_ < 0) { + GlobalLogger.Error("Init bakcup log %s error while opening file: %s\n", + backup_log.c_str(), strerror(errno)); + s = Status::IOError; + } + } + + if (s == Status::Ok) { + stage_ = BackupStage::NotFinished; + file_size_ = write(fd_, &stage_, sizeof(BackupStage)); + if (file_size_ != sizeof(BackupStage)) { + GlobalLogger.Error( + "Init bakcup log %s error while writing backup stage: %s\n", + backup_log.c_str(), strerror(errno)); + s = Status::IOError; + } + } + + if (s == Status::Ok) { + log_file_ = mmap(nullptr, sizeof(BackupStage), PROT_WRITE | PROT_READ, + MAP_SHARED, fd_, 0); + if (log_file_ == MAP_FAILED) { + GlobalLogger.Error( + "Init bakcup log %s error while mapping log file: %s\n", + backup_log.c_str(), strerror(errno)); + log_file_ = nullptr; + s = Status::IOError; + } + } + + if (s != Status::Ok) { + Destroy(); + } + return s; + } + + // Open a existing backup log + Status Open(const std::string& backup_log) { + Status s = Status::Ok; + file_name_ = backup_log; + fd_ = open(backup_log.c_str(), O_RDWR, 0666); + if (fd_ < 0) { + GlobalLogger.Error("Open bakcup log %s error while opening file: %s\n", + backup_log.c_str(), strerror(errno)); + s = Status::IOError; + } + + if (s == Status::Ok) { + file_size_ = lseek(fd_, 0, SEEK_END); + if (file_size_ < sizeof(BackupStage)) { + GlobalLogger.Error( + "Open backup log file %s error: file size %lu smaller than " + "persisted " + "stage flag", + backup_log.size(), file_size_); + s = Status::Abort; + } + } + + if (s == Status::Ok) { + log_file_ = + mmap(nullptr, file_size_, PROT_WRITE | PROT_READ, MAP_SHARED, fd_, 0); + if (log_file_ == MAP_FAILED) { + GlobalLogger.Error( + "Open bakcup log %s error while mapping log file: %s\n", + backup_log.c_str(), strerror(errno)); + log_file_ = nullptr; + s = Status::IOError; + } else { + stage_ = *persistedStage(); + } + } + + if (s != Status::Ok) { + Close(); + } + + return s; + } + + // Append a record to backup log + Status Append(RecordType type, const StringView& key, const StringView& val, + ExpireTimeType expire_time) { + if (finished()) { + changeStage(BackupStage::NotFinished); + } + // we do not encapsulate LogRecord here to avoid a memory copy + // TODO: use pinable string view in LogRecord + AppendUint32(&delta_, type); + AppendFixedString(&delta_, key); + AppendFixedString(&delta_, val); + AppendUint64(&delta_, expire_time); + if (delta_.size() >= kMaxBackupLogBufferSize) { + return persistDelta(); + } + return Status::Ok; + } + + Status Finish() { + Status s = persistDelta(); + if (s != Status::Ok) { + return s; + } + changeStage(BackupStage::Finished); + return Status::Ok; + } + + // Get iterator of log records + // Notice: the iterator will be corrupted if append new records to log + std::unique_ptr GetIterator() { + return finished() + ? std::unique_ptr(new LogIterator(logRecordsView())) + : nullptr; + } + + // Close backup log file + void Close() { + if (log_file_ != nullptr) { + munmap(log_file_, file_size_); + } + if (fd_ >= 0) { + close(fd_); + } + delta_.clear(); + file_name_.clear(); + fd_ = -1; + file_size_ = 0; + stage_ = BackupStage::NotFinished; + log_file_ = nullptr; + } + + // Destroy backup log file + void Destroy() { + Close(); + remove(file_name_.c_str()); + } + + private: + Status persistDelta() { + if (ftruncate64(fd_, file_size_ + delta_.size())) { + GlobalLogger.Error("Allocate space for backup log file error: %s\n", + strerror(errno)); + return Status::IOError; + } + log_file_ = mremap(log_file_, file_size_, file_size_ + delta_.size(), + MREMAP_MAYMOVE); + + if (log_file_ == MAP_FAILED) { + GlobalLogger.Error("Map backup log file error: %s\n", strerror(errno)); + log_file_ = nullptr; + return Status::IOError; + } + memcpy((char*)log_file_ + file_size_, delta_.data(), delta_.size()); + msync((char*)log_file_ + file_size_, delta_.size(), MS_SYNC); + file_size_ += delta_.size(); + delta_.clear(); + return Status::Ok; + } + + StringView logRecordsView() { + kvdk_assert(log_file_ != MAP_FAILED && file_size_ >= sizeof(BackupStage), + ""); + return StringView((char*)log_file_ + sizeof(BackupStage), + file_size_ - sizeof(BackupStage)); + } + + BackupStage* persistedStage() { + kvdk_assert(log_file_ != MAP_FAILED && file_size_ >= sizeof(BackupStage), + ""); + return (BackupStage*)log_file_; + } + + void changeStage(BackupStage stage) { + memcpy(persistedStage(), &stage, sizeof(BackupStage)); + msync(persistedStage(), sizeof(BackupStage), MS_SYNC); + } + + bool finished() { return stage_ == BackupStage::Finished; } + + std::string file_name_{}; + std::string delta_{}; + void* log_file_{nullptr}; + size_t file_size_{0}; + int fd_{-1}; + BackupStage stage_{BackupStage::NotFinished}; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/c/kvdk_basic_op.cpp b/volatile/engine/c/kvdk_basic_op.cpp new file mode 100644 index 00000000..c239f0db --- /dev/null +++ b/volatile/engine/c/kvdk_basic_op.cpp @@ -0,0 +1,135 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include +#include +#include + +#include "kvdk_c.hpp" + +extern "C" { +KVDKRegex* KVDKRegexCreate(char const* data, size_t len) { + KVDKRegex* re = new KVDKRegex; + re->rep = std::regex{data, len}; + return re; +} + +void KVDKRegexDestroy(KVDKRegex* re) { delete re; } + +KVDKConfigs* KVDKCreateConfigs() { return new KVDKConfigs; } + +void KVDKSetConfigs(KVDKConfigs* kv_config, uint64_t max_access_threads, + uint64_t hash_bucket_num, uint32_t num_buckets_per_slot) { + kv_config->rep.max_access_threads = max_access_threads; + kv_config->rep.hash_bucket_num = hash_bucket_num; + kv_config->rep.num_buckets_per_slot = num_buckets_per_slot; +} + +void KVDKConfigRegisterCompFunc(KVDKConfigs* kv_config, + const char* compara_name, size_t compara_len, + int (*compare)(const char* src, size_t src_len, + const char* target, + size_t target_len)) { + auto comp_func = [compare](const StringView& src, + const StringView& target) -> int { + return compare(src.data(), src.size(), target.data(), target.size()); + }; + kv_config->rep.comparator.RegisterComparator( + StringView(compara_name, compara_len), comp_func); +} + +void KVDKDestroyConfigs(KVDKConfigs* kv_config) { delete kv_config; } + +KVDKWriteOptions* KVDKCreateWriteOptions(void) { return new KVDKWriteOptions; } + +void KVDKDestroyWriteOptions(KVDKWriteOptions* kv_options) { + delete kv_options; +} + +void KVDKWriteOptionsSetTTLTime(KVDKWriteOptions* kv_options, + int64_t ttl_time) { + kv_options->rep.ttl_time = ttl_time; +} + +void KVDKWriteOptionsSetUpdateTTL(KVDKWriteOptions* kv_options, + int update_ttl) { + kv_options->rep.update_ttl = update_ttl; +} + +KVDKStatus KVDKOpen(const char* name, const KVDKConfigs* config, FILE* log_file, + KVDKEngine** kv_engine) { + Engine* engine; + KVDKStatus s = + Engine::Open(std::string(name), &engine, config->rep, log_file); + *kv_engine = nullptr; + if (s != KVDKStatus::Ok) { + return s; + } + *kv_engine = new KVDKEngine; + (*kv_engine)->rep.reset(engine); + return s; +} + +KVDKStatus KVDKBackup(KVDKEngine* engine, const char* backup_path, + size_t backup_path_len, KVDKSnapshot* snapshot) { + return engine->rep->Backup(StringView(backup_path, backup_path_len), + snapshot->rep); +} + +KVDKStatus KVDKRestore(const char* name, const char* backup_log, + const KVDKConfigs* config, FILE* log_file, + KVDKEngine** kv_engine) { + Engine* engine; + KVDKStatus s = Engine::Restore(std::string(name), std::string(backup_log), + &engine, config->rep, log_file); + if (s == KVDKStatus::Ok) { + *kv_engine = new KVDKEngine; + (*kv_engine)->rep.reset(engine); + } + return s; +} + +KVDKSnapshot* KVDKGetSnapshot(KVDKEngine* engine, int make_checkpoint) { + KVDKSnapshot* snapshot = new KVDKSnapshot; + snapshot->rep = engine->rep->GetSnapshot(make_checkpoint); + return snapshot; +} + +void KVDKReleaseSnapshot(KVDKEngine* engine, KVDKSnapshot* snapshot) { + engine->rep->ReleaseSnapshot(snapshot->rep); + delete snapshot; +} + +void KVDKCloseEngine(KVDKEngine* engine) { delete engine; } + +int KVDKRegisterCompFunc(KVDKEngine* engine, const char* compara_name, + size_t compara_len, + int (*compare)(const char* src, size_t src_len, + const char* target, + size_t target_len)) { + auto comp_func = [compare](const StringView& src, + const StringView& target) -> int { + return compare(src.data(), src.size(), target.data(), target.size()); + }; + return engine->rep->registerComparator(StringView(compara_name, compara_len), + comp_func); +} + +KVDKStatus KVDKExpire(KVDKEngine* engine, const char* str, size_t str_len, + int64_t ttl_time) { + return engine->rep->Expire(StringView{str, str_len}, ttl_time); +} + +KVDKStatus KVDKGetTTL(KVDKEngine* engine, const char* str, size_t str_len, + int64_t* ttl_time) { + return engine->rep->GetTTL(StringView{str, str_len}, ttl_time); +} + +KVDKStatus KVDKTypeOf(KVDKEngine* engine, char const* key_data, size_t key_len, + KVDKValueType* type) { + return engine->rep->TypeOf(StringView{key_data, key_len}, type); +} +} + +// List diff --git a/volatile/engine/c/kvdk_batch.cpp b/volatile/engine/c/kvdk_batch.cpp new file mode 100644 index 00000000..5127bf89 --- /dev/null +++ b/volatile/engine/c/kvdk_batch.cpp @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "kvdk_c.hpp" + +extern "C" { +KVDKWriteBatch* KVDKWriteBatchCreate(KVDKEngine* engine) { + KVDKWriteBatch* batch = new KVDKWriteBatch{}; + batch->rep = engine->rep->WriteBatchCreate(); + return batch; +} + +void KVDKWriteBatchDestory(KVDKWriteBatch* batch) { delete batch; } + +void KVDKWRiteBatchClear(KVDKWriteBatch* batch) { batch->rep->Clear(); } + +void KVDKWriteBatchStringPut(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* val_data, + size_t val_len) { + batch->rep->StringPut(std::string{key_data, key_len}, + std::string{val_data, val_len}); +} + +void KVDKWriteBatchStringDelete(KVDKWriteBatch* batch, char const* key_data, + size_t key_len) { + batch->rep->StringDelete(std::string{key_data, key_len}); +} + +void KVDKWriteBatchSortedPut(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* field_data, + size_t field_len, char const* val_data, + size_t val_len) { + batch->rep->SortedPut(std::string{key_data, key_len}, + std::string{field_data, field_len}, + std::string{val_data, val_len}); +} + +void KVDKWriteBatchSortedDelete(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* field_data, + size_t field_len) { + batch->rep->SortedDelete(std::string{key_data, key_len}, + std::string{field_data, field_len}); +} + +void KVDKWriteBatchHashPut(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* field_data, + size_t field_len, char const* val_data, + size_t val_len) { + batch->rep->HashPut(std::string{key_data, key_len}, + std::string{field_data, field_len}, + std::string{val_data, val_len}); +} + +void KVDKWriteBatchHashDelete(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* field_data, + size_t field_len) { + batch->rep->HashDelete(std::string{key_data, key_len}, + std::string{field_data, field_len}); +} + +KVDKStatus KVDKBatchWrite(KVDKEngine* engine, KVDKWriteBatch const* batch) { + return engine->rep->BatchWrite(batch->rep); +} + +} // extern "C" diff --git a/volatile/engine/c/kvdk_c.hpp b/volatile/engine/c/kvdk_c.hpp new file mode 100644 index 00000000..ee9af260 --- /dev/null +++ b/volatile/engine/c/kvdk_c.hpp @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include +#include +#include +#include + +#include "../alias.hpp" +#include "kvdk/volatile/configs.hpp" +#include "kvdk/volatile/engine.h" +#include "kvdk/volatile/engine.hpp" +#include "kvdk/volatile/iterator.hpp" +#include "kvdk/volatile/write_batch.hpp" + +using kvdk::StringView; + +using kvdk::Configs; +using kvdk::Engine; +using kvdk::HashIterator; +using kvdk::ListIterator; +using kvdk::Snapshot; +using kvdk::SortedCollectionConfigs; +using kvdk::SortedIterator; +using kvdk::WriteBatch; +using kvdk::WriteOptions; + +extern "C" { + +struct KVDKConfigs { + Configs rep; +}; + +struct KVDKEngine { + std::unique_ptr rep; +}; + +struct KVDKWriteBatch { + std::unique_ptr rep; +}; + +struct KVDKSortedIterator { + SortedIterator* rep; +}; + +struct KVDKListIterator { + ListIterator* rep; +}; + +struct KVDKHashIterator { + HashIterator* rep; +}; + +struct KVDKSnapshot { + Snapshot* rep; +}; + +struct KVDKWriteOptions { + WriteOptions rep; +}; + +struct KVDKSortedCollectionConfigs { + SortedCollectionConfigs rep; +}; + +struct KVDKRegex { + std::regex rep; +}; + +inline char* CopyStringToChar(const std::string& str) { + char* result = static_cast(malloc(str.size())); + memcpy(result, str.data(), str.size()); + return result; +} + +} // extern "C" \ No newline at end of file diff --git a/volatile/engine/c/kvdk_hash.cpp b/volatile/engine/c/kvdk_hash.cpp new file mode 100644 index 00000000..b3fb04d8 --- /dev/null +++ b/volatile/engine/c/kvdk_hash.cpp @@ -0,0 +1,147 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include +#include + +#include "kvdk_c.hpp" + +extern "C" { +KVDKStatus KVDKHashCreate(KVDKEngine* engine, char const* key_data, + size_t key_len) { + return engine->rep->HashCreate(StringView{key_data, key_len}); +} +KVDKStatus KVDKHashDestroy(KVDKEngine* engine, char const* key_data, + size_t key_len) { + return engine->rep->HashDestroy(StringView{key_data, key_len}); +} + +KVDKStatus KVDKHashLength(KVDKEngine* engine, char const* key_data, + size_t key_len, size_t* len) { + return engine->rep->HashSize(StringView{key_data, key_len}, len); +} + +KVDKStatus KVDKHashGet(KVDKEngine* engine, const char* key_data, size_t key_len, + const char* field_data, size_t field_len, + char** val_data, size_t* val_len) { + std::string val_str; + *val_data = nullptr; + KVDKStatus s = + engine->rep->HashGet(StringView(key_data, key_len), + StringView(field_data, field_len), &val_str); + if (s != KVDKStatus::Ok) { + *val_len = 0; + return s; + } + *val_len = val_str.size(); + *val_data = CopyStringToChar(val_str); + return s; +} + +KVDKStatus KVDKHashPut(KVDKEngine* engine, const char* key_data, size_t key_len, + const char* field_data, size_t field_len, + const char* val_data, size_t val_len) { + return engine->rep->HashPut(StringView(key_data, key_len), + StringView(field_data, field_len), + StringView(val_data, val_len)); +} + +KVDKStatus KVDKHashDelete(KVDKEngine* engine, const char* key_data, + size_t key_len, const char* field_data, + size_t field_len) { + return engine->rep->HashDelete(StringView(key_data, key_len), + StringView(field_data, field_len)); +} + +extern KVDKStatus KVDKHashModify(KVDKEngine* engine, const char* key_data, + size_t key_len, const char* field_data, + size_t field_len, KVDKModifyFunc modify_func, + void* args, KVDKFreeFunc free_func) { + auto ModifyFunc = [&](const std::string* old_value, std::string* new_value, + void* arg) { + int op; + char* new_val_data = nullptr; + size_t new_val_len = 0; + if (old_value != nullptr) { + op = modify_func(old_value->data(), old_value->size(), &new_val_data, + &new_val_len, arg); + } else { + op = modify_func(nullptr, 0, &new_val_data, &new_val_len, arg); + } + if (op == KVDK_MODIFY_WRITE) { + assert(new_val_data != nullptr); + new_value->assign(new_val_data, new_val_len); + } + if (free_func != nullptr && new_val_data != nullptr) { + free_func(new_val_data); + } + return static_cast(op); + }; + KVDKStatus s = engine->rep->HashModify(StringView{key_data, key_len}, + StringView{field_data, field_len}, + ModifyFunc, args); + return s; +} + +KVDKHashIterator* KVDKHashIteratorCreate(KVDKEngine* engine, + char const* key_data, size_t key_len, + KVDKSnapshot* snapshot, + KVDKStatus* s) { + auto iter = engine->rep->HashIteratorCreate( + StringView{key_data, key_len}, snapshot ? snapshot->rep : nullptr, s); + if (iter == nullptr) { + return nullptr; + } + KVDKHashIterator* hash_iter = new KVDKHashIterator; + hash_iter->rep = iter; + return hash_iter; +} + +void KVDKHashIteratorDestroy(KVDKEngine* engine, KVDKHashIterator* iter) { + if (iter->rep) { + engine->rep->HashIteratorRelease(iter->rep); + } + delete iter; +} + +void KVDKHashIteratorPrev(KVDKHashIterator* iter) { iter->rep->Prev(); } + +void KVDKHashIteratorNext(KVDKHashIterator* iter) { iter->rep->Next(); } + +void KVDKHashIteratorSeekToFirst(KVDKHashIterator* iter) { + iter->rep->SeekToFirst(); +} + +void KVDKHashIteratorSeekToLast(KVDKHashIterator* iter) { + iter->rep->SeekToLast(); +} + +int KVDKHashIteratorIsValid(KVDKHashIterator* iter) { + bool valid = iter->rep->Valid(); + return (valid ? 1 : 0); +} + +void KVDKHashIteratorGetValue(KVDKHashIterator* iter, char** value_data, + size_t* value_len) { + *value_data = nullptr; + *value_len = 0; + std::string buffer = iter->rep->Value(); + *value_data = CopyStringToChar(buffer); + *value_len = buffer.size(); +} + +void KVDKHashIteratorGetKey(KVDKHashIterator* iter, char** field_data, + size_t* field_len) { + *field_data = nullptr; + *field_len = 0; + std::string buffer = iter->rep->Key(); + *field_data = CopyStringToChar(buffer); + *field_len = buffer.size(); +} + +int KVDKHashIteratorMatchKey(KVDKHashIterator* iter, KVDKRegex const* re) { + return iter->rep->MatchKey(re->rep) ? 1 : 0; +} + +} // extern "C" diff --git a/volatile/engine/c/kvdk_list.cpp b/volatile/engine/c/kvdk_list.cpp new file mode 100644 index 00000000..b3ceb138 --- /dev/null +++ b/volatile/engine/c/kvdk_list.cpp @@ -0,0 +1,250 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "kvdk_c.hpp" +extern "C" { +KVDKStatus KVDKListCreate(KVDKEngine* engine, char const* key_data, + size_t key_len) { + return engine->rep->ListCreate(StringView{key_data, key_len}); +} +KVDKStatus KVDKListDestroy(KVDKEngine* engine, char const* key_data, + size_t key_len) { + return engine->rep->ListDestroy(StringView{key_data, key_len}); +} +KVDKStatus KVDKListSize(KVDKEngine* engine, char const* key_data, + size_t key_len, size_t* len) { + return engine->rep->ListSize(StringView{key_data, key_len}, len); +} + +KVDKStatus KVDKListPushFront(KVDKEngine* engine, char const* key_data, + size_t key_len, char const* elem_data, + size_t elem_len) { + return engine->rep->ListPushFront(StringView{key_data, key_len}, + StringView{elem_data, elem_len}); +} + +KVDKStatus KVDKListPushBack(KVDKEngine* engine, char const* key_data, + size_t key_len, char const* elem_data, + size_t elem_len) { + return engine->rep->ListPushBack(StringView{key_data, key_len}, + StringView{elem_data, elem_len}); +} + +KVDKStatus KVDKListPopFront(KVDKEngine* engine, char const* key_data, + size_t key_len, char** elem_data, + size_t* elem_len) { + *elem_data = nullptr; + *elem_len = 0; + std::string buffer; + KVDKStatus s = + engine->rep->ListPopFront(StringView{key_data, key_len}, &buffer); + if (s == KVDKStatus::Ok) { + *elem_data = CopyStringToChar(buffer); + *elem_len = buffer.size(); + } + return s; +} + +KVDKStatus KVDKListPopBack(KVDKEngine* engine, char const* key_data, + size_t key_len, char** elem_data, size_t* elem_len) { + *elem_data = nullptr; + *elem_len = 0; + std::string buffer; + KVDKStatus s = + engine->rep->ListPopBack(StringView{key_data, key_len}, &buffer); + if (s == KVDKStatus::Ok) { + *elem_data = CopyStringToChar(buffer); + *elem_len = buffer.size(); + } + return s; +} + +KVDKStatus KVDKListInsertAt(KVDKEngine* engine, char const* list_name, + size_t list_name_len, char const* elem_data, + size_t elem_len, long index) { + return engine->rep->ListInsertAt(StringView(list_name, list_name_len), + StringView(elem_data, elem_len), index); +} + +KVDKStatus KVDKListInsertBefore(KVDKEngine* engine, char const* list_name, + size_t list_name_len, char const* elem_data, + size_t elem_len, char const* pos_elem, + size_t pos_elem_len) { + return engine->rep->ListInsertBefore(StringView(list_name, list_name_len), + StringView(elem_data, elem_len), + StringView(pos_elem, pos_elem_len)); +} + +KVDKStatus KVDKListInsertAfter(KVDKEngine* engine, char const* list_name, + size_t list_name_len, char const* elem_data, + size_t elem_len, char const* pos_elem, + size_t pos_elem_len) { + return engine->rep->ListInsertAfter(StringView(list_name, list_name_len), + StringView(elem_data, elem_len), + StringView(pos_elem, pos_elem_len)); +} + +KVDKStatus KVDKListErase(KVDKEngine* engine, char const* list_name, + size_t list_len, long index, char** elem_data, + size_t* elem_len) { + std::string buffer; + KVDKStatus s = + engine->rep->ListErase(StringView(list_name, list_len), index, &buffer); + if (s == KVDKStatus::Ok) { + *elem_data = CopyStringToChar(buffer); + *elem_len = buffer.size(); + } + return s; +} + +KVDKStatus KVDKListReplace(KVDKEngine* engine, char const* list_name, + size_t list_name_len, long index, char const* elem, + size_t elem_len) { + return engine->rep->ListReplace(StringView(list_name, list_name_len), index, + StringView(elem, elem_len)); +} + +KVDKStatus KVDKListBatchPushFront(KVDKEngine* engine, char const* key_data, + size_t key_len, char const* const* elems_data, + size_t const* elems_len, size_t elems_cnt) { + std::vector elems; + for (size_t i = 0; i < elems_cnt; i++) { + elems.emplace_back(elems_data[i], elems_len[i]); + } + return engine->rep->ListBatchPushFront(StringView{key_data, key_len}, elems); +} + +KVDKStatus KVDKListBatchPushBack(KVDKEngine* engine, char const* key_data, + size_t key_len, char const* const* elems_data, + size_t const* elems_len, size_t elems_cnt) { + std::vector elems; + for (size_t i = 0; i < elems_cnt; i++) { + elems.emplace_back(elems_data[i], elems_len[i]); + } + return engine->rep->ListBatchPushBack(StringView{key_data, key_len}, elems); +} + +KVDKStatus KVDKListBatchPopFront(KVDKEngine* engine, char const* key_data, + size_t key_len, size_t n, + void (*cb)(char const* elem_data, + size_t elem_len, void* args), + void* args) { + std::vector elems; + KVDKStatus s = + engine->rep->ListBatchPopFront(StringView{key_data, key_len}, n, &elems); + if (s != KVDKStatus::Ok) { + return s; + } + for (auto const& elem : elems) { + cb(elem.data(), elem.size(), args); + } + return s; +} + +KVDKStatus KVDKListBatchPopBack(KVDKEngine* engine, char const* key_data, + size_t key_len, size_t n, + void (*cb)(char const* elem_data, + size_t elem_len, void* args), + void* args) { + std::vector elems; + KVDKStatus s = + engine->rep->ListBatchPopBack(StringView{key_data, key_len}, n, &elems); + if (s != KVDKStatus::Ok) { + return s; + } + for (auto const& elem : elems) { + cb(elem.data(), elem.size(), args); + } + return s; +} + +KVDKStatus KVDKListMove(KVDKEngine* engine, char const* src_data, + size_t src_len, int src_pos, char const* dst_data, + size_t dst_len, int dst_pos, char** elem_data, + size_t* elem_len) { + if ((src_pos != KVDK_LIST_BACK && src_pos != KVDK_LIST_FRONT) || + (dst_pos != KVDK_LIST_BACK && dst_pos != KVDK_LIST_FRONT)) { + return InvalidArgument; + } + std::string elem; + KVDKStatus s = engine->rep->ListMove( + StringView{src_data, src_len}, + static_cast(src_pos), + StringView{dst_data, dst_len}, + static_cast(dst_pos), &elem); + *elem_data = CopyStringToChar(elem); + *elem_len = elem.size(); + return s; +} + +KVDKListIterator* KVDKListIteratorCreate(KVDKEngine* engine, + char const* key_data, size_t key_len, + KVDKStatus* s) { + auto rep = engine->rep->ListIteratorCreate(StringView{key_data, key_len}, + nullptr, s); + if (rep == nullptr) { + return nullptr; + } + KVDKListIterator* iter = new KVDKListIterator; + iter->rep = rep; + return iter; +} + +void KVDKListIteratorDestroy(KVDKEngine* engine, KVDKListIterator* iter) { + if (iter->rep) { + engine->rep->ListIteratorRelease(iter->rep); + } + delete iter; +} + +void KVDKListIteratorPrev(KVDKListIterator* iter) { iter->rep->Prev(); } + +void KVDKListIteratorNext(KVDKListIterator* iter) { iter->rep->Next(); } + +void KVDKListIteratorSeekToFirst(KVDKListIterator* iter) { + iter->rep->SeekToFirst(); +} + +void KVDKListIteratorSeekToLast(KVDKListIterator* iter) { + iter->rep->SeekToLast(); +} + +void KVDKListIteratorSeekPos(KVDKListIterator* iter, long pos) { + iter->rep->Seek(pos); +} + +void KVDKListIteratorPrevElem(KVDKListIterator* iter, char const* elem_data, + size_t elem_len) { + iter->rep->Prev(StringView{elem_data, elem_len}); +} + +void KVDKListIteratorNextElem(KVDKListIterator* iter, char const* elem_data, + size_t elem_len) { + iter->rep->Next(StringView{elem_data, elem_len}); +} + +void KVDKListIteratorSeekToFirstElem(KVDKListIterator* iter, + char const* elem_data, size_t elem_len) { + iter->rep->SeekToFirst(StringView{elem_data, elem_len}); +} + +void KVDKListIteratorSeekToLastElem(KVDKListIterator* iter, + char const* elem_data, size_t elem_len) { + iter->rep->SeekToLast(StringView{elem_data, elem_len}); +} + +int KVDKListIteratorIsValid(KVDKListIterator* iter) { + bool valid = iter->rep->Valid(); + return (valid ? 1 : 0); +} + +void KVDKListIteratorGetValue(KVDKListIterator* iter, char** elem_data, + size_t* elem_len) { + *elem_data = nullptr; + *elem_len = 0; + std::string buffer = iter->rep->Value(); + *elem_data = CopyStringToChar(buffer); + *elem_len = buffer.size(); +} +} diff --git a/volatile/engine/c/kvdk_sorted.cpp b/volatile/engine/c/kvdk_sorted.cpp new file mode 100644 index 00000000..66bb4c27 --- /dev/null +++ b/volatile/engine/c/kvdk_sorted.cpp @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "kvdk_c.hpp" + +extern "C" { +KVDKSortedCollectionConfigs* KVDKCreateSortedCollectionConfigs() { + return new KVDKSortedCollectionConfigs; +} + +void KVDKSetSortedCollectionConfigs(KVDKSortedCollectionConfigs* configs, + const char* comp_func_name, + size_t comp_func_len, + int index_with_hashtable) { + configs->rep.comparator_name = std::string(comp_func_name, comp_func_len); + configs->rep.index_with_hashtable = index_with_hashtable; +} + +void KVDKDestroySortedCollectionConfigs(KVDKSortedCollectionConfigs* configs) { + delete configs; +} + +KVDKStatus KVDKSortedCreate(KVDKEngine* engine, const char* collection_name, + size_t collection_len, + KVDKSortedCollectionConfigs* configs) { + KVDKStatus s = engine->rep->SortedCreate( + StringView(collection_name, collection_len), configs->rep); + if (s != KVDKStatus::Ok) { + return s; + } + return s; +} + +KVDKStatus KVDKSortedDestroy(KVDKEngine* engine, const char* collection_name, + size_t collection_len) { + return engine->rep->SortedDestroy( + StringView(collection_name, collection_len)); +} + +KVDKStatus KVDKSortedSize(KVDKEngine* engine, const char* collection, + size_t collection_len, size_t* size) { + return engine->rep->SortedSize(StringView(collection, collection_len), size); +} + +KVDKStatus KVDKSortedPut(KVDKEngine* engine, const char* collection, + size_t collection_len, const char* key, size_t key_len, + const char* val, size_t val_len) { + return engine->rep->SortedPut(StringView(collection, collection_len), + StringView(key, key_len), + StringView(val, val_len)); +} + +KVDKStatus KVDKSortedGet(KVDKEngine* engine, const char* collection, + size_t collection_len, const char* key, size_t key_len, + size_t* val_len, char** val) { + std::string val_str; + + *val = nullptr; + KVDKStatus s = engine->rep->SortedGet(StringView(collection, collection_len), + StringView(key, key_len), &val_str); + if (s != KVDKStatus::Ok) { + *val_len = 0; + return s; + } + *val_len = val_str.size(); + *val = CopyStringToChar(val_str); + return s; +} + +KVDKStatus KVDKSortedDelete(KVDKEngine* engine, const char* collection, + size_t collection_len, const char* key, + size_t key_len) { + return engine->rep->SortedDelete(StringView(collection, collection_len), + StringView(key, key_len)); +} + +KVDKSortedIterator* KVDKSortedIteratorCreate(KVDKEngine* engine, + const char* collection, + size_t collection_len, + KVDKSnapshot* snapshot, + KVDKStatus* s) { + KVDKSortedIterator* result = new KVDKSortedIterator; + result->rep = (engine->rep->SortedIteratorCreate( + StringView{collection, collection_len}, + snapshot ? snapshot->rep : nullptr, s)); + if (!result->rep) { + delete result; + return nullptr; + } + return result; +} + +void KVDKSortedIteratorDestroy(KVDKEngine* engine, + KVDKSortedIterator* iterator) { + if (iterator != nullptr) { + engine->rep->SortedIteratorRelease(iterator->rep); + } + delete iterator; +} + +void KVDKSortedIteratorSeekToFirst(KVDKSortedIterator* iter) { + iter->rep->SeekToFirst(); +} + +void KVDKSortedIteratorSeekToLast(KVDKSortedIterator* iter) { + iter->rep->SeekToLast(); +} + +void KVDKSortedIteratorSeek(KVDKSortedIterator* iter, const char* str, + size_t str_len) { + iter->rep->Seek(std::string(str, str_len)); +} + +unsigned char KVDKSortedIteratorValid(KVDKSortedIterator* iter) { + return iter->rep->Valid(); +} + +void KVDKSortedIteratorNext(KVDKSortedIterator* iter) { iter->rep->Next(); } + +void KVDKSortedIteratorPrev(KVDKSortedIterator* iter) { iter->rep->Prev(); } + +void KVDKSortedIteratorKey(KVDKSortedIterator* iter, char** key, + size_t* key_len) { + std::string key_str = iter->rep->Key(); + *key_len = key_str.size(); + *key = CopyStringToChar(key_str); +} + +void KVDKSortedIteratorValue(KVDKSortedIterator* iter, char** value, + size_t* val_len) { + std::string val_str = iter->rep->Value(); + *val_len = val_str.size(); + *value = CopyStringToChar(val_str); +} + +} // extern "C" diff --git a/volatile/engine/c/kvdk_string.cpp b/volatile/engine/c/kvdk_string.cpp new file mode 100644 index 00000000..764db0ac --- /dev/null +++ b/volatile/engine/c/kvdk_string.cpp @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "kvdk_c.hpp" +extern "C" { +KVDKStatus KVDKGet(KVDKEngine* engine, const char* key, size_t key_len, + size_t* val_len, char** val) { + std::string val_str; + + *val = nullptr; + KVDKStatus s = engine->rep->Get(StringView(key, key_len), &val_str); + if (s != KVDKStatus::Ok) { + *val_len = 0; + return s; + } + *val_len = val_str.size(); + *val = CopyStringToChar(val_str); + return s; +} + +KVDKStatus KVDKPut(KVDKEngine* engine, const char* key, size_t key_len, + const char* val, size_t val_len, + const KVDKWriteOptions* write_option) { + return engine->rep->Put(StringView(key, key_len), StringView(val, val_len), + write_option->rep); +} + +KVDKStatus KVDKModify(KVDKEngine* engine, const char* key, size_t key_len, + KVDKModifyFunc modify_func, void* modify_args, + KVDKFreeFunc free_func, + const KVDKWriteOptions* write_option) { + auto cpp_modify_func = [&](const std::string* old_value, + std::string* new_value, void* args) { + char* nv = nullptr; + size_t nv_len = 0; + auto result = + modify_func(old_value ? old_value->data() : nullptr, + old_value ? old_value->size() : 0, &nv, &nv_len, args); + if (result == KVDK_MODIFY_WRITE) { + assert(nv != nullptr); + new_value->assign(nv, nv_len); + } + if (nv != nullptr && free_func != nullptr) { + free_func(nv); + } + return kvdk::ModifyOperation(result); + }; + KVDKStatus s = engine->rep->Modify(StringView(key, key_len), cpp_modify_func, + modify_args, write_option->rep); + return s; +} + +KVDKStatus KVDKDelete(KVDKEngine* engine, const char* key, size_t key_len) { + return engine->rep->Delete(StringView(key, key_len)); +} + +} // extern "C" diff --git a/volatile/engine/collection.hpp b/volatile/engine/collection.hpp new file mode 100644 index 00000000..2f18885a --- /dev/null +++ b/volatile/engine/collection.hpp @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include +#include + +#include "alias.hpp" +#include "macros.hpp" +#include "utils/codec.hpp" +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { +/// TODO: (ziyan) add expire_time field to Collection. +class Collection { + public: + Collection(const StringView& name, CollectionIDType id) + : collection_name_(string_view_2_string(name)), collection_id_(id) {} + virtual ~Collection() = default; + // Return unique ID of the collection + uint64_t ID() const { return collection_id_; } + + // Return name of the collection + const std::string& Name() const { return collection_name_; } + + virtual ExpireTimeType GetExpireTime() const = 0; + virtual bool HasExpired() const = 0; + + // Return internal representation of "key" in the collection + // By default, we concat key with the collection id + std::string InternalKey(const StringView& key) { + return makeInternalKey(key, ID()); + } + + inline static std::string EncodeID(CollectionIDType id) { + return EncodeUint64(id); + } + + inline static CollectionIDType DecodeID(const StringView& string_id) { + CollectionIDType id{}; + bool ret = DecodeUint64(string_id, &id); + kvdk_assert(ret, "size of string id does not match CollectionIDType size!"); + return id; + } + + inline static StringView ExtractUserKey(const StringView& internal_key) { + constexpr size_t sz_id = sizeof(CollectionIDType); + kvdk_assert(sz_id <= internal_key.size(), + "internal_key does not has space for key"); + return StringView(internal_key.data() + sz_id, internal_key.size() - sz_id); + } + + inline static uint64_t ExtractID(const StringView& internal_key) { + return DecodeID(internal_key); + } + + inline static std::string ID2String(CollectionIDType id) { + return std::string{reinterpret_cast(&id), sizeof(CollectionIDType)}; + } + + struct TTLCmp { + public: + bool operator()(const Collection* a, const Collection* b) const { + if (a->GetExpireTime() < b->GetExpireTime()) return true; + if (a->GetExpireTime() == b->GetExpireTime() && a->ID() < b->ID()) + return true; + return false; + } + }; + + protected: + inline static std::string makeInternalKey(const StringView& user_key, + uint64_t list_id) { + return ID2String(list_id).append(user_key.data(), user_key.size()); + } + + std::string collection_name_; + CollectionIDType collection_id_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/data_record.cpp b/volatile/engine/data_record.cpp new file mode 100644 index 00000000..c6cd3830 --- /dev/null +++ b/volatile/engine/data_record.cpp @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "data_record.hpp" + +namespace KVDK_NAMESPACE {} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/data_record.hpp b/volatile/engine/data_record.hpp new file mode 100644 index 00000000..4b40a810 --- /dev/null +++ b/volatile/engine/data_record.hpp @@ -0,0 +1,318 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include + +#include "alias.hpp" +#include "kvdk/volatile/configs.hpp" +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { + +enum RecordType : uint8_t { + Empty = 0, + String = (1 << 0), + SortedHeader = (1 << 1), + SortedElem = (1 << 2), + HashHeader = (1 << 3), + HashElem = (1 << 4), + ListHeader = (1 << 5), + ListElem = (1 << 6), +}; + +enum class RecordStatus : uint8_t { + // Indicate a up-to-date record + Normal = 0, + // Indicate key of the record is updated + Dirty, + // Indicate deleted or expired record + Outdated, +}; + +const uint8_t ExpirableRecordType = + (RecordType::String | RecordType::SortedHeader | RecordType::HashHeader | + RecordType::ListHeader); + +const uint8_t PrimaryRecordType = ExpirableRecordType; + +const uint8_t ElemType = + (RecordType::SortedElem | RecordType::HashElem | RecordType::ListElem); + +const uint8_t CollectionType = + (RecordType::SortedHeader | RecordType::HashHeader | + RecordType::ListHeader); + +struct DataHeader { + DataHeader() = default; + DataHeader(uint32_t c, uint32_t s) : checksum(c), record_size(s) {} + + uint32_t checksum; + uint32_t record_size; +}; + +struct DataMeta { + DataMeta() = default; + DataMeta(TimestampType _timestamp, RecordType _type, RecordStatus _status, + uint16_t _key_size, uint32_t _value_size) + : type(_type), + status(_status), + k_size(_key_size), + v_size(_value_size), + timestamp(_timestamp) {} + + RecordType type; + RecordStatus status; + uint16_t k_size; + uint32_t v_size; + TimestampType timestamp; +}; + +// Header and metadata of a data record +struct DataEntry { + DataEntry(uint32_t _checksum, uint32_t _record_size /* size in blocks */, + TimestampType _timestamp, RecordType _type, RecordStatus _status, + uint16_t _key_size, uint32_t _value_size) + : header(_checksum, _record_size), + meta(_timestamp, _type, _status, _key_size, _value_size) {} + + DataEntry() = default; + + void Destroy() { meta.type = RecordType::Empty; } + + // TODO jiayu: use function to access these + DataHeader header; + DataMeta meta; +}; +static_assert(sizeof(DataEntry) <= kMinMemoryBlockSize); + +struct StringRecord { + public: + DataEntry entry; + MemoryOffsetType old_version; + ExpireTimeType expired_time; + char data[0]; + + // Construct a StringRecord instance at target_address. As the record need + // additional space to store data, we need pre-allocate enough space for it. + // + // target_address: pre-allocated space to store constructed record, it + // should larger than sizeof(StringRecord) + key size + value size + static StringRecord* ConstructStringRecord( + void* target_address, uint32_t _record_size, TimestampType _timestamp, + RecordType _record_type, RecordStatus _record_status, + MemoryOffsetType _old_version, const StringView& _key, + const StringView& _value, ExpireTimeType _expired_time = kPersistTime) { + StringRecord* record = new (target_address) + StringRecord(_record_size, _timestamp, _record_type, _record_status, + _old_version, _key, _value, _expired_time); + return record; + } + + void Destroy() { entry.Destroy(); } + + // make sure there is data followed in data[0] + StringView Key() const { return StringView(data, entry.meta.k_size); } + + // make sure there is data followed in data[0] + StringView Value() const { + return StringView(data + entry.meta.k_size, entry.meta.v_size); + } + + // Check whether the record corrupted + bool Validate() { + if (ValidateRecordSize()) { + return Checksum() == entry.header.checksum; + } + return false; + } + + bool ValidOrDirty() { + bool valid = Validate(); + asm volatile("" ::: "memory"); + bool dirty = (GetRecordStatus() == RecordStatus::Dirty); + return (valid || dirty); + } + + // Check whether the record corrupted with expected checksum + bool Validate(uint32_t expected_checksum) { + if (ValidateRecordSize()) { + return Checksum() == expected_checksum; + } + return false; + } + + ExpireTimeType GetExpireTime() const { return expired_time; } + bool HasExpired() const { return TimeUtils::CheckIsExpired(GetExpireTime()); } + + void SetExpireTime(ExpireTimeType time) { expired_time = time; } + + void SetOldVersion(MemoryOffsetType offset) { old_version = offset; } + + void SetStatus(RecordStatus status) { entry.meta.status = status; } + + TimestampType GetTimestamp() const { return entry.meta.timestamp; } + + RecordType GetRecordType() const { return entry.meta.type; } + + RecordStatus GetRecordStatus() const { return entry.meta.status; } + + uint32_t GetRecordSize() const { return entry.header.record_size; } + + static uint32_t RecordSize(const StringView& key, const StringView& value) { + return key.size() + value.size() + sizeof(StringRecord); + } + + private: + StringRecord(uint32_t _record_size, TimestampType _timestamp, + RecordType _record_type, RecordStatus _record_status, + MemoryOffsetType _old_version, const StringView& _key, + const StringView& _value, ExpireTimeType _expired_time) + : entry(0, _record_size, _timestamp, _record_type, _record_status, + _key.size(), _value.size()), + old_version(_old_version), + expired_time(_expired_time) { + kvdk_assert(_record_type == RecordType::String, ""); + memcpy(data, _key.data(), _key.size()); + memcpy(data + _key.size(), _value.data(), _value.size()); + entry.header.checksum = Checksum(); + } + + // check validation of k_size and v_size, as record may be left corrupted + bool ValidateRecordSize() { + return entry.meta.k_size + entry.meta.v_size + sizeof(StringRecord) <= + entry.header.record_size; + } + + uint32_t Checksum() { + // we don't checksum expire time and old version + uint32_t meta_checksum_size = sizeof(DataMeta); + uint32_t data_checksum_size = entry.meta.k_size + entry.meta.v_size; + + return get_checksum((char*)&entry.meta, meta_checksum_size) + + get_checksum(data, data_checksum_size); + } +}; + +// doubly linked record +struct DLRecord { + public: + DataEntry entry; + MemoryOffsetType old_version; + MemoryOffsetType prev; + MemoryOffsetType next; + ExpireTimeType expired_time; + + char data[0]; + + // Construct a DLRecord instance at "target_address". As the record need + // additional space to store data, we need pre-allocate enough space for it. + // + // target_address: pre-allocated space to store constructed record, it + // should no smaller than sizeof(DLRecord) + key size + value size + static DLRecord* ConstructDLRecord( + void* target_address, uint32_t record_size, TimestampType timestamp, + RecordType record_type, RecordStatus record_status, + MemoryOffsetType old_version, uint64_t prev, uint64_t next, + const StringView& key, const StringView& value, + ExpireTimeType expired_time = kPersistTime) { + DLRecord* record = new (target_address) + DLRecord(record_size, timestamp, record_type, record_status, + old_version, prev, next, key, value, expired_time); + return record; + } + + void Destroy() { entry.Destroy(); } + + bool Validate() { + if (ValidateRecordSize()) { + return Checksum() == entry.header.checksum; + } + return false; + } + + bool Validate(uint32_t expected_checksum) { + if (ValidateRecordSize()) { + return Checksum() == expected_checksum; + } + return false; + } + + StringView Key() const { return StringView(data, entry.meta.k_size); } + + StringView Value() const { + return StringView(data + entry.meta.k_size, entry.meta.v_size); + } + + void SetNext(MemoryOffsetType offset) { next = offset; } + + void SetPrev(MemoryOffsetType offset) { prev = offset; } + + void SetPrevSetExpireTime(ExpireTimeType time) { + kvdk_assert(entry.meta.type & ExpirableRecordType, ""); + expired_time = time; + } + + void SetOldVersion(MemoryOffsetType offset) { old_version = offset; } + + void SetStatus(RecordStatus status) { entry.meta.status = status; } + + ExpireTimeType GetExpireTime() const { + kvdk_assert(entry.meta.type & ExpirableRecordType, + "Call DLRecord::GetExpireTime with an unexpirable type"); + return expired_time; + } + + RecordType GetRecordType() const { return entry.meta.type; } + + RecordStatus GetRecordStatus() const { return entry.meta.status; } + + bool HasExpired() const { return TimeUtils::CheckIsExpired(GetExpireTime()); } + TimestampType GetTimestamp() const { return entry.meta.timestamp; } + + uint32_t GetRecordSize() const { return entry.header.record_size; } + + static uint32_t RecordSize(const StringView& key, const StringView& value) { + return sizeof(DLRecord) + key.size() + value.size(); + } + + private: + DLRecord(uint32_t _record_size, TimestampType _timestamp, RecordType _type, + RecordStatus _status, MemoryOffsetType _old_version, + MemoryOffsetType _prev, MemoryOffsetType _next, + const StringView& _key, const StringView& _value, + ExpireTimeType _expired_time) + : entry(0, _record_size, _timestamp, _type, _status, _key.size(), + _value.size()), + old_version(_old_version), + prev(_prev), + next(_next), + expired_time(_expired_time) { + kvdk_assert(_type & (RecordType::SortedElem | RecordType::SortedHeader | + RecordType::HashElem | RecordType::HashHeader | + RecordType::ListElem | RecordType::ListHeader), + ""); + memcpy(data, _key.data(), _key.size()); + memcpy(data + _key.size(), _value.data(), _value.size()); + entry.header.checksum = Checksum(); + } + + // check validation of k_size and v_size, as record may be left corrupted + bool ValidateRecordSize() { + return entry.meta.k_size + entry.meta.v_size + sizeof(DLRecord) <= + entry.header.record_size; + } + + uint32_t Checksum() { + // we don't checksum next/prev pointers, expire time and old_version + uint32_t meta_checksum_size = sizeof(DataMeta); + uint32_t data_checksum_size = entry.meta.k_size + entry.meta.v_size; + + return get_checksum((char*)&entry.meta, meta_checksum_size) + + get_checksum(data, data_checksum_size); + } +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/dl_list.cpp b/volatile/engine/dl_list.cpp new file mode 100644 index 00000000..375c9a45 --- /dev/null +++ b/volatile/engine/dl_list.cpp @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "dl_list.hpp" + +namespace KVDK_NAMESPACE { +std::unique_ptr DLList::GetRecordIterator() { + return std::unique_ptr( + new DLListRecordIterator(this, kv_allocator_)); +} + +Status DLList::PushBack(const DLList::WriteArgs& args) { + kvdk_assert(header_ != nullptr, ""); + Status s; + do { + s = InsertBefore(args, header_); + } while (s == Status::Fail); + return s; +} + +Status DLList::PushFront(const DLList::WriteArgs& args) { + kvdk_assert(header_ != nullptr, ""); + Status s; + do { + s = InsertAfter(args, header_); + } while (s == Status::Fail); + return s; +} + +DLRecord* DLList::RemoveFront() { + kvdk_assert(header_ != nullptr, ""); + while (true) { + DLRecord* front = + kv_allocator_->offset2addr_checked(header_->next); + if (front == header_) { + return nullptr; + } + // Maybe removed by another thread + bool success = Remove(front); + if (success) { + return front; + } + } +} + +DLRecord* DLList::RemoveBack() { + kvdk_assert(header_ != nullptr, ""); + while (true) { + DLRecord* back = + kv_allocator_->offset2addr_checked(header_->prev); + if (back == header_) { + return nullptr; + } + // Maybe removed by another thread + bool success = Remove(back); + if (success) { + return back; + } + } +} + +Status DLList::InsertBetween(const DLList::WriteArgs& args, DLRecord* prev, + DLRecord* next) { + auto ul = acquireInsertLock(prev); + MemoryOffsetType next_offset = kv_allocator_->addr2offset_checked(next); + MemoryOffsetType prev_offset = kv_allocator_->addr2offset_checked(prev); + // Check if the linkage has changed before we successfully acquire lock. + bool check_linkage = prev->next == next_offset && next->prev == prev_offset; + if (!check_linkage) { + return Status::Fail; + } + + DLRecord* new_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(args.space.offset), args.space.size, + args.ts, args.type, args.status, kNullMemoryOffset, prev_offset, + next_offset, args.key, args.val); + linkRecord(prev, next, new_record); + + return Status::Ok; +} + +Status DLList::InsertAfter(const DLList::WriteArgs& args, DLRecord* prev) { + return InsertBetween( + args, prev, kv_allocator_->offset2addr_checked(prev->next)); +} + +Status DLList::InsertBefore(const DLList::WriteArgs& args, DLRecord* next) { + return InsertBetween( + args, kv_allocator_->offset2addr_checked(next->prev), next); +} + +Status DLList::Update(const DLList::WriteArgs& args, DLRecord* current) { + kvdk_assert(current != nullptr && equal_string_view(current->Key(), args.key), + ""); + auto guard = acquireRecordLock(current); + MemoryOffsetType current_offset = kv_allocator_->addr2offset_checked(current); + MemoryOffsetType prev_offset = current->prev; + MemoryOffsetType next_offset = current->next; + DLRecord* prev = kv_allocator_->offset2addr_checked(prev_offset); + DLRecord* next = kv_allocator_->offset2addr_checked(next_offset); + if (next->prev != current_offset || prev->next != current_offset) { + return Status::Fail; + } + DLRecord* new_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(args.space.offset), args.space.size, + args.ts, args.type, args.status, current_offset, prev_offset, next_offset, + args.key, args.val); + linkRecord(prev, next, new_record); + return Status::Ok; +} + +bool DLList::Replace(DLRecord* old_record, DLRecord* new_record) { + bool ret = Replace(old_record, new_record, kv_allocator_, lock_table_); + if (ret && old_record == header_) { + header_ = new_record; + } + return ret; +} + +bool DLList::Remove(DLRecord* removing_record) { + bool ret = Remove(removing_record, kv_allocator_, lock_table_); + return ret; +} + +bool DLList::Replace(DLRecord* old_record, DLRecord* new_record, + Allocator* kv_allocator, LockTable* lock_table) { + auto guard = acquireRecordLock(old_record, kv_allocator, lock_table); + MemoryOffsetType prev_offset = old_record->prev; + MemoryOffsetType next_offset = old_record->next; + auto old_record_offset = kv_allocator->addr2offset(old_record); + DLRecord* prev = kv_allocator->offset2addr_checked(prev_offset); + DLRecord* next = kv_allocator->offset2addr_checked(next_offset); + bool on_list = + prev != nullptr && next != nullptr && prev->next == old_record_offset; + if (on_list) { + if (prev_offset == old_record_offset && next_offset == old_record_offset) { + // old record is the only record (the header) in the list, so we + // make + // new record point to itself and break linkage of the old one for + // recovery + kvdk_assert((new_record->GetRecordType() & CollectionType) && + (old_record->GetRecordType() & CollectionType), + "Non-header record shouldn't be the only record in a list"); + linkRecord(new_record, new_record, new_record, kv_allocator); + auto new_record_offset = kv_allocator->addr2offset(new_record); + old_record->SetPrev(new_record_offset); + } else { + new_record->prev = prev_offset; + new_record->next = next_offset; + linkRecord(prev, next, new_record, kv_allocator); + } + } + return on_list; +} + +bool DLList::Remove(DLRecord* removing_record, Allocator* kv_allocator, + LockTable* lock_table) { + auto guard = acquireRecordLock(removing_record, kv_allocator, lock_table); + MemoryOffsetType removing_offset = kv_allocator->addr2offset(removing_record); + MemoryOffsetType prev_offset = removing_record->prev; + MemoryOffsetType next_offset = removing_record->next; + DLRecord* prev = kv_allocator->offset2addr_checked(prev_offset); + DLRecord* next = kv_allocator->offset2addr_checked(next_offset); + bool on_list = + prev != nullptr && next != nullptr && prev->next == removing_offset; + if (on_list) { + // For repair in recovery due to crashes during pointers changing, we + // should + // first unlink deleting entry from next's prev.(It is the reverse process + // of insertion) + next->prev = prev_offset; + TEST_SYNC_POINT("KVEngine::DLList::Remove::PersistNext'sPrev::After"); + prev->next = next_offset; + } + return on_list; +} +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/dl_list.hpp b/volatile/engine/dl_list.hpp new file mode 100644 index 00000000..d5a6831d --- /dev/null +++ b/volatile/engine/dl_list.hpp @@ -0,0 +1,344 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#pragma once + +#include "allocator.hpp" +#include "collection.hpp" +#include "data_record.hpp" +#include "kvdk/volatile/types.hpp" +#include "lock_table.hpp" +#include "utils/sync_point.hpp" +#include "version/version_controller.hpp" + +namespace KVDK_NAMESPACE { +class DLListRecordIterator; + +// Persistent doubly linked list +class DLList { + public: + DLList(DLRecord* header, Allocator* kv_allocator, LockTable* lock_table) + : header_(header), kv_allocator_(kv_allocator), lock_table_(lock_table) {} + + struct WriteArgs { + WriteArgs(const StringView& _key, const StringView& _val, RecordType _type, + RecordStatus _status, TimestampType _ts, const SpaceEntry& _space) + : key(_key), + val(_val), + type(_type), + status(_status), + ts(_ts), + space(_space) { + kvdk_assert(space.size >= DLRecord::RecordSize(_key, _val), + "space to write dl record too small"); + } + + WriteArgs(const WriteArgs&) = delete; + + WriteArgs(WriteArgs&& args) = delete; + + StringView key; + StringView val; + RecordType type; + RecordStatus status; + TimestampType ts; + SpaceEntry space; + }; + + const DLRecord* Header() const { return header_; } + + DLRecord* Header() { return header_; } + + Status PushBack(const WriteArgs& args); + + Status PushFront(const WriteArgs& args); + + DLRecord* RemoveFront(); + + DLRecord* RemoveBack(); + + Status InsertBetween(const WriteArgs& args, DLRecord* prev, DLRecord* next); + + Status InsertAfter(const WriteArgs& args, DLRecord* prev); + + Status InsertBefore(const WriteArgs& args, DLRecord* next); + + Status Update(const WriteArgs& args, DLRecord* current); + + bool Replace(DLRecord* old_record, DLRecord* new_record); + + bool Remove(DLRecord* removing_record); + + std::unique_ptr GetRecordIterator(); + + static bool Replace(DLRecord* old_record, DLRecord* new_record, + Allocator* kv_allocator, LockTable* lock_table); + + static bool Remove(DLRecord* removing_record, Allocator* kv_allocator, + LockTable* lock_table); + + private: + // lock position to insert a new record by locking the prev DLRecord + LockTable::ULockType acquireInsertLock(DLRecord* prev) { + return lock_table_->AcquireLock(recordHash(prev)); + } + + // lock position of "record" to replace or unlink it by locking its prev + // DLRecord and itself + LockTable::MultiGuardType acquireRecordLock(DLRecord* record) { + return acquireRecordLock(record, kv_allocator_, lock_table_); + } + + // lock position of "record" to replace or unlink it by locking its prev + // DLRecord and itself + static LockTable::MultiGuardType acquireRecordLock(DLRecord* record, + Allocator* kv_allocator, + LockTable* lock_table) { + while (1) { + MemoryOffsetType prev_offset = record->prev; + MemoryOffsetType next_offset = record->next; + DLRecord* prev = kv_allocator->offset2addr_checked(prev_offset); + auto guard = + lock_table->MultiGuard({recordHash(prev), recordHash(record)}); + // Check if the linkage has changed before we successfully acquire lock. + if (record->prev != prev_offset || record->next != next_offset) { + continue; + } + + return guard; + } + } + + void linkRecord(DLRecord* prev, DLRecord* next, DLRecord* linking_record) { + linkRecord(prev, next, linking_record, kv_allocator_); + } + + static LockTable::HashValueType recordHash(const DLRecord* record) { + kvdk_assert(record != nullptr, ""); + return XXH3_64bits(record, sizeof(const DLRecord*)); + } + + static void linkRecord(DLRecord* prev, DLRecord* next, + DLRecord* linking_record, Allocator* kv_allocator) { + auto linking_record_offset = + kv_allocator->addr2offset_checked(linking_record); + prev->SetNext(linking_record_offset); + TEST_SYNC_POINT("KVEngine::DLList::LinkDLRecord::HalfLink"); + next->SetPrev(linking_record_offset); + } + + DLRecord* header_; + Allocator* kv_allocator_; + LockTable* lock_table_; +}; + +// Iter valid data under a snapshot in a dl list +class DLListDataIterator { + public: + DLListDataIterator(DLList* dl_list, const Allocator* kv_allocator, + const SnapshotImpl* snapshot) + : dl_list_(dl_list), + kv_allocator_(kv_allocator), + current_(nullptr), + snapshot_(snapshot) {} + + void Locate(DLRecord* record, bool forward) { + kvdk_assert(record != nullptr, ""); + current_ = record; + skipInvalidRecords(forward); + } + + void SeekToFirst() { + auto first = dl_list_->Header()->next; + current_ = kv_allocator_->offset2addr_checked(first); + skipInvalidRecords(true); + } + + void SeekToLast() { + auto last = dl_list_->Header()->prev; + current_ = kv_allocator_->offset2addr(last); + skipInvalidRecords(false); + } + + bool Valid() const { + return current_ != nullptr && (current_->GetRecordType() & ElemType); + } + + virtual void Next() { + if (!Valid()) { + return; + } + current_ = kv_allocator_->offset2addr_checked(current_->next); + skipInvalidRecords(true); + } + + void Prev() { + if (!Valid()) { + return; + } + current_ = (kv_allocator_->offset2addr(current_->prev)); + skipInvalidRecords(false); + } + + StringView Key() const { + if (!Valid()) return ""; + return current_->Key(); + } + + StringView Value() const { + if (!Valid()) return ""; + return current_->Value(); + } + + private: + DLRecord* findValidVersion(DLRecord* data_record) { + DLRecord* curr = data_record; + TimestampType ts = snapshot_->GetTimestamp(); + while (curr != nullptr && curr->GetTimestamp() > ts) { + curr = kv_allocator_->offset2addr(curr->old_version); + kvdk_assert(curr == nullptr || curr->Validate(), + "Broken checkpoint: invalid older version sorted record"); + kvdk_assert( + curr == nullptr || equal_string_view(curr->Key(), data_record->Key()), + "Broken checkpoint: key of older version sorted data is " + "not same as new " + "version"); + } + return curr; + } + + // Move current_ to next/prev valid version data record + void skipInvalidRecords(bool forward) { + while (Valid()) { + DLRecord* valid_version_record = findValidVersion(current_); + if (valid_version_record == nullptr || + valid_version_record->GetRecordStatus() == RecordStatus::Outdated) { + current_ = + forward + ? kv_allocator_->offset2addr_checked(current_->next) + : kv_allocator_->offset2addr_checked(current_->prev); + } else { + current_ = valid_version_record; + break; + } + } + } + + DLList* dl_list_; + const Allocator* kv_allocator_; + DLRecord* current_; + const SnapshotImpl* snapshot_; +}; + +// Iter all records in a dl list +class DLListRecordIterator { + public: + DLListRecordIterator(DLList* dl_list, Allocator* kv_allocator) + : dl_list_(dl_list), + header_(dl_list->Header()), + current_(header_), + kv_allocator_(kv_allocator) {} + + void Locate(DLRecord* record) { current_ = record; } + + void Next() { + if (Valid()) { + current_ = kv_allocator_->offset2addr_checked(current_->next); + } + } + + void Prev() { + if (Valid()) { + current_ = kv_allocator_->offset2addr_checked(current_->prev); + } + } + + bool Valid() { return current_ && (current_->GetRecordType() & ElemType); } + + void SeekToFirst() { + kvdk_assert(header_ != nullptr, ""); + current_ = kv_allocator_->offset2addr_checked(header_->next); + } + + void SeekToLast() { + kvdk_assert(header_ != nullptr, ""); + current_ = kv_allocator_->offset2addr_checked(header_->prev); + } + + DLRecord* Record() { return Valid() ? current_ : nullptr; } + + private: + DLList* dl_list_; + DLRecord* header_; + DLRecord* current_; + const Allocator* kv_allocator_; +}; + +// Used in recovery of dl list based collections +template +class DLListRecoveryUtils { + public: + DLListRecoveryUtils(const Allocator* kv_allocator) + : kv_allocator_(kv_allocator) {} + + bool CheckAndRepairLinkage(DLRecord* record) { + // The next linkage is correct. If the prev linkage is correct too, the + // record linkage is ok. If the prev linkage is not correct, it will be + // repaired by the correct prodecessor soon, so directly return true here. + if (CheckNextLinkage(record)) { + return true; + } + // If only prev linkage is correct, then repair the next linkage + if (CheckPrevLinkage(record)) { + DLRecord* next = + kv_allocator_->offset2addr_checked(record->next); + next->SetPrev(kv_allocator_->addr2offset_checked(record)); + return true; + } + + return false; + } + + bool CheckNextLinkage(DLRecord* record) { + uint64_t offset = kv_allocator_->addr2offset_checked(record); + DLRecord* next = kv_allocator_->offset2addr_checked(record->next); + + auto check_linkage = [&]() { return next->prev == offset; }; + + auto check_type = [&]() { return CType::MatchType(record); }; + + auto check_id = [&]() { + auto next_id = CType::FetchID(next); + auto record_id = CType::FetchID(record); + return record_id == next_id; + }; + + return check_linkage() && check_type() && check_id(); + } + + bool CheckPrevLinkage(DLRecord* record) { + uint64_t offset = kv_allocator_->addr2offset_checked(record); + DLRecord* prev = kv_allocator_->offset2addr_checked(record->prev); + + auto check_linkage = [&]() { return prev->next == offset; }; + + auto check_type = [&]() { return CType::MatchType(record); }; + + auto check_id = [&]() { + auto prev_id = CType::FetchID(prev); + auto record_id = CType::FetchID(record); + return record_id == prev_id; + }; + + return check_linkage() && check_type() && check_id(); + } + + bool CheckLinkage(DLRecord* record) { + return CheckPrevLinkage(record) && CheckNextLinkage(record); + } + + private: + const Allocator* kv_allocator_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/dram_allocator.cpp b/volatile/engine/dram_allocator.cpp new file mode 100644 index 00000000..4798660a --- /dev/null +++ b/volatile/engine/dram_allocator.cpp @@ -0,0 +1,299 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "dram_allocator.hpp" + +#include "thread_manager.hpp" + +namespace KVDK_NAMESPACE { + +constexpr size_t kDefaultChunkAlignment = 64; + +void ChunkBasedAllocator::Free(const SpaceEntry&) { + // Not supported yet +} + +SpaceEntry ChunkBasedAllocator::Allocate(uint64_t size) { + kvdk_assert(ThreadManager::ThreadID() >= 0, ""); + SpaceEntry entry; + auto& tc = dalloc_thread_cache_[ThreadManager::ThreadID() % + dalloc_thread_cache_.size()]; + if (size > chunk_size_) { + entry = alloc_->AllocateAligned(kDefaultChunkAlignment, chunk_size_); + if (entry.size != 0) { + tc.allocated_chunks.push_back(entry); + } + return entry; + } + + if (tc.usable_bytes < size) { + entry = alloc_->AllocateAligned(kDefaultChunkAlignment, chunk_size_); + if (entry.size == 0) { + return entry; + } + void* addr = alloc_->offset2addr(entry.offset); + tc.chunk_addr = (char*)addr; + tc.usable_bytes = chunk_size_; + tc.allocated_chunks.push_back(entry); + } + + entry.size = size; + entry.offset = alloc_->addr2offset(tc.chunk_addr); + tc.chunk_addr += size; + tc.usable_bytes -= size; + return entry; +} + +static struct GlobalState { + SystemMemoryAllocator system_memory_allocator; +} global_state; + +Allocator* global_memory_allocator() { + return &global_state.system_memory_allocator; +} + +static unsigned integer_log2(unsigned v) { + return (sizeof(unsigned) * 8) - (__builtin_clz(v) + 1); +} + +static unsigned round_pow2_up(unsigned v) { + unsigned v_log2 = integer_log2(v); + + unsigned one = 1; + if (v != one << v_log2) { + v = one << (v_log2 + 1); + } + return v; +} + +// SplitMix64 hash +static uint64_t hash64(uint64_t x) { + x += 0x9e3779b97f4a7c15; + x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9; + x = (x ^ (x >> 27)) * 0x94d049bb133111eb; + return x ^ (x >> 31); +} + +static uintptr_t get_fs_base() { return (uintptr_t)pthread_self(); } + +#ifdef KVDK_WITH_JEMALLOC +bool JemallocMemoryAllocator::SetMaxAccessThreads(uint32_t max_access_threads) { + if (max_access_threads == 0 || arenas_initialized_) { + return false; + } + + auto ul = Allocator::AcquireLock(); + + if (arenas_initialized_) { + return false; + } + + num_arenas_ = round_pow2_up(max_access_threads); + arena_mask_ = num_arenas_ - 1; + + return Allocator::EnableThreadLocalCounters(max_access_threads); +} + +static SpinMutex jemalloc_arena_spin; +static std::map arena_allocator_map; + +// Allocates size bytes aligned to alignment. Returns NULL if allocation fails. +void* alloc_aligned_slow(size_t size, size_t alignment, unsigned arena_index) { + JemallocMemoryAllocator* alloc = arena_allocator_map[arena_index]; + if (alloc == nullptr) { + return NULL; + } + + size_t extended_size = size + alignment; + void* ptr; + + ptr = mmap(NULL, extended_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + + if (ptr == MAP_FAILED) { + return NULL; + } + + // mbind + bitmask* mask = alloc->GetMbindNodesMask(); + if (mask) { + int mode = alloc->GetMbindMode(); + int err = mbind(ptr, extended_size, mode, mask->maskp, mask->size, 0); + if (KVDK_UNLIKELY(err)) { + GlobalLogger.Error("syscall mbind() errno: %d.\n", errno); + munmap(ptr, extended_size); + return NULL; + } + } + + uintptr_t addr = (uintptr_t)ptr; + uintptr_t aligned_addr = (addr + alignment) & ~(alignment - 1); + + size_t head_len = aligned_addr - addr; + if (head_len > 0) { + munmap(ptr, head_len); + } + + uintptr_t tail = aligned_addr + size; + size_t tail_len = (addr + extended_size) - (aligned_addr + size); + if (tail_len > 0) { + munmap((void*)tail, tail_len); + } + + return (void*)aligned_addr; +} + +static void* arena_extent_alloc(extent_hooks_t*, void* new_addr, size_t size, + size_t alignment, bool* zero, bool* commit, + unsigned arena_index) { + JemallocMemoryAllocator* alloc = arena_allocator_map[arena_index]; + if (alloc == nullptr) { + return NULL; + } + + void* addr = mmap(new_addr, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (addr == MAP_FAILED) { + return NULL; + } + + // mbind + bitmask* mask = alloc->GetMbindNodesMask(); + if (mask) { + int mode = alloc->GetMbindMode(); + int err = mbind(addr, size, mode, mask->maskp, mask->size, 0); + if (KVDK_UNLIKELY(err)) { + GlobalLogger.Error("syscall mbind() errno: %d.\n", errno); + munmap(addr, size); + return NULL; + } + } + + if (new_addr != NULL && addr != new_addr) { + /* wrong place */ + munmap(addr, size); + return NULL; + } + + if ((uintptr_t)addr & (alignment - 1)) { + munmap(addr, size); + addr = alloc_aligned_slow(size, alignment, arena_index); + if (addr == NULL) { + return NULL; + } + } + + *zero = true; + *commit = true; + + return addr; +} + +static bool arena_extent_dalloc(extent_hooks_t*, void*, size_t, bool, + unsigned) { + return true; +} + +static bool arena_extent_commit(extent_hooks_t*, void*, size_t, size_t, size_t, + unsigned) { + return false; +} + +static bool arena_extent_decommit(extent_hooks_t*, void*, size_t, size_t, + size_t, unsigned) { + return true; +} + +static bool arena_extent_purge(extent_hooks_t*, void* addr, size_t, + size_t offset, size_t length, unsigned) { + int err = madvise((char*)addr + offset, length, MADV_DONTNEED); + return (err != 0); +} + +static bool arena_extent_split(extent_hooks_t*, void*, size_t, size_t, size_t, + bool, unsigned) { + return false; +} + +static bool arena_extent_merge(extent_hooks_t*, void*, size_t, void*, size_t, + bool, unsigned) { + return false; +} + +// clang-format off +static extent_hooks_t arena_extent_hooks = { + arena_extent_alloc, + arena_extent_dalloc, + NULL, + arena_extent_commit, + arena_extent_decommit, + arena_extent_purge, + NULL, + arena_extent_split, + arena_extent_merge +}; +// clang-format on + +unsigned JemallocMemoryAllocator::getArenaForCurrentThread() { + unsigned int arena_idx; + arena_idx = hash64(get_fs_base()) & arena_mask_; + return arena_index_[arena_idx]; +} + +unsigned JemallocMemoryAllocator::createOneArena() { + std::unique_lock ul(jemalloc_arena_spin); + + unsigned arena_index; + size_t unsigned_size = sizeof(unsigned int); + je_kvdk_mallctl("arenas.create", (void*)&arena_index, &unsigned_size, NULL, + 0); + + // setup extent_hooks for newly created arena + char cmd[64]; + extent_hooks_t* hooks = &arena_extent_hooks; + snprintf(cmd, sizeof(cmd), "arena.%u.extent_hooks", arena_index); + je_kvdk_mallctl(cmd, NULL, NULL, (void*)&hooks, sizeof(extent_hooks_t*)); + + arena_allocator_map[arena_index] = this; + return arena_index; +} + +void JemallocMemoryAllocator::destroyArenas() { + if (!arenas_initialized_ || num_arenas_ == 0) { + return; + } + + std::unique_lock ul(jemalloc_arena_spin); + + for (uint32_t i = 0; i < num_arenas_; i++) { + char cmd[128]; + unsigned arena_index = arena_index_[i]; + snprintf(cmd, 128, "arena.%u.destroy", arena_index); + je_kvdk_mallctl(cmd, NULL, NULL, NULL, 0); + arena_allocator_map.erase(arena_index); + } + + arena_index_.clear(); + arenas_initialized_ = false; +} + +void* JemallocMemoryAllocator::allocateWithFlags(size_t size, int flags) { + if (KVDK_LIKELY(size)) { + void* ptr = je_kvdk_mallocx(size, flags); + return ptr; + } + return nullptr; +} + +int JemallocMemoryAllocator::checkAlignment(size_t alignment) { + int err = 0; + if ((alignment < sizeof(void*)) || (((alignment - 1) & alignment) != 0)) { + err = 1; + } + return err; +} + +#endif // #ifdef KVDK_WITH_JEMALLOC + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/dram_allocator.hpp b/volatile/engine/dram_allocator.hpp new file mode 100644 index 00000000..e32f19c0 --- /dev/null +++ b/volatile/engine/dram_allocator.hpp @@ -0,0 +1,253 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include + +#include +#include +#include + +#include "alias.hpp" +#include "allocator.hpp" +#include "kvdk/volatile/engine.hpp" +#include "logger.hpp" +#include "macros.hpp" +#include "structures.hpp" + +#ifdef KVDK_WITH_JEMALLOC +#include "jemalloc/jemalloc.h" +#include "numa.h" +#include "numaif.h" +#endif + +namespace KVDK_NAMESPACE { + +// System memory allocator used for default scenarios. +class SystemMemoryAllocator : public Allocator { + public: + SystemMemoryAllocator() : Allocator(0, UINT64_MAX) {} + + SpaceEntry Allocate(uint64_t size) override { + SpaceEntry entry; + void* addr = malloc(size); + if (addr != nullptr) { + LogAllocation(ThreadManager::ThreadID(), size); + entry.offset = reinterpret_cast(addr); + entry.size = size; + } + + return entry; + } + + SpaceEntry AllocateAligned(size_t alignment, uint64_t size) override { + SpaceEntry entry; + void* addr = aligned_alloc(alignment, size); + if (addr != nullptr) { + LogAllocation(ThreadManager::ThreadID(), size); + entry.offset = reinterpret_cast(addr); + entry.size = size; + } + + return entry; + } + + void Free(const SpaceEntry& entry) override { + if (entry.offset) { + free(reinterpret_cast(entry.offset)); + LogDeallocation(ThreadManager::ThreadID(), entry.size); + } + } + + std::string AllocatorName() override { return "System"; } +}; + +#ifdef KVDK_WITH_JEMALLOC +// jemalloc memory allocator used for KV. +class JemallocMemoryAllocator : public Allocator { + public: + JemallocMemoryAllocator() : Allocator(0, UINT64_MAX) {} + + SpaceEntry Allocate(uint64_t size) override { + SpaceEntry entry; + + void* addr = nullptr; + if (KVDK_UNLIKELY(num_arenas_ == 0)) { + addr = je_kvdk_malloc(size); + } else { + initializeArenas(); + unsigned arena = getArenaForCurrentThread(); + addr = + allocateWithFlags(size, MALLOCX_ARENA(arena) | MALLOCX_TCACHE_NONE); + } + + if (KVDK_LIKELY(addr != nullptr)) { + LogAllocation(ThreadManager::ThreadID(), size); + entry.offset = reinterpret_cast(addr); + entry.size = size; + } + + return entry; + } + + SpaceEntry AllocateAligned(size_t alignment, uint64_t size) override { + SpaceEntry entry; + + void* addr = nullptr; + if (KVDK_UNLIKELY(num_arenas_ == 0)) { + addr = je_kvdk_aligned_alloc(alignment, size); + } else { + if (KVDK_LIKELY(checkAlignment(alignment) == 0)) { + initializeArenas(); + unsigned arena = getArenaForCurrentThread(); + addr = allocateWithFlags(size, MALLOCX_ALIGN(alignment) | + MALLOCX_ARENA(arena) | + MALLOCX_TCACHE_NONE); + } + } + + if (KVDK_LIKELY(addr != nullptr)) { + LogAllocation(ThreadManager::ThreadID(), size); + entry.offset = reinterpret_cast(addr); + entry.size = size; + } + + return entry; + } + + void Free(const SpaceEntry& entry) override { + if (KVDK_UNLIKELY(entry.offset == 0)) { + return; + } + + void* ptr = reinterpret_cast(entry.offset); + if (KVDK_UNLIKELY(num_arenas_ == 0)) { + je_kvdk_free(ptr); + } else { + je_kvdk_dallocx(ptr, MALLOCX_TCACHE_NONE); + } + + LogDeallocation(ThreadManager::ThreadID(), entry.size); + } + + std::string AllocatorName() override { return "jemalloc"; } + + bool SetMaxAccessThreads(uint32_t max_access_threads) override; + + void SetDestMemoryNodes(std::string dest_memory_nodes) override { + auto ul = Allocator::AcquireLock(); + + dest_memory_nodes_ = dest_memory_nodes; + setDestMemoryNodesMask(); + } + + int GetMbindMode() { return MPOL_BIND; } + + bitmask* GetMbindNodesMask() { return dest_memory_nodes_mask_; } + + ~JemallocMemoryAllocator() { + destroyArenas(); + + if (dest_memory_nodes_mask_) { + numa_bitmask_free(dest_memory_nodes_mask_); + dest_memory_nodes_mask_ = nullptr; + } + } + + private: + uint32_t num_arenas_{0}; + uint32_t arena_mask_{0}; + + bool arenas_initialized_{false}; + std::vector arena_index_; + + std::string dest_memory_nodes_; + bitmask* dest_memory_nodes_mask_ = nullptr; + + unsigned createOneArena(); + unsigned getArenaForCurrentThread(); + void destroyArenas(); + + static void* allocateWithFlags(size_t size, int flags); + static int checkAlignment(size_t alignment); + + inline void initializeArenas() { + if (KVDK_LIKELY(arenas_initialized_ || num_arenas_ == 0)) { + return; + } + + auto ul = Allocator::AcquireLock(); + if (arenas_initialized_ || num_arenas_ == 0) { + return; + } + + for (uint32_t i = 0; i < num_arenas_; i++) { + unsigned arena_index = createOneArena(); + arena_index_.push_back(arena_index); + } + + arenas_initialized_ = true; + } + + void setDestMemoryNodesMask() { + if (dest_memory_nodes_mask_) { + numa_bitmask_free(dest_memory_nodes_mask_); + dest_memory_nodes_mask_ = nullptr; + } + + if (dest_memory_nodes_.size() == 0) { + return; + } + + dest_memory_nodes_mask_ = numa_parse_nodestring(dest_memory_nodes_.c_str()); + if (dest_memory_nodes_mask_ == 0) { + GlobalLogger.Error("Invalid value of `dest_numa_nodes`: %s.\n", + dest_memory_nodes_.c_str()); + } + } +}; +#endif // #ifdef KVDK_WITH_JEMALLOC + +// Chunk based simple implementation +// TODO: optimize, implement free +class ChunkBasedAllocator { + public: + SpaceEntry Allocate(uint64_t size); + void Free(const SpaceEntry& entry); + + ChunkBasedAllocator(uint32_t max_access_threads, Allocator* alloc) + : dalloc_thread_cache_(max_access_threads), alloc_(alloc) {} + ChunkBasedAllocator(ChunkBasedAllocator const&) = delete; + ChunkBasedAllocator(ChunkBasedAllocator&&) = delete; + ~ChunkBasedAllocator() { + for (uint64_t i = 0; i < dalloc_thread_cache_.size(); i++) { + auto& tc = dalloc_thread_cache_[i]; + for (auto chunk : tc.allocated_chunks) { + alloc_->Free(chunk); + } + } + } + + template + inline T* offset2addr(MemoryOffsetType offset) const { + return alloc_->offset2addr(offset); + } + + private: + struct alignas(64) DAllocThreadCache { + char* chunk_addr = nullptr; + uint64_t usable_bytes = 0; + std::vector allocated_chunks; + + DAllocThreadCache() = default; + DAllocThreadCache(const DAllocThreadCache&) = delete; + DAllocThreadCache(DAllocThreadCache&&) = delete; + }; + + const uint32_t chunk_size_ = (1 << 20); + Array dalloc_thread_cache_; + Allocator* alloc_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/engine/engine.cpp b/volatile/engine/engine.cpp similarity index 95% rename from engine/engine.cpp rename to volatile/engine/engine.cpp index 9ba3bf3a..c4169ddb 100644 --- a/engine/engine.cpp +++ b/volatile/engine/engine.cpp @@ -2,7 +2,7 @@ * Copyright(c) 2021 Intel Corporation */ -#include "kvdk/engine.hpp" +#include "kvdk/volatile/engine.hpp" #include "kv_engine.hpp" diff --git a/volatile/engine/hash_collection/hash_list.cpp b/volatile/engine/hash_collection/hash_list.cpp new file mode 100644 index 00000000..d85ede4b --- /dev/null +++ b/volatile/engine/hash_collection/hash_list.cpp @@ -0,0 +1,424 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "hash_list.hpp" + +namespace KVDK_NAMESPACE { +HashList::WriteResult HashList::Put(const StringView& key, + const StringView& value, + TimestampType timestamp) { + WriteResult ret; + HashWriteArgs args = InitWriteArgs(key, value, WriteOp::Put); + ret.s = PrepareWrite(args, timestamp); + if (ret.s == Status::Ok) { + ret = Write(args); + } + return ret; +} + +Status HashList::Get(const StringView& key, std::string* value) { + std::string internal_key(InternalKey(key)); + auto lookup_result = + hash_table_->Lookup(internal_key, RecordType::HashElem); + if (lookup_result.s != Status::Ok || + lookup_result.entry.GetRecordStatus() == RecordStatus::Outdated) { + return Status::NotFound; + } + + DLRecord* data_record = lookup_result.entry.GetIndex().dl_record; + kvdk_assert(data_record->GetRecordType() == RecordType::HashElem, ""); + // As get is lockless, skiplist node may point to a new elem delete record + // after we get it from hashtable + if (data_record->GetRecordStatus() == RecordStatus::Outdated) { + return Status::NotFound; + } else { + value->assign(data_record->Value().data(), data_record->Value().size()); + return Status::Ok; + } +} + +HashList::WriteResult HashList::Delete(const StringView& key, + TimestampType timestamp) { + WriteResult ret; + HashWriteArgs args = InitWriteArgs(key, "", WriteOp::Delete); + ret.s = PrepareWrite(args, timestamp); + if (ret.s == Status::Ok && args.space.size > 0) { + ret = Write(args); + } + return ret; +} + +HashList::WriteResult HashList::Modify(const StringView key, + ModifyFunc modify_func, + void* modify_args, TimestampType ts) { + WriteResult ret; + std::string internal_key(InternalKey(key)); + auto lookup_result = + hash_table_->Lookup(internal_key, RecordType::HashElem); + DLRecord* existing_record = nullptr; + std::string exisiting_value; + std::string new_value; + bool data_existing = false; + if (lookup_result.s == Status::Ok) { + existing_record = lookup_result.entry.GetIndex().dl_record; + ret.existing_record = existing_record; + if (existing_record->GetRecordStatus() != RecordStatus::Outdated) { + data_existing = true; + exisiting_value.assign(existing_record->Value().data(), + existing_record->Value().size()); + } + } else if (lookup_result.s == Status::NotFound) { + // nothing todo + } else { + ret.s = lookup_result.s; + return ret; + } + + auto modify_operation = modify_func( + data_existing ? &exisiting_value : nullptr, &new_value, modify_args); + switch (modify_operation) { + case ModifyOperation::Write: { + // TODO: check new value size + HashWriteArgs args = InitWriteArgs(key, new_value, WriteOp::Put); + args.ts = ts; + args.lookup_result = lookup_result; + args.space = kv_allocator_->Allocate( + DLRecord::RecordSize(internal_key, new_value)); + if (args.space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + return Write(args); + } + + case ModifyOperation::Delete: { + HashWriteArgs args = InitWriteArgs(key, "", WriteOp::Delete); + args.ts = ts; + args.lookup_result = lookup_result; + args.space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, "")); + if (args.space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + return Write(args); + } + + case ModifyOperation::Abort: { + ret.s = Status::Abort; + return ret; + } + + case ModifyOperation::Noop: { + ret.s = Status::Ok; + return ret; + } + + default: { + std::abort(); // non-reach area + } + } +} + +HashWriteArgs HashList::InitWriteArgs(const StringView& key, + const StringView& value, WriteOp op) { + HashWriteArgs args; + args.key = key; + args.value = value; + args.op = op; + args.collection = Name(); + args.hlist = this; + return args; +} + +Status HashList::PrepareWrite(HashWriteArgs& args, TimestampType ts) { + kvdk_assert(args.op == WriteOp::Put || args.value.size() == 0, + "value of delete operation should be empty"); + if (args.hlist != this) { + return Status::InvalidArgument; + } + + args.ts = ts; + bool op_delete = args.op == WriteOp::Delete; + std::string internal_key(InternalKey(args.key)); + bool allocate_space = true; + if (op_delete) { + args.lookup_result = + hash_table_->Lookup(internal_key, RecordType::HashElem); + } else { + args.lookup_result = + hash_table_->Lookup(internal_key, RecordType::HashElem); + } + + switch (args.lookup_result.s) { + case Status::Ok: { + if (op_delete && args.lookup_result.entry.GetRecordStatus() == + RecordStatus::Outdated) { + allocate_space = false; + } + break; + } + case Status::NotFound: { + if (op_delete) { + allocate_space = false; + } + break; + } + case Status::MemoryOverflow: { + return args.lookup_result.s; + } + default: + std::abort(); // never should reach + } + + if (allocate_space) { + auto request_size = DLRecord::RecordSize(internal_key, args.value); + args.space = kv_allocator_->Allocate(request_size); + if (args.space.size == 0) { + return Status::MemoryOverflow; + } + } + + return Status::Ok; +} + +HashList::WriteResult HashList::Write(HashWriteArgs& args) { + WriteResult ret; + if (args.hlist != this) { + ret.s = Status::InvalidArgument; + return ret; + } + if (args.op == WriteOp::Put) { + ret = putPrepared(args.lookup_result, args.key, args.value, args.ts, + args.space); + if (ret.existing_record == nullptr || + ret.existing_record->GetRecordStatus() == RecordStatus::Outdated) { + UpdateSize(1); + } + } else { + ret = deletePrepared(args.lookup_result, args.key, args.ts, args.space); + if (ret.existing_record != nullptr && + ret.existing_record->GetRecordStatus() == RecordStatus::Normal) { + UpdateSize(-1); + } + } + return ret; +} + +HashList::WriteResult HashList::SetExpireTime(ExpireTimeType expired_time, + TimestampType timestamp) { + WriteResult ret; + DLRecord* header = HeaderRecord(); + SpaceEntry space = kv_allocator_->Allocate( + DLRecord::RecordSize(header->Key(), header->Value())); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space.offset), space.size, timestamp, + RecordType::HashHeader, RecordStatus::Normal, + kv_allocator_->addr2offset_checked(header), header->prev, header->next, + header->Key(), header->Value(), expired_time); + bool success = dl_list_.Replace(header, data_record); + kvdk_assert(success, "existing header should be linked on its list"); + ret.existing_record = header; + ret.write_record = data_record; + return ret; +} + +Status HashList::CheckIndex() { + DLRecord* prev = HeaderRecord(); + size_t cnt = 0; + DLListRecoveryUtils recovery_utils(kv_allocator_); + while (true) { + DLRecord* curr = kv_allocator_->offset2addr_checked(prev->next); + if (curr == HeaderRecord()) { + break; + } + StringView key = curr->Key(); + auto ret = hash_table_->Lookup(key, curr->GetRecordType()); + if (ret.s != Status::Ok) { + GlobalLogger.Error( + "Check hash index error: record not exist in hash table\n"); + return Status::Abort; + } + if (ret.entry.GetIndex().dl_record != curr) { + GlobalLogger.Error( + "Check hash index error: Dlrecord miss-match with hash " + "table\n"); + return Status::Abort; + } + if (!recovery_utils.CheckLinkage(curr)) { + GlobalLogger.Error("Check hash index error: dl record linkage error\n"); + return Status::Abort; + } + cnt++; + prev = curr; + } + return Status::Ok; +} + +CollectionIDType HashList::FetchID(const DLRecord* record) { + assert(record != nullptr); + switch (record->GetRecordType()) { + case RecordType::HashElem: + return ExtractID(record->Key()); + case RecordType::HashHeader: + return DecodeID(record->Value()); + default: + GlobalLogger.Error("Wrong record type %u in HashListID", + record->GetRecordType()); + kvdk_assert(false, "Wrong type in HashListID"); + return 0; + } +} + +void HashList::Destroy() { + std::vector to_free; + DLRecord* header = HeaderRecord(); + if (header) { + DLRecord* to_destroy = nullptr; + do { + to_destroy = kv_allocator_->offset2addr_checked(header->next); + StringView key = to_destroy->Key(); + auto ul = hash_table_->AcquireLock(key); + if (dl_list_.Remove(to_destroy)) { + auto lookup_result = + hash_table_->Lookup(key, to_destroy->GetRecordType()); + if (lookup_result.s == Status::Ok) { + DLRecord* hash_indexed_record = nullptr; + auto hash_index = lookup_result.entry.GetIndex(); + switch (lookup_result.entry.GetIndexType()) { + case PointerType::HashList: + hash_indexed_record = hash_index.hlist->HeaderRecord(); + break; + case PointerType::DLRecord: + hash_indexed_record = hash_index.dl_record; + break; + default: + kvdk_assert(false, "Wrong hash index type of hash record"); + } + + if (hash_indexed_record == to_destroy) { + hash_table_->Erase(lookup_result.entry_ptr); + } + } + + to_destroy->Destroy(); + to_free.emplace_back(kv_allocator_->addr2offset_checked(to_destroy), + to_destroy->GetRecordSize()); + if (to_free.size() > kMaxCachedOldRecords) { + kv_allocator_->BatchFree(to_free); + to_free.clear(); + } + } + } while (to_destroy != header); + } + kv_allocator_->BatchFree(to_free); +} + +void HashList::DestroyAll() { + std::vector to_free; + DLRecord* header = HeaderRecord(); + DLRecord* to_destroy = nullptr; + kvdk_assert(header != nullptr, ""); + do { + to_destroy = kv_allocator_->offset2addr_checked(header->next); + StringView key = to_destroy->Key(); + auto ul = hash_table_->AcquireLock(key); + if (dl_list_.Remove(to_destroy)) { + auto lookup_result = + hash_table_->Lookup(key, to_destroy->GetRecordType()); + if (lookup_result.s == Status::Ok) { + DLRecord* hash_indexed_record = nullptr; + auto hash_index = lookup_result.entry.GetIndex(); + switch (lookup_result.entry.GetIndexType()) { + case PointerType::HashList: + hash_indexed_record = hash_index.hlist->HeaderRecord(); + break; + case PointerType::DLRecord: + hash_indexed_record = hash_index.dl_record; + break; + default: + kvdk_assert(false, "Wrong hash index type of hash record"); + } + + if (hash_indexed_record == to_destroy) { + hash_table_->Erase(lookup_result.entry_ptr); + } + } + auto old_record = + kv_allocator_->offset2addr(to_destroy->old_version); + while (old_record) { + auto old_version = old_record->old_version; + to_free.emplace_back(kv_allocator_->addr2offset_checked(old_record), + old_record->GetRecordSize()); + old_record->Destroy(); + old_record = kv_allocator_->offset2addr(old_version); + } + + to_free.emplace_back(kv_allocator_->addr2offset_checked(to_destroy), + to_destroy->GetRecordSize()); + to_destroy->Destroy(); + if (to_free.size() > kMaxCachedOldRecords) { + kv_allocator_->BatchFree(to_free); + to_free.clear(); + } + } + } while (to_destroy != header); + kv_allocator_->BatchFree(to_free); +} + +HashList::WriteResult HashList::putPrepared( + const HashTable::LookupResult& lookup_result, const StringView& key, + const StringView& value, TimestampType timestamp, const SpaceEntry& space) { + WriteResult ret; + std::string internal_key(InternalKey(key)); + DLList::WriteArgs args(internal_key, value, RecordType::HashElem, + RecordStatus::Normal, timestamp, space); + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + ret.hash_entry_ptr = lookup_result.entry_ptr; + if (lookup_result.s == Status::Ok) { + ret.existing_record = lookup_result.entry.GetIndex().dl_record; + kvdk_assert(timestamp > ret.existing_record->GetTimestamp(), ""); + while ((ret.s = dl_list_.Update(args, ret.existing_record)) != Status::Ok) { + kvdk_assert(ret.s == Status::Fail, ""); + } + } else { + kvdk_assert(lookup_result.s == Status::NotFound, ""); + bool push_back = fast_random_64() % 2 == 0; + Status s = push_back ? dl_list_.PushBack(args) : dl_list_.PushFront(args); + kvdk_assert(s == Status::Ok, ""); + } + hash_table_->Insert(lookup_result, RecordType::HashElem, RecordStatus::Normal, + ret.write_record, PointerType::DLRecord); + return ret; +} + +HashList::WriteResult HashList::deletePrepared( + const HashTable::LookupResult& lookup_result, const StringView& key, + TimestampType timestamp, const SpaceEntry& space) { + WriteResult ret; + std::string internal_key(InternalKey(key)); + kvdk_assert(lookup_result.s == Status::Ok && + lookup_result.entry.GetRecordType() == RecordType::HashElem && + lookup_result.entry.GetRecordStatus() == RecordStatus::Normal, + ""); + assert(space.size >= DLRecord::RecordSize(internal_key, "")); + ret.existing_record = lookup_result.entry.GetIndex().dl_record; + kvdk_assert(timestamp > ret.existing_record->GetTimestamp(), ""); + DLList::WriteArgs args(internal_key, "", RecordType::HashElem, + RecordStatus::Outdated, timestamp, space); + while ((ret.s = dl_list_.Update(args, ret.existing_record)) != Status::Ok) { + kvdk_assert(ret.s == Status::Fail, ""); + } + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + hash_table_->Insert(lookup_result, RecordType::HashElem, + RecordStatus::Outdated, ret.write_record, + PointerType::DLRecord); + return ret; +} + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/hash_collection/hash_list.hpp b/volatile/engine/hash_collection/hash_list.hpp new file mode 100644 index 00000000..b64f21f9 --- /dev/null +++ b/volatile/engine/hash_collection/hash_list.hpp @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#pragma once + +#include "../dl_list.hpp" +#include "../hash_table.hpp" +#include "kvdk/volatile/types.hpp" + +namespace KVDK_NAMESPACE { + +class HashIteratorImpl; + +struct HashWriteArgs { + StringView collection; + StringView key; + StringView value; + WriteOp op; + HashList* hlist; + SpaceEntry space; + TimestampType ts; + HashTable::LookupResult lookup_result; +}; + +class HashList : public Collection { + public: + struct WriteResult { + Status s = Status::Ok; + DLRecord* existing_record = nullptr; + DLRecord* write_record = nullptr; + HashEntry* hash_entry_ptr = nullptr; + }; + + HashList(DLRecord* header, const StringView& name, CollectionIDType id, + Allocator* kv_allocator, HashTable* hash_table, + LockTable* lock_table) + : Collection(name, id), + dl_list_(header, kv_allocator, lock_table), + size_(0), + kv_allocator_(kv_allocator), + hash_table_(hash_table) {} + + ~HashList() final = default; + + DLList* GetDLList() { return &dl_list_; } + + const DLRecord* HeaderRecord() const { return dl_list_.Header(); } + + DLRecord* HeaderRecord() { return dl_list_.Header(); } + + ExpireTimeType GetExpireTime() const final { + return HeaderRecord()->GetExpireTime(); + } + + TimestampType GetTimeStamp() const { return HeaderRecord()->GetTimestamp(); } + + bool HasExpired() const final { return HeaderRecord()->HasExpired(); } + + // Return number of valid data record in this hash list + size_t Size() { return size_; } + + // Put "key, value" to the hash list + // + // Args: + // * timestamp: kvdk engine timestamp of this operation + // + // Return Ok on success, with the writed data record, its dram node and + // updated data record if it exists + // + // Notice: the putting key should already been locked by engine + WriteResult Put(const StringView& key, const StringView& value, + TimestampType timestamp); + + // Get value of "key" from the hash list + Status Get(const StringView& key, std::string* value); + + // Delete "key" from the hash list by replace it with a delete record + // + // Args: + // * timestamp: kvdk engine timestamp of this operation + // + // Return Ok on success, with the writed delete record and deleted + // record if it exists + // + // Notice: the deleting key should already been locked by engine + WriteResult Delete(const StringView& key, TimestampType timestamp); + + // Modify value of "key" in the hash list + // + // Args: + // * modify_func: customized function to modify existing value of key. See + // definition of ModifyFunc (types.hpp) for more details. + // * modify_args: customized arguments of modify_func. + // + // Return: + // Status::Ok if modify success. + // Status::Abort if modify function abort modifying. + // Return other non-Ok status on any error. + WriteResult Modify(const StringView key, ModifyFunc modify_func, + void* modify_args, TimestampType timestamp); + + // Init args for put or delete operations + HashWriteArgs InitWriteArgs(const StringView& key, const StringView& value, + WriteOp op); + + // Prepare neccessary resources for write, store lookup result of key and + // required memory space to write new reocrd in args + // + // Args: + // * args: generated by InitWriteArgs() + // + // Return: + // Ok on success + // MemoryOverflow if no enough kv memory space + // MemoryOverflow if no enough dram space + // + // Notice: args.key should already been locked by engine + Status PrepareWrite(HashWriteArgs& args, TimestampType ts); + + // Do batch write according to args + // + // Args: + // * args: write args prepared by PrepareWrite() + // + // Return: + // Status Ok on success, with the writed delete record, its dram node and + // deleted record if existing + WriteResult Write(HashWriteArgs& args); + + // Set this hash list expire at expired_time + // + // Args: + // * expired_time: time to expire + // * timestamp: kvdk engine timestamp of calling this function + // + // Return Ok on success + WriteResult SetExpireTime(ExpireTimeType expired_time, + TimestampType timestamp); + + // Replace "old_record" from the hash list with "replacing_record" + // + // Args: + // * old_record: existing record to be replaced + // * new_record: new reocrd to replace the older one + // + // Return: + // * true on success + // * false if old_record not linked on a skiplist + // + // Notice: + // 1. key of the replacing record should already been locked by engine + // 2. hash table will not be modified + bool Replace(DLRecord* old_record, DLRecord* new_record) { + return dl_list_.Replace(old_record, new_record); + } + + // Destroy and free the whole hash list with old version list. + void DestroyAll(); + + // Destroy and free the whole hash list of newest version records + void Destroy(); + + void UpdateSize(int64_t delta) { + kvdk_assert(delta >= 0 || size_.load() >= static_cast(-delta), + "Update hash list size to negative"); + size_.fetch_add(delta, std::memory_order_relaxed); + } + + Status CheckIndex(); + + bool TryCleaningLock() { return cleaning_lock_.try_lock(); } + + void ReleaseCleaningLock() { cleaning_lock_.unlock(); } + + static CollectionIDType FetchID(const DLRecord* record); + + static bool MatchType(const DLRecord* record) { + RecordType type = record->GetRecordType(); + return type == RecordType::HashElem || type == RecordType::HashHeader; + } + + private: + friend HashIteratorImpl; + DLList dl_list_; + std::atomic size_; + Allocator* kv_allocator_; + HashTable* hash_table_; + // to avoid illegal access caused by cleaning skiplist by multi-thread + SpinMutex cleaning_lock_; + + WriteResult putPrepared(const HashTable::LookupResult& lookup_result, + const StringView& key, const StringView& value, + TimestampType timestamp, const SpaceEntry& space); + + WriteResult deletePrepared(const HashTable::LookupResult& lookup_result, + const StringView& key, TimestampType timestamp, + const SpaceEntry& space); +}; +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/hash_collection/iterator.hpp b/volatile/engine/hash_collection/iterator.hpp new file mode 100644 index 00000000..d1d2c21f --- /dev/null +++ b/volatile/engine/hash_collection/iterator.hpp @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#pragma once + +#include "../version/version_controller.hpp" +#include "hash_list.hpp" +#include "kvdk/volatile/engine.hpp" +#include "kvdk/volatile/iterator.hpp" + +namespace KVDK_NAMESPACE { +class KVEngine; + +class HashIteratorImpl final : public HashIterator { + public: + HashIteratorImpl(HashList* hlist, const SnapshotImpl* snapshot, + bool own_snapshot) + : hlist_(hlist), + snapshot_(snapshot), + own_snapshot_(own_snapshot), + dl_iter_(&hlist->dl_list_, hlist->kv_allocator_, snapshot) {} + void SeekToFirst() final { dl_iter_.SeekToFirst(); } + + void SeekToLast() final { dl_iter_.SeekToLast(); } + + bool Valid() const final { return dl_iter_.Valid(); } + + void Next() final { dl_iter_.Next(); } + + void Prev() final { dl_iter_.Prev(); } + + std::string Key() const final { + if (!Valid()) { + kvdk_assert(false, "Accessing data with invalid HashIterator!"); + return std::string{}; + } + return string_view_2_string(Collection::ExtractUserKey(dl_iter_.Key())); + } + + std::string Value() const final { + if (!Valid()) { + kvdk_assert(false, "Accessing data with invalid HashIterator!"); + return std::string{}; + } + return string_view_2_string(dl_iter_.Value()); + } + + bool MatchKey(std::regex const& re) final { + if (!Valid()) { + kvdk_assert(false, "Accessing data with invalid HashIterator!"); + return false; + } + return std::regex_match(Key(), re); + } + + private: + friend KVEngine; + + HashList* hlist_; + const SnapshotImpl* snapshot_; + bool own_snapshot_; + DLListDataIterator dl_iter_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/hash_table.cpp b/volatile/engine/hash_table.cpp new file mode 100644 index 00000000..3ffedbfe --- /dev/null +++ b/volatile/engine/hash_table.cpp @@ -0,0 +1,205 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "hash_table.hpp" + +#include "hash_collection/hash_list.hpp" +#include "list_collection/list.hpp" +#include "sorted_collection/skiplist.hpp" +#include "thread_manager.hpp" + +namespace KVDK_NAMESPACE { +HashTable* HashTable::NewHashTable(uint64_t hash_bucket_num, + uint32_t num_buckets_per_slot, + const Allocator* kv_allocator, + Allocator* new_bucket_allocator, + uint32_t max_access_threads) { + HashTable* table; + // We catch exception here as we may need to allocate large memory for hash + // table here + try { + table = new HashTable(hash_bucket_num, num_buckets_per_slot, kv_allocator, + new_bucket_allocator, max_access_threads); + } catch (std::bad_alloc& b) { + GlobalLogger.Error("No enough dram to create global hash table: b\n", + b.what()); + table = nullptr; + } + + return table; +} + +bool HashEntry::Match(const StringView& key, uint32_t hash_k_prefix, + uint8_t target_type, DataEntry* data_entry_metadata) { + if ((target_type & header_.record_type) && + hash_k_prefix == header_.key_prefix) { + void* data_record = nullptr; + StringView data_entry_key; + + switch (header_.index_type) { + case PointerType::Empty: + case PointerType::Allocated: { + return false; + } + case PointerType::StringRecord: { + data_record = index_.string_record; + data_entry_key = index_.string_record->Key(); + break; + } + case PointerType::HashElem: + case PointerType::DLRecord: { + data_record = index_.dl_record; + data_entry_key = index_.dl_record->Key(); + break; + } + case PointerType::List: { + data_entry_key = index_.list->Name(); + break; + } + case PointerType::HashList: { + data_entry_key = index_.hlist->Name(); + break; + } + case PointerType::SkiplistNode: { + SkiplistNode* dram_node = index_.skiplist_node; + data_record = dram_node->record; + data_entry_key = dram_node->record->Key(); + break; + } + case PointerType::Skiplist: { + Skiplist* skiplist = index_.skiplist; + data_record = skiplist->HeaderRecord(); + data_entry_key = skiplist->Name(); + break; + } + default: { + GlobalLogger.Error("Not supported hash index type: %u\n", + header_.index_type); + assert(false && "Trying to use invalid PointerType!"); + return false; + } + } + + if (data_entry_metadata != nullptr) { + memcpy(data_entry_metadata, data_record, sizeof(DataEntry)); + } + + if (equal_string_view(key, data_entry_key)) { + return true; + } + } + return false; +} + +template +HashTable::LookupResult HashTable::Lookup(const StringView& key, + uint8_t type_mask) { + LookupResult ret; + HashEntry* empty_entry = nullptr; + auto hint = getHint(key); + ret.key_hash_prefix = hint.key_hash_prefix; + + HashBucket* bucket_ptr = &hash_buckets_[hint.bucket]; + _mm_prefetch(bucket_ptr, _MM_HINT_T0); + + // search cache + ret.entry_ptr = slots_[hint.slot].hash_cache.entry_ptr; + if (ret.entry_ptr != nullptr) { + atomic_load_16(&ret.entry, ret.entry_ptr); + if (ret.entry.Match(key, hint.key_hash_prefix, type_mask, nullptr)) { + return ret; + } + } + + // iterate hash entries in the bucket + HashBucketIterator iter(this, hint.bucket); + while (iter.Valid()) { + ret.entry_ptr = &*iter; + atomic_load_16(&ret.entry, ret.entry_ptr); + if (ret.entry.Match(key, hint.key_hash_prefix, type_mask, nullptr)) { + slots_[hint.slot].hash_cache.entry_ptr = ret.entry_ptr; + return ret; + } + if (ret.entry_ptr->Empty()) { + empty_entry = ret.entry_ptr; + } + iter++; + } + + if (may_insert) { + if (empty_entry == nullptr) { + ret.s = allocateEntry(iter); + if (ret.s != Status::Ok) { + kvdk_assert(ret.s == Status::MemoryOverflow, ""); + return ret; + } + kvdk_assert( + iter.Valid(), + "HashBucketIterator should be valid after allocate new entry"); + kvdk_assert(iter->Empty(), "newly allocated hash entry should be empty"); + ret.entry_ptr = &(*iter); + } else { + ret.entry_ptr = empty_entry; + } + } + + ret.s = NotFound; + if (may_insert) { + ret.entry_ptr->MarkAsAllocated(); + } + return ret; +} + +template HashTable::LookupResult HashTable::Lookup(const StringView&, + uint8_t); +template HashTable::LookupResult HashTable::Lookup(const StringView&, + uint8_t); + +void HashTable::Insert(const LookupResult& insert_position, RecordType type, + RecordStatus status, void* index, + PointerType index_type) { + HashEntry new_hash_entry(insert_position.key_hash_prefix, type, status, index, + index_type); + atomic_store_16(insert_position.entry_ptr, &new_hash_entry); +} + +HashTable::LookupResult HashTable::Insert(const StringView& key, + RecordType type, RecordStatus status, + void* index, PointerType index_type) { + auto lookup_result = Lookup(key, type); + if (lookup_result.s == Status::Ok || lookup_result.s == Status::NotFound) { + Insert(lookup_result, type, status, index, index_type); + } + return lookup_result; +} + +Status HashTable::allocateEntry(HashBucketIterator& bucket_iter) { + kvdk_assert(bucket_iter.hash_table_ == this, ""); + kvdk_assert( + bucket_iter.entry_idx_ == hash_bucket_entries_[bucket_iter.bucket_idx_], + "Only allocate new hash entry at end of hash bucket"); + kvdk_assert(bucket_iter.bucket_ptr_ != nullptr, ""); + if (hash_bucket_entries_[bucket_iter.bucket_idx_] > 0 && + hash_bucket_entries_[bucket_iter.bucket_idx_] % kNumEntryPerBucket == 0) { + auto space = chunk_based_bucket_allocator_.Allocate(kHashBucketSize); + if (space.size == 0) { + GlobalLogger.Error("MemoryOverflow!\n"); + return Status::MemoryOverflow; + } + bucket_iter.bucket_ptr_->next = + chunk_based_bucket_allocator_.offset2addr(space.offset); + bucket_iter.bucket_ptr_ = bucket_iter.bucket_ptr_->next; + } + bucket_iter.entry_idx_ = hash_bucket_entries_[bucket_iter.bucket_idx_]++; + bucket_iter->Clear(); + kvdk_assert(bucket_iter.Valid(), ""); + return Status::Ok; +} + +HashTableIterator HashTable::GetIterator(uint64_t start_slot_idx, + uint64_t end_slot_idx) { + return HashTableIterator{this, start_slot_idx, end_slot_idx}; +} + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/hash_table.hpp b/volatile/engine/hash_table.hpp new file mode 100644 index 00000000..f5720109 --- /dev/null +++ b/volatile/engine/hash_table.hpp @@ -0,0 +1,420 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include +#include + +#include "alias.hpp" +#include "allocator.hpp" +#include "data_record.hpp" +#include "dram_allocator.hpp" +#include "kvdk/volatile/engine.hpp" +#include "structures.hpp" + +namespace KVDK_NAMESPACE { + +class Skiplist; +class SkiplistNode; +struct HashBucketIterator; + +struct List; +struct HashList; + +struct alignas(16) HashEntry { + public: + friend class HashTable; + HashEntry& operator=(const HashEntry&) = delete; + HashEntry(const HashEntry& hash_entry) { atomic_load_16(this, &hash_entry); } + union Index { + Index(void* _ptr) : ptr(_ptr) {} + Index() = default; + void* ptr; + SkiplistNode* skiplist_node; + StringRecord* string_record; + DLRecord* dl_record; + Skiplist* skiplist; + List* list; + HashList* hlist; + }; + static_assert(sizeof(Index) == 8); + + HashEntry() = default; + + HashEntry(uint32_t key_hash_prefix, RecordType record_type, + RecordStatus record_status, void* _index, PointerType index_type) + : index_(_index), + header_({key_hash_prefix, record_type, record_status, index_type}) {} + + bool Empty() { return header_.index_type == PointerType::Empty; } + + // Make this hash entry empty while its content been deleted + void Clear() { header_.index_type = PointerType::Empty; } + + bool Allocated() { return header_.index_type == PointerType::Allocated; } + + void MarkAsAllocated() { header_.index_type = PointerType::Allocated; } + + Index GetIndex() const { return index_; } + + PointerType GetIndexType() const { return header_.index_type; } + + RecordType GetRecordType() const { return header_.record_type; } + + RecordStatus GetRecordStatus() const { return header_.record_status; } + + // Check if "key" of data type "target_type" is indexed by "this". If + // matches, copy data entry of data record of "key" to "data_entry_metadata" + // and return true, otherwise return false. + // + // Args: + // * target_type: a mask of RecordType, search all masked types + bool Match(const StringView& key, uint32_t hash_k_prefix, uint8_t target_type, + DataEntry* data_entry_metadata); + + private: + struct EntryHeader { + uint32_t key_prefix; + RecordType record_type; + RecordStatus record_status; + PointerType index_type; + }; + + Index index_; + EntryHeader header_; +}; +static_assert(sizeof(HashEntry) == 16); + +// Size of each hash bucket +// +// It should be larger than hans entry size (which is 16) plus 8 (the pointer +// to next bucket). It is recommended to set it align to cache line +constexpr size_t kHashBucketSize = 128; +constexpr size_t kNumEntryPerBucket = + (kHashBucketSize - sizeof(void*)) / sizeof(HashEntry); +struct HashBucket { + HashBucket() { + memset(hash_entries, 0, sizeof(HashEntry) * kNumEntryPerBucket); + } + HashEntry hash_entries[kNumEntryPerBucket]; + HashBucket* next{nullptr}; + char padding[kHashBucketSize - sizeof(hash_entries) - sizeof(next)]; +}; +static_assert(sizeof(HashBucket) == kHashBucketSize); + +struct HashCache { + HashEntry* entry_ptr = nullptr; +}; + +struct Slot { + HashCache hash_cache; + SpinMutex spin; +}; + +struct HashTableIterator; + +class HashTable { + public: + friend class HashTableIterator; + friend class HashSlotIterator; + friend class HashBucketIterator; + struct LookupResult { + public: + Status s{Status::Ok}; + HashEntry entry{}; + HashEntry* entry_ptr{nullptr}; + + LookupResult& operator=(LookupResult const& other) { + s = other.s; + memcpy_16(&entry, &other.entry); + entry_ptr = other.entry_ptr; + key_hash_prefix = other.key_hash_prefix; + return *this; + } + + private: + friend class HashTable; + uint32_t key_hash_prefix; + }; + + static HashTable* NewHashTable(uint64_t hash_bucket_num, + uint32_t num_buckets_per_slot, + const Allocator* kv_allocator, + Allocator* new_bucket_allocator, + uint32_t max_access_threads); + + // Look up key in hashtable + // Store a copy of hash entry in LookupResult::entry, and a pointer to the + // hash entry on hash table in LookupResult::entry_ptr + // If may_insert is true and key not found, then store + // pointer of a free-to-write hash entry in LookupResult::entry_ptr. + // + // * type_mask: which data types to search + // + // return status: + // Status::NotFound is key is not found. + // Status::MemoryOverflow if may_insert is true but + // failed to allocate new hash entry + // Status::Ok on success. + // + // Notice: key should be locked if set may_insert to true + template + LookupResult Lookup(const StringView& key, uint8_t type_mask); + + // Insert a hash entry to hash table + // * insert_position: indicate the the postion to insert new entry, it should + // be return of Lookup of the inserting key + void Insert(const LookupResult& insert_position, RecordType type, + RecordStatus status, void* index, PointerType index_type); + + // Lookup and insert a hash entry of key to hash table, return lookup result + LookupResult Insert(const StringView& key, RecordType type, + RecordStatus status, void* index, PointerType index_type); + + // Erase a hash entry so it can be reused in future + void Erase(HashEntry* entry_ptr) { + assert(entry_ptr != nullptr); + entry_ptr->Clear(); + } + + std::unique_lock AcquireLock(StringView const& key) { + return std::unique_lock{*getHint(key).spin}; + } + + HashTableIterator GetIterator(uint64_t start_slot_idx, uint64_t end_slot_idx); + + size_t GetSlotsNum() { return slots_.size(); } + + // StringAlike is std::string or StringView + template + std::vector> RangeLock( + std::vector const& keys) { + std::vector spins; + for (auto const& key : keys) { + spins.push_back(getHint(key).spin); + } + std::sort(spins.begin(), spins.end()); + auto end = std::unique(spins.begin(), spins.end()); + + std::vector> guard; + for (auto iter = spins.begin(); iter != end; ++iter) { + guard.emplace_back(**iter); + } + return guard; + } + + private: + HashTable(uint64_t hash_bucket_num, uint32_t num_buckets_per_slot, + const Allocator* kv_allocator, Allocator* new_bucket_allocator, + uint32_t max_access_threads) + : num_hash_buckets_(hash_bucket_num), + num_buckets_per_slot_(num_buckets_per_slot), + kv_allocator_(kv_allocator), + chunk_based_bucket_allocator_{max_access_threads, new_bucket_allocator}, + slots_(hash_bucket_num / num_buckets_per_slot), + hash_bucket_entries_(hash_bucket_num, 0), + hash_buckets_(num_hash_buckets_) {} + + struct KeyHashHint { + uint32_t bucket; + uint32_t slot; + // hash value stored on hash entry + uint32_t key_hash_prefix; + SpinMutex* spin; + }; + + KeyHashHint getHint(const StringView& key) { + KeyHashHint hint; + uint64_t hash_val = hash_str(key.data(), key.size()); + hint.key_hash_prefix = hash_val >> 32; + hint.bucket = get_bucket_num(hash_val); + hint.slot = get_slot_num(hint.bucket); + hint.spin = &slots_[hint.slot].spin; + return hint; + } + + inline uint32_t get_bucket_num(uint64_t key_hash_value) { + return key_hash_value & (num_hash_buckets_ - 1); + } + + inline uint32_t get_slot_num(uint32_t bucket) { + return bucket / num_buckets_per_slot_; + } + + Status allocateEntry(HashBucketIterator& bucket_iter); + + const uint64_t num_hash_buckets_; + const uint32_t num_buckets_per_slot_; + const Allocator* kv_allocator_; + ChunkBasedAllocator chunk_based_bucket_allocator_; + Array slots_; + std::vector hash_bucket_entries_; + Array hash_buckets_; + void* main_buckets_; +}; + +// Iterator all hash entries in a hash table bucket +class HashBucketIterator { + public: + HashBucketIterator(HashTable* hash_table /* should be non-null */, + uint64_t bucket_idx) + : hash_table_(hash_table), + bucket_idx_(bucket_idx), + entry_idx_(0), + bucket_ptr_(nullptr) { + if (bucket_idx_ < hash_table_->hash_buckets_.size()) { + bucket_ptr_ = &hash_table_->hash_buckets_[bucket_idx_]; + _mm_prefetch(bucket_ptr_, _MM_HINT_T0); + } + } + + HashBucketIterator(const HashBucketIterator&) = default; + + bool Valid() { + return bucket_ptr_ != nullptr && + entry_idx_ < hash_table_->hash_bucket_entries_[bucket_idx_]; + } + + HashEntry& operator*() { + return bucket_ptr_->hash_entries[entry_idx_ % kNumEntryPerBucket]; + } + + HashEntry* operator->() { return &operator*(); } + + HashBucketIterator& operator++() { + next(); + return *this; + } + + HashBucketIterator operator++(int) { + HashBucketIterator tmp{*this}; + this->operator++(); + return tmp; + } + + private: + friend class HashTable; + + void next() { + if (Valid()) { + entry_idx_++; + if (entry_idx_ % kNumEntryPerBucket == 0 && Valid()) { + bucket_ptr_ = bucket_ptr_->next; + _mm_prefetch(bucket_ptr_, _MM_HINT_T0); + } + } + } + + HashTable* hash_table_; + uint64_t bucket_idx_; + uint64_t entry_idx_; + HashBucket* bucket_ptr_; +}; + +// Iterator all hash entries in a hash table slot +class HashSlotIterator { + public: + HashSlotIterator(HashTable* hash_table /* should be non null */, + uint64_t slot_idx) + : hash_table_(hash_table), + start_bucket_(hash_table_->num_buckets_per_slot_ * slot_idx), + end_bucket_(start_bucket_ + hash_table_->num_buckets_per_slot_), + current_bucket_(start_bucket_), + bucket_iter_(hash_table_, current_bucket_) { + getBucket(); + } + + HashEntry& operator*() { return *bucket_iter_; } + + HashEntry* operator->() { return &operator*(); } + + HashSlotIterator& operator++() { + next(); + return *this; + } + + HashSlotIterator operator++(int) { + auto tmp = *this; + this->operator++(); + return tmp; + } + + bool Valid() { + return bucket_iter_.Valid() && current_bucket_ < end_bucket_ && + current_bucket_ >= start_bucket_; + } + + private: + // Locate to bucket with hash entries from current + void getBucket() { + if (!bucket_iter_.Valid()) { + current_bucket_++; + while (current_bucket_ < end_bucket_) { + bucket_iter_ = HashBucketIterator(hash_table_, current_bucket_); + if (bucket_iter_.Valid()) { + return; + } + current_bucket_++; + } + } + } + + void next() { + if (Valid()) { + bucket_iter_++; + if (!bucket_iter_.Valid()) { + getBucket(); + } + } + } + + HashTable* hash_table_; + uint64_t start_bucket_; + uint64_t end_bucket_; + uint64_t current_bucket_; + HashBucketIterator bucket_iter_; +}; + +// Iterate all slots in a hashtable +struct HashTableIterator { + public: + HashTableIterator(HashTable* hash_table, uint64_t start_slot_idx, + uint64_t end_slot_idx) + : hash_table_(hash_table), + current_slot_idx_(start_slot_idx), + end_slot_idx_(end_slot_idx) {} + + std::unique_lock AcquireSlotLock() { + SpinMutex* slot_lock = GetSlotLock(); + return std::unique_lock(*slot_lock); + } + + void Next() { + if (Valid()) { + current_slot_idx_++; + } + } + + bool Valid() { return current_slot_idx_ < end_slot_idx_; } + + HashSlotIterator Slot() { + return HashSlotIterator{hash_table_, current_slot_idx_}; + } + + SpinMutex* GetSlotLock() { + return &hash_table_->slots_[current_slot_idx_].spin; + } + + private: + // lock current access slot + std::unique_lock iter_lock_slot_; + // current slot id + HashTable* hash_table_; + uint64_t current_slot_idx_; + uint64_t end_slot_idx_; +}; +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/kv_engine.cpp b/volatile/engine/kv_engine.cpp new file mode 100644 index 00000000..3fc1d0da --- /dev/null +++ b/volatile/engine/kv_engine.cpp @@ -0,0 +1,1064 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "kv_engine.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "backup_log.hpp" +#include "dram_allocator.hpp" +#include "hash_collection/iterator.hpp" +#include "kvdk/volatile/engine.hpp" +#include "list_collection/iterator.hpp" +#include "sorted_collection/iterator.hpp" +#include "structures.hpp" +#include "utils/sync_point.hpp" +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { + +// Set class name of memory allocator for volatile key values. +#ifndef KVDK_VOLATILE_KV_MEMORY_ALLOCATOR_CLASS +#define KVDK_VOLATILE_KV_MEMORY_ALLOCATOR_CLASS SystemMemoryAllocator +#endif +// Enable jemalloc memory allocator for volatile key values if available. +#ifdef KVDK_WITH_JEMALLOC +#undef KVDK_VOLATILE_KV_MEMORY_ALLOCATOR_CLASS +#define KVDK_VOLATILE_KV_MEMORY_ALLOCATOR_CLASS JemallocMemoryAllocator +#endif + +// Set class name of memory allocator for Skiplist nodes. +#ifndef KVDK_SKIPLIST_NODE_MEMORY_ALLOCATOR_CLASS +#define KVDK_SKIPLIST_NODE_MEMORY_ALLOCATOR_CLASS SystemMemoryAllocator +#endif + +// Set class name of memory allocator for Hash Table's new buckets. +// HashTable's initial buckets are allocated by `global_memory_allocator` +#ifndef KVDK_HASH_TABLE_NEW_BUCKET_MEMORY_ALLOCATOR_CLASS +#define KVDK_HASH_TABLE_NEW_BUCKET_MEMORY_ALLOCATOR_CLASS SystemMemoryAllocator +#endif + +KVEngine::~KVEngine() { + GlobalLogger.Info("Closing instance ... \n"); + GlobalLogger.Info("Waiting bg threads exit ... \n"); + closing_ = true; + terminateBackgroundWorks(); + // deleteCollections(); + ReportMemoryUsage(); + + GlobalLogger.Info("Instance closed\n"); +} + +Status KVEngine::Open(const StringView engine_path, Engine** engine_ptr, + const Configs& configs) { + std::string engine_path_str(string_view_2_string(engine_path)); + GlobalLogger.Info("Opening kvdk instance from %s ...\n", + engine_path_str.c_str()); + KVEngine* engine = new KVEngine(configs); + Status s = engine->init(engine_path_str, configs); + + if (s == Status::Ok) { + *engine_ptr = engine; + engine->startBackgroundWorks(); + engine->ReportMemoryUsage(); + } else { + GlobalLogger.Error("Init kvdk instance failed: %d\n", s); + delete engine; + } + return s; +} + +void KVEngine::ReportMemoryUsage() { + // Check KV allocator is initialized before use it. + // It may not be successfully initialized due to file operation errors. + if (kv_allocator_ == nullptr) { + return; + } + + auto bytes = global_memory_allocator()->BytesAllocated(); + auto total = bytes; + GlobalLogger.Info( + "[0] Global Memory Allocator Usage: %ld B, %ld KB, %ld MB, %ld GB\n", + bytes, (bytes / (1LL << 10)), (bytes / (1LL << 20)), + (bytes / (1LL << 30))); + + bytes = kv_allocator_->BytesAllocated(); + total += bytes; + GlobalLogger.Info( + "[1] KV Memory Allocator Usage: %ld B, %ld KB, %ld MB, %ld GB\n", bytes, + (bytes / (1LL << 10)), (bytes / (1LL << 20)), (bytes / (1LL << 30))); + + bytes = skiplist_node_allocator_->BytesAllocated(); + total += bytes; + GlobalLogger.Info( + "[2] Skiplist Node Memory Allocator Usage: %ld B, %ld KB, %ld MB, %ld " + "GB\n", + bytes, (bytes / (1LL << 10)), (bytes / (1LL << 20)), + (bytes / (1LL << 30))); + + bytes = hashtable_new_bucket_allocator_->BytesAllocated(); + total += bytes; + GlobalLogger.Info( + "[3] Hashtable New Bucket Memory Allocator Usage: %ld B, %ld KB, %ld MB, " + "%ld " + "GB\n", + bytes, (bytes / (1LL << 10)), (bytes / (1LL << 20)), + (bytes / (1LL << 30))); + + GlobalLogger.Info("[+] Total Memory Usage: %ld B, %ld KB, %ld MB, %ld GB\n", + total, (total / (1LL << 10)), (total / (1LL << 20)), + (total / (1LL << 30))); +} + +void KVEngine::startBackgroundWorks() { + std::unique_lock ul(bg_work_signals_.terminating_lock); + bg_work_signals_.terminating = false; + bg_threads_.emplace_back(&KVEngine::backgroundMemoryUsageReporter, this); + + bool close_reclaimer = false; + TEST_SYNC_POINT_CALLBACK("KVEngine::backgroundCleaner::NothingToDo", + &close_reclaimer); + if (!close_reclaimer) { + cleaner_.Start(); + } +} + +void KVEngine::terminateBackgroundWorks() { + cleaner_.Close(); + { + std::unique_lock ul(bg_work_signals_.terminating_lock); + bg_work_signals_.terminating = true; + bg_work_signals_.dram_cleaner_cv.notify_all(); + bg_work_signals_.memory_usage_reporter_cv.notify_all(); + } + for (auto& t : bg_threads_) { + t.join(); + } +} + +Status KVEngine::init(const std::string& name, const Configs& configs) { + Status s = Status::Ok; + configs_ = configs; + s = checkGeneralConfigs(configs); + if (s != Status::Ok) { + return s; + } + + (void)name; // To suppress compile warnings + Allocator* kv_allocator = new KVDK_VOLATILE_KV_MEMORY_ALLOCATOR_CLASS(); + kv_allocator->SetMaxAccessThreads(configs_.max_access_threads); + kv_allocator->SetDestMemoryNodes(configs_.dest_memory_nodes); + kv_allocator_.reset(kv_allocator); + + Allocator* skiplist_node_allocator = + new KVDK_SKIPLIST_NODE_MEMORY_ALLOCATOR_CLASS(); + skiplist_node_allocator->SetMaxAccessThreads(configs_.max_access_threads); + skiplist_node_allocator_.reset(skiplist_node_allocator); + + Allocator* hashtable_new_bucket_allocator = + new KVDK_HASH_TABLE_NEW_BUCKET_MEMORY_ALLOCATOR_CLASS(); + hashtable_new_bucket_allocator->SetMaxAccessThreads( + configs_.max_access_threads); + hashtable_new_bucket_allocator_.reset(hashtable_new_bucket_allocator); + + GlobalLogger.Info("Global memory allocator: %s\n", + global_memory_allocator()->AllocatorName().c_str()); + GlobalLogger.Info("KV memory allocator: %s\n", + kv_allocator_->AllocatorName().c_str()); + GlobalLogger.Info("Skiplist node memory allocator: %s\n", + skiplist_node_allocator_->AllocatorName().c_str()); + GlobalLogger.Info("Hashtable new bucket memory allocator: %s\n", + hashtable_new_bucket_allocator->AllocatorName().c_str()); + + hash_table_.reset(HashTable::NewHashTable( + configs_.hash_bucket_num, configs_.num_buckets_per_slot, + kv_allocator_.get(), hashtable_new_bucket_allocator, + configs_.max_access_threads)); + dllist_locks_.reset(new LockTable{1UL << 20}); + + if (kv_allocator_ == nullptr || hash_table_ == nullptr || + dllist_locks_ == nullptr) { + GlobalLogger.Error("Init kvdk basic components error\n"); + return Status::Abort; + } + + registerComparator("default", compare_string_view); + return s; +} + +Status KVEngine::Backup(const pmem::obj::string_view backup_log, + const Snapshot* snapshot) { + std::string backup_log_file = string_view_2_string(backup_log); + BackupLog backup; + Status s = backup.Init(backup_log_file); + GlobalLogger.Info("Backup instance to %s ...\n", backup_log_file.c_str()); + if (s != Status::Ok) { + return s; + } + TimestampType backup_ts = + static_cast(snapshot)->GetTimestamp(); + auto hashtable_iterator = + hash_table_->GetIterator(0, hash_table_->GetSlotsNum()); + while (hashtable_iterator.Valid()) { + auto ul = hashtable_iterator.AcquireSlotLock(); + auto slot_iter = hashtable_iterator.Slot(); + while (slot_iter.Valid()) { + switch (slot_iter->GetRecordType()) { + case RecordType::String: { + StringRecord* record = slot_iter->GetIndex().string_record; + while (record != nullptr && record->GetTimestamp() > backup_ts) { + record = + kv_allocator_->offset2addr(record->old_version); + } + if (record && record->GetRecordStatus() == RecordStatus::Normal && + !record->HasExpired()) { + s = backup.Append(RecordType::String, record->Key(), + record->Value(), record->GetExpireTime()); + } + break; + } + case RecordType::SortedHeader: { + DLRecord* header = slot_iter->GetIndex().skiplist->HeaderRecord(); + while (header != nullptr && header->GetTimestamp() > backup_ts) { + header = kv_allocator_->offset2addr(header->old_version); + } + if (header && header->GetRecordStatus() == RecordStatus::Normal && + !header->HasExpired()) { + s = backup.Append(RecordType::SortedHeader, header->Key(), + header->Value(), header->GetExpireTime()); + if (s == Status::Ok) { + // Append skiplist elems following the header + auto skiplist = getSkiplist(Skiplist::FetchID(header)); + kvdk_assert(skiplist != nullptr, + "Backup skiplist should exist in map"); + auto skiplist_iter = SortedIteratorImpl( + skiplist.get(), kv_allocator_.get(), + static_cast(snapshot), false); + for (skiplist_iter.SeekToFirst(); skiplist_iter.Valid(); + skiplist_iter.Next()) { + s = backup.Append(RecordType::SortedElem, skiplist_iter.Key(), + skiplist_iter.Value(), kPersistTime); + if (s != Status::Ok) { + break; + } + } + } + } + break; + } + case RecordType::HashHeader: { + DLRecord* header = slot_iter->GetIndex().hlist->HeaderRecord(); + while (header != nullptr && header->GetTimestamp() > backup_ts) { + header = kv_allocator_->offset2addr(header->old_version); + } + if (header && header->GetRecordStatus() == RecordStatus::Normal && + !header->HasExpired()) { + s = backup.Append(RecordType::HashHeader, header->Key(), + header->Value(), header->GetExpireTime()); + if (s == Status::Ok) { + // Append hlist elems following the header + auto hlist = getHashlist(HashList::FetchID(header)); + kvdk_assert(hlist != nullptr, "Backup hlist should exist in map"); + auto hlist_iter = HashIteratorImpl( + hlist.get(), static_cast(snapshot), + false); + for (hlist_iter.SeekToFirst(); hlist_iter.Valid(); + hlist_iter.Next()) { + s = backup.Append(RecordType::HashElem, hlist_iter.Key(), + hlist_iter.Value(), kPersistTime); + if (s != Status::Ok) { + break; + } + } + } + } + break; + } + case RecordType::ListHeader: { + DLRecord* header = slot_iter->GetIndex().list->HeaderRecord(); + while (header != nullptr && header->GetTimestamp() > backup_ts) { + header = kv_allocator_->offset2addr(header->old_version); + } + if (header && header->GetRecordStatus() == RecordStatus::Normal && + !header->HasExpired()) { + s = backup.Append(RecordType::ListHeader, header->Key(), + header->Value(), header->GetExpireTime()); + if (s == Status::Ok) { + // Append hlist elems following the header + auto list = getList(List::FetchID(header)); + kvdk_assert(list != nullptr, "Backup list should exist in map"); + auto list_iter = ListIteratorImpl( + list.get(), static_cast(snapshot), + false); + for (list_iter.SeekToFirst(); list_iter.Valid(); + list_iter.Next()) { + s = backup.Append(RecordType::ListElem, "", list_iter.Value(), + kPersistTime); + if (s != Status::Ok) { + break; + } + } + } + } + break; + } + default: + // Hash and list backup is not supported yet + break; + } + if (s != Status::Ok) { + backup.Destroy(); + return s; + } + slot_iter++; + } + hashtable_iterator.Next(); + } + backup.Finish(); + GlobalLogger.Info("Backup instance to %s Finished\n", + backup_log_file.c_str()); + return Status::Ok; +} + +Status KVEngine::Restore(const StringView engine_path, + const StringView backup_log, Engine** engine_ptr, + const Configs& configs) { + std::string engine_path_str(string_view_2_string(engine_path)); + std::string backup_log_str(string_view_2_string(backup_log)); + GlobalLogger.Info( + "Restoring kvdk instance from backup log %s to engine path %s\n", + backup_log_str.c_str(), engine_path_str.c_str()); + KVEngine* engine = new KVEngine(configs); + Status s = engine->init(engine_path_str, configs); + if (s == Status::Ok) { + s = engine->restoreDataFromBackup(backup_log_str); + } + + if (s == Status::Ok) { + *engine_ptr = engine; + engine->startBackgroundWorks(); + engine->ReportMemoryUsage(); + } else { + GlobalLogger.Error("Restore kvdk instance from backup log %s failed: %d\n", + backup_log_str.c_str(), s); + delete engine; + } + return s; +} + +Status KVEngine::restoreDataFromBackup(const std::string& backup_log) { + // TODO: make this multi-thread + BackupLog backup; + Status s = backup.Open(backup_log); + if (s != Status::Ok) { + return s; + } + auto wo = WriteOptions(); + auto iter = backup.GetIterator(); + if (iter == nullptr) { + GlobalLogger.Error("Restore from a not finished backup log %s\n", + backup_log.c_str()); + return Status::Abort; + } + uint64_t cnt = 0; + GlobalLogger.Info("Start iterating backup log\n"); + while (iter->Valid()) { + auto record = iter->Record(); + bool expired = TimeUtils::CheckIsExpired(record.expire_time); + wo.ttl_time = record.expire_time; + switch (record.type) { + case RecordType::String: { + if (!expired) { + cnt++; + s = Put(record.key, record.val, wo); + } + iter->Next(); + break; + } + case RecordType::SortedHeader: { + // Maybe reuse id? + std::shared_ptr skiplist = nullptr; + if (!expired) { + CollectionIDType id; + SortedCollectionConfigs s_configs; + s = Skiplist::DecodeSortedCollectionValue(record.val, id, s_configs); + if (s != Status::Ok) { + break; + } + s = buildSkiplist(record.key, s_configs, skiplist); + if (s == Status::Ok && wo.ttl_time != kPersistTime) { + skiplist->SetExpireTime(wo.ttl_time, + version_controller_.GetCurrentTimestamp()); + } + if (s != Status::Ok) { + break; + } + cnt++; + } + iter->Next(); + // the header is followed by all its elems in backup log + while (iter->Valid()) { + record = iter->Record(); + if (record.type != RecordType::SortedElem) { + break; + } + if (!expired) { + auto ret = skiplist->Put(record.key, record.val, + version_controller_.GetCurrentTimestamp()); + s = ret.s; + if (s != Status::Ok) { + break; + } + cnt++; + } + iter->Next(); + } + break; + } + case RecordType::HashHeader: { + std::shared_ptr hlist = nullptr; + if (!expired) { + s = buildHashlist(record.key, hlist); + if (s == Status::Ok && wo.ttl_time != kPersistTime) { + hlist->SetExpireTime(wo.ttl_time, + version_controller_.GetCurrentTimestamp()); + } + if (s != Status::Ok) { + break; + } + cnt++; + } + iter->Next(); + // the header is followed by all its elems in backup log + while (iter->Valid()) { + record = iter->Record(); + if (record.type != RecordType::HashElem) { + break; + } + if (!expired) { + auto ret = hlist->Put(record.key, record.val, + version_controller_.GetCurrentTimestamp()); + s = ret.s; + if (s != Status::Ok) { + break; + } + cnt++; + } + iter->Next(); + } + break; + } + case RecordType::ListHeader: { + std::shared_ptr list = nullptr; + if (!expired) { + s = buildList(record.key, list); + if (s == Status::Ok && wo.ttl_time != kPersistTime) { + list->SetExpireTime(wo.ttl_time, + version_controller_.GetCurrentTimestamp()); + } + if (s != Status::Ok) { + break; + } + cnt++; + } + iter->Next(); + // the header is followed by all its elems in backup log + while (iter->Valid()) { + record = iter->Record(); + if (record.type != RecordType::ListElem) { + break; + } + if (!expired) { + auto ret = list->PushBack( + record.val, version_controller_.GetCurrentTimestamp()); + s = ret.s; + if (s != Status::Ok) { + break; + } + cnt++; + } + iter->Next(); + } + break; + } + case RecordType::SortedElem: { + GlobalLogger.Error("sorted elems not lead by header in backup log %s\n", + backup_log.c_str()); + return Status::Abort; + } + case RecordType::HashElem: { + GlobalLogger.Error("hash elems not lead by header in backup log %s\n", + backup_log.c_str()); + return Status::Abort; + } + case RecordType::ListElem: { + GlobalLogger.Error("list elems not lead by header in backup log %s\n", + backup_log.c_str()); + return Status::Abort; + } + default: + GlobalLogger.Error("unsupported record type %u in backup log %s\n", + record.type, backup_log.c_str()); + return Status::Abort; + } + if (s != Status::Ok) { + return Status::Abort; + } + } + GlobalLogger.Info("Restore %lu records from backup log\n", cnt); + return Status::Ok; +} + +Status KVEngine::checkGeneralConfigs(const Configs& configs) { + auto is_2pown = [](uint64_t n) { return (n > 0) && (n & (n - 1)) == 0; }; + + if (!is_2pown(configs.hash_bucket_num) || + !is_2pown(configs.num_buckets_per_slot)) { + GlobalLogger.Error( + "hash_bucket_num and num_buckets_per_slot should be 2^n\n"); + return Status::InvalidConfiguration; + } + + if (configs.hash_bucket_num >= ((uint64_t)1 << 32)) { + GlobalLogger.Error("too many hash buckets\n"); + return Status::InvalidConfiguration; + } + + if (configs.num_buckets_per_slot > configs.hash_bucket_num) { + GlobalLogger.Error( + "num_buckets_per_slot should less than hash_bucket_num\n"); + return Status::InvalidConfiguration; + } + + return Status::Ok; +} + +Status KVEngine::BatchWrite(std::unique_ptr const& batch) { + WriteBatchImpl const* batch_impl = + dynamic_cast(batch.get()); + if (batch_impl == nullptr) { + return Status::InvalidArgument; + } + + return batchWriteImpl(*batch_impl); +} + +Status KVEngine::maybeInitBatchLogFile() { + // TODO Implement batch write without pmem + return Status::Ok; +} + +Status KVEngine::batchWriteImpl(WriteBatchImpl const& batch) { + if (batch.Size() > BatchWriteLog::Capacity()) { + return Status::InvalidBatchSize; + } + + auto thread_holder = AcquireAccessThread(); + + Status s = maybeInitBatchLogFile(); + if (s != Status::Ok) { + return s; + } + + // Prevent collection and nodes in double linked lists from being deleted + auto access_token = version_controller_.GetLocalSnapshotHolder(); + + std::vector string_args; + string_args.reserve(batch.StringOps().size()); + std::vector sorted_args; + sorted_args.reserve(batch.SortedOps().size()); + std::vector hash_args; + hash_args.reserve(batch.HashOps().size()); + + for (auto const& string_op : batch.StringOps()) { + string_args.emplace_back(); + string_args.back().Assign(string_op); + } + + // Lookup Skiplists and Hashes for further operations + for (auto const& sorted_op : batch.SortedOps()) { + auto res = lookupKey(sorted_op.collection, RecordType::SortedHeader); + /// TODO: this is a temporary work-around + /// We cannot lock both key and field, which may trigger deadlock. + /// However, if a collection is created and a field is inserted, + /// Delete operation in batch may skip this field, + /// which causes inconsistency. + if (res.s == Status::Outdated) { + return Status::NotFound; + } + if (res.s != Status::Ok) { + return res.s; + } + Skiplist* skiplist = res.entry.GetIndex().skiplist; + sorted_args.emplace_back( + skiplist->InitWriteArgs(sorted_op.key, sorted_op.value, sorted_op.op)); + } + + for (auto const& hash_op : batch.HashOps()) { + HashList* hlist; + Status s = hashListFind(hash_op.collection, &hlist); + if (s != Status::Ok) { + return s; + } + hash_args.emplace_back( + hlist->InitWriteArgs(hash_op.key, hash_op.value, hash_op.op)); + } + + // Keys/internal keys to be locked on HashTable + std::vector keys_to_lock; + for (auto const& string_op : batch.StringOps()) { + keys_to_lock.push_back(string_op.key); + } + for (auto const& arg : sorted_args) { + keys_to_lock.push_back(arg.skiplist->InternalKey(arg.key)); + } + for (auto const& arg : hash_args) { + keys_to_lock.push_back(arg.hlist->InternalKey(arg.key)); + } + + auto guard = hash_table_->RangeLock(keys_to_lock); + keys_to_lock.clear(); + + // Lookup keys, allocate space according to result. + auto ReleaseResources = [&]() { + // Don't Free() if we simulate a crash. +#ifndef KVDK_ENABLE_CRASHPOINT + for (auto iter = hash_args.rbegin(); iter != hash_args.rend(); ++iter) { + kv_allocator_->Free(iter->space); + if (iter->lookup_result.entry_ptr->Allocated()) { + kvdk_assert(iter->lookup_result.s == Status::NotFound, ""); + iter->lookup_result.entry_ptr->Clear(); + } + } + for (auto iter = sorted_args.rbegin(); iter != sorted_args.rend(); ++iter) { + kv_allocator_->Free(iter->space); + if (iter->lookup_result.entry_ptr->Allocated()) { + kvdk_assert(iter->lookup_result.s == Status::NotFound, ""); + iter->lookup_result.entry_ptr->Clear(); + } + } + for (auto iter = string_args.rbegin(); iter != string_args.rend(); ++iter) { + kv_allocator_->Free(iter->space); + if (iter->res.entry_ptr->Allocated()) { + kvdk_assert(iter->res.s == Status::NotFound, ""); + iter->res.entry_ptr->Clear(); + } + } +#endif + }; + + defer(ReleaseResources()); + + // Prevent generating snapshot newer than this WriteBatch + auto bw_token = version_controller_.GetBatchWriteToken(); + + // Prepare for Strings + for (auto& args : string_args) { + Status s = stringWritePrepare(args, bw_token.Timestamp()); + if (s != Status::Ok) { + return s; + } + } + + // Prepare for Sorted Elements + for (auto& args : sorted_args) { + Status s = sortedWritePrepare(args, bw_token.Timestamp()); + if (s != Status::Ok) { + return s; + } + } + + // Prepare for Hash Elements + for (auto& args : hash_args) { + Status s = hashWritePrepare(args, bw_token.Timestamp()); + if (s != Status::Ok) { + return s; + } + } + + // Preparation done. Persist BatchLog for rollback. + BatchWriteLog log; + log.SetTimestamp(bw_token.Timestamp()); + auto& tc = engine_thread_cache_[ThreadManager::ThreadID() % + configs_.max_access_threads]; + for (auto& args : string_args) { + if (args.space.size == 0) { + continue; + } + if (args.op == WriteOp::Put) { + log.StringPut(args.space.offset); + } else { + log.StringDelete(args.space.offset); + } + } + for (auto& args : sorted_args) { + if (args.space.size == 0) { + continue; + } + if (args.op == WriteOp::Put) { + log.SortedPut(args.space.offset); + } else { + log.SortedDelete(args.space.offset); + } + } + + for (auto& args : hash_args) { + if (args.space.size == 0) { + continue; + } + if (args.op == WriteOp::Put) { + log.HashPut(args.space.offset); + } else { + log.HashDelete(args.space.offset); + } + } + + log.EncodeTo(tc.batch_log); + + BatchWriteLog::MarkProcessing(tc.batch_log); + + // After preparation stage, no runtime error is allowed for now, + // otherwise we have to perform runtime rollback. + + // Write Strings + for (auto& args : string_args) { + if (args.space.size == 0) { + continue; + } + Status s = stringWrite(args); + kvdk_assert(s == Status::Ok, ""); + } + + // Write Sorted Elems + for (auto& args : sorted_args) { + if (args.space.size == 0) { + continue; + } + Status s = sortedWrite(args); + kvdk_assert(s == Status::Ok, ""); + } + + // Write Hash Elems + for (auto& args : hash_args) { + if (args.space.size == 0) { + continue; + } + Status s = hashListWrite(args); + kvdk_assert(s == Status::Ok, ""); + } + + TEST_CRASH_POINT("KVEngine::batchWriteImpl::BeforeCommit", ""); + + BatchWriteLog::MarkCommitted(tc.batch_log); + + // Publish stages is where Strings and Collections make BatchWrite + // visible to other threads. + // This stage allows no failure during runtime, + // otherwise dirty read may occur. + // Crash is tolerated as BatchWrite will be recovered. + + // Publish Strings to HashTable + for (auto const& args : string_args) { + if (args.space.size == 0) { + continue; + } + Status s = stringWritePublish(args); + kvdk_assert(s == Status::Ok, ""); + } + + // Publish Sorted Elements to HashTable + for (auto const& args : sorted_args) { + if (args.space.size == 0) { + continue; + } + Status s = sortedWritePublish(args); + kvdk_assert(s == Status::Ok, ""); + } + + // Publish Hash Elements to HashTable + for (auto& args : hash_args) { + if (args.space.size == 0) { + continue; + } + Status s = hashListPublish(args); + kvdk_assert(s == Status::Ok, ""); + } + + hash_args.clear(); + sorted_args.clear(); + string_args.clear(); + + return Status::Ok; +} + +Status KVEngine::GetTTL(const StringView key, TTLType* ttl_time) { + *ttl_time = kInvalidTTL; + auto ul = hash_table_->AcquireLock(key); + auto res = lookupKey(key, ExpirableRecordType); + + if (res.s == Status::Ok) { + ExpireTimeType expire_time; + switch (res.entry_ptr->GetIndexType()) { + case PointerType::Skiplist: { + expire_time = res.entry_ptr->GetIndex().skiplist->GetExpireTime(); + break; + } + case PointerType::List: { + expire_time = res.entry_ptr->GetIndex().list->GetExpireTime(); + break; + } + case PointerType::HashList: { + expire_time = res.entry_ptr->GetIndex().hlist->GetExpireTime(); + break; + } + case PointerType::StringRecord: { + expire_time = res.entry_ptr->GetIndex().string_record->GetExpireTime(); + break; + } + default: { + return Status::NotSupported; + } + } + // return ttl time + *ttl_time = TimeUtils::ExpireTimeToTTL(expire_time); + } + return res.s == Status::Outdated ? Status::NotFound : res.s; +} + +Status KVEngine::TypeOf(StringView key, ValueType* type) { + auto res = lookupKey(key, ExpirableRecordType); + + if (res.s == Status::Ok) { + switch (res.entry_ptr->GetIndexType()) { + case PointerType::Skiplist: { + *type = ValueType::SortedCollection; + break; + } + case PointerType::List: { + *type = ValueType::List; + break; + } + case PointerType::HashList: { + *type = ValueType::HashCollection; + break; + } + case PointerType::StringRecord: { + *type = ValueType::String; + break; + } + default: { + return Status::Abort; + } + } + } + return res.s == Status::Outdated ? Status::NotFound : res.s; +} + +Status KVEngine::Expire(const StringView key, TTLType ttl_time) { + auto thread_holder = AcquireAccessThread(); + + int64_t base_time = TimeUtils::millisecond_time(); + if (!TimeUtils::CheckTTL(ttl_time, base_time)) { + return Status::InvalidArgument; + } + + ExpireTimeType expired_time = TimeUtils::TTLToExpireTime(ttl_time, base_time); + auto ul = hash_table_->AcquireLock(key); + auto snapshot_holder = version_controller_.GetLocalSnapshotHolder(); + // TODO: maybe have a wrapper function(lookupKeyAndMayClean). + auto lookup_result = lookupKey(key, ExpirableRecordType); + if (lookup_result.s == Status::Outdated) { + return Status::NotFound; + } + + if (lookup_result.s == Status::Ok) { + WriteOptions write_option{ttl_time}; + switch (lookup_result.entry_ptr->GetIndexType()) { + case PointerType::StringRecord: { + ul.unlock(); + version_controller_.ReleaseLocalSnapshot(); + lookup_result.s = Modify( + key, + [](const std::string* old_val, std::string* new_val, void*) { + new_val->assign(*old_val); + return ModifyOperation::Write; + }, + nullptr, write_option); + break; + } + case PointerType::Skiplist: { + auto new_ts = snapshot_holder.Timestamp(); + Skiplist* skiplist = lookup_result.entry_ptr->GetIndex().skiplist; + std::unique_lock skiplist_lock(skiplists_mu_); + expirable_skiplists_.erase(skiplist); + auto ret = skiplist->SetExpireTime(expired_time, new_ts); + expirable_skiplists_.emplace(skiplist); + lookup_result.s = ret.s; + break; + } + case PointerType::HashList: { + auto new_ts = snapshot_holder.Timestamp(); + HashList* hlist = lookup_result.entry_ptr->GetIndex().hlist; + std::unique_lock hlist_lock(hlists_mu_); + expirable_hlists_.erase(hlist); + lookup_result.s = hlist->SetExpireTime(expired_time, new_ts).s; + expirable_hlists_.emplace(hlist); + break; + } + case PointerType::List: { + auto new_ts = snapshot_holder.Timestamp(); + List* list = lookup_result.entry_ptr->GetIndex().list; + lookup_result.s = list->SetExpireTime(expired_time, new_ts).s; + break; + } + default: { + return Status::NotSupported; + } + } + } + return lookup_result.s; +} +} // namespace KVDK_NAMESPACE + +// lookupKey +namespace KVDK_NAMESPACE { +template +HashTable::LookupResult KVEngine::lookupElem(StringView key, + uint8_t type_mask) { + kvdk_assert(type_mask & (RecordType::HashElem | RecordType::SortedElem), ""); + return hash_table_->Lookup(key, type_mask); +} + +template HashTable::LookupResult KVEngine::lookupElem(StringView, + uint8_t); +template HashTable::LookupResult KVEngine::lookupElem(StringView, + uint8_t); + +template +HashTable::LookupResult KVEngine::lookupKey(StringView key, uint8_t type_mask) { + auto result = hash_table_->Lookup(key, PrimaryRecordType); + + if (result.s != Status::Ok) { + kvdk_assert( + result.s == Status::NotFound || result.s == Status::MemoryOverflow, ""); + return result; + } + + RecordType type = result.entry.GetRecordType(); + RecordStatus record_status = result.entry.GetRecordStatus(); + bool type_match = type_mask & type; + + // TODO: fix mvcc of different type keys + if (!type_match) { + result.s = Status::WrongType; + } else if (record_status == RecordStatus::Outdated) { + result.s = Status::Outdated; + } else { + switch (type) { + case RecordType::String: { + result.s = result.entry.GetIndex().string_record->HasExpired() + ? Status::Outdated + : Status::Ok; + break; + } + case RecordType::SortedHeader: + result.s = result.entry.GetIndex().skiplist->HasExpired() + ? Status::Outdated + : Status::Ok; + + break; + case RecordType::ListHeader: + result.s = result.entry.GetIndex().list->HasExpired() ? Status::Outdated + : Status::Ok; + break; + case RecordType::HashHeader: { + result.s = result.entry.GetIndex().hlist->HasExpired() + ? Status::Outdated + : Status::Ok; + break; + } + default: { + kvdk_assert(false, "Unreachable branch!"); + std::abort(); + } + } + } + return result; +} + +template HashTable::LookupResult KVEngine::lookupKey(StringView, uint8_t); +template HashTable::LookupResult KVEngine::lookupKey(StringView, + uint8_t); + +template +T* KVEngine::removeOutDatedVersion(T* record, TimestampType min_snapshot_ts) { + static_assert( + std::is_same::value || std::is_same::value, + "Invalid record type, should be StringRecord or DLRecord."); + T* ret = nullptr; + auto old_record = record; + while (old_record && old_record->GetTimestamp() > min_snapshot_ts) { + old_record = + static_cast(kv_allocator_->offset2addr(old_record->old_version)); + } + + // the snapshot should access the old record, so we need to purge and free the + // older version of the old record + if (old_record && old_record->old_version != kNullMemoryOffset) { + T* remove_record = + kv_allocator_->offset2addr_checked(old_record->old_version); + ret = remove_record; + old_record->SetOldVersion(kNullMemoryOffset); + while (remove_record != nullptr) { + if (remove_record->GetRecordStatus() == RecordStatus::Normal) { + remove_record->SetStatus(RecordStatus::Dirty); + } + remove_record = kv_allocator_->offset2addr(remove_record->old_version); + } + } + return ret; +} + +template StringRecord* KVEngine::removeOutDatedVersion( + StringRecord*, TimestampType); +template DLRecord* KVEngine::removeOutDatedVersion(DLRecord*, + TimestampType); + +} // namespace KVDK_NAMESPACE + +// Snapshot, delayFree and background work +namespace KVDK_NAMESPACE { + +/// TODO: move this into VersionController. +Snapshot* KVEngine::GetSnapshot(bool make_checkpoint) { + Snapshot* ret = version_controller_.NewGlobalSnapshot(); + return ret; +} + +void KVEngine::backgroundMemoryUsageReporter() { + auto interval = std::chrono::milliseconds{ + static_cast(configs_.report_memory_usage_interval * 1000)}; + while (!bg_work_signals_.terminating) { + { + std::unique_lock ul(bg_work_signals_.terminating_lock); + if (!bg_work_signals_.terminating) { + bg_work_signals_.memory_usage_reporter_cv.wait_for(ul, interval); + } + } + ReportMemoryUsage(); + GlobalLogger.Info("Cleaner Thread Num: %ld\n", cleaner_.ActiveThreadNum()); + } +} + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/kv_engine.hpp b/volatile/engine/kv_engine.hpp new file mode 100644 index 00000000..0fd7008e --- /dev/null +++ b/volatile/engine/kv_engine.hpp @@ -0,0 +1,609 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alias.hpp" +#include "data_record.hpp" +#include "dram_allocator.hpp" +#include "hash_collection/hash_list.hpp" +#include "hash_table.hpp" +#include "kvdk/volatile/engine.hpp" +#include "list_collection/list.hpp" +#include "lock_table.hpp" +#include "logger.hpp" +#include "sorted_collection/skiplist.hpp" +#include "structures.hpp" +#include "thread_manager.hpp" +#include "utils/utils.hpp" +#include "version/old_records_cleaner.hpp" +#include "version/version_controller.hpp" +#include "write_batch_impl.hpp" + +namespace KVDK_NAMESPACE { +class KVEngine : public Engine { + public: + ~KVEngine(); + + static Status Open(const StringView engine_path, Engine** engine_ptr, + const Configs& configs); + + static Status Restore(const StringView engine_path, + const StringView backup_log, Engine** engine_ptr, + const Configs& configs); + + Snapshot* GetSnapshot(bool make_checkpoint) override; + + Status Backup(const pmem::obj::string_view backup_log, + const Snapshot* snapshot) override; + + void ReleaseSnapshot(const Snapshot* snapshot) override { + version_controller_.ReleaseSnapshot( + static_cast(snapshot)); + } + void ReportMemoryUsage(); + + // Expire str after ttl_time + // + // Notice: + // 1. Expire assumes that str is not duplicated among all types, which is not + // implemented yet + // 2. Expire is not compatible with checkpoint for now + Status Expire(const StringView key, TTLType ttl_time) final; + + // Get time to expire of str + // + // Notice: + // Expire assumes that str is not duplicated among all types, which is not + // implemented yet + Status GetTTL(const StringView key, TTLType* ttl_time) final; + + Status TypeOf(StringView key, ValueType* type) final; + + // String + Status Get(const StringView key, std::string* value) override; + Status Put(const StringView key, const StringView value, + const WriteOptions& write_options) override; + Status Delete(const StringView key) override; + Status Modify(const StringView key, ModifyFunc modify_func, void* modify_args, + const WriteOptions& options) override; + + // Sorted + Status SortedCreate(const StringView collection_name, + const SortedCollectionConfigs& configs) override; + Status SortedDestroy(const StringView collection_name) override; + Status SortedSize(const StringView collection, size_t* size) override; + Status SortedGet(const StringView collection, const StringView user_key, + std::string* value) override; + Status SortedPut(const StringView collection, const StringView user_key, + const StringView value) override; + Status SortedDelete(const StringView collection, + const StringView user_key) override; + SortedIterator* SortedIteratorCreate(const StringView collection, + Snapshot* snapshot, Status* s) override; + void SortedIteratorRelease(SortedIterator* sorted_iterator) override; + + // List + Status ListCreate(StringView key) final; + Status ListDestroy(StringView key) final; + Status ListSize(StringView key, size_t* sz) final; + Status ListPushFront(StringView key, StringView elem) final; + Status ListPushBack(StringView key, StringView elem) final; + Status ListPopFront(StringView key, std::string* elem) final; + Status ListPopBack(StringView key, std::string* elem) final; + Status ListBatchPushFront(StringView key, + std::vector const& elems) final; + Status ListBatchPushFront(StringView key, + std::vector const& elems) final; + Status ListBatchPushBack(StringView key, + std::vector const& elems) final; + Status ListBatchPushBack(StringView key, + std::vector const& elems) final; + Status ListBatchPopFront(StringView key, size_t n, + std::vector* elems) final; + Status ListBatchPopBack(StringView key, size_t n, + std::vector* elems) final; + Status ListMove(StringView src, ListPos src_pos, StringView dst, + ListPos dst_pos, std::string* elem) final; + Status ListInsertAt(StringView collection, StringView key, long index) final; + Status ListInsertBefore(StringView collection, StringView key, + StringView pos) final; + Status ListInsertAfter(StringView collection, StringView key, + StringView pos) final; + Status ListErase(StringView collection, long index, std::string* elem) final; + + Status ListReplace(StringView list_name, long index, StringView elem) final; + ListIterator* ListIteratorCreate(StringView collection, Snapshot* snapshot, + Status* status) final; + void ListIteratorRelease(ListIterator* iter) final; + + // Hash + Status HashCreate(StringView key) final; + Status HashDestroy(StringView key) final; + Status HashSize(StringView key, size_t* len) final; + Status HashGet(StringView key, StringView field, std::string* value) final; + Status HashPut(StringView key, StringView field, StringView value) final; + Status HashDelete(StringView key, StringView field) final; + Status HashModify(StringView key, StringView field, ModifyFunc modify_func, + void* cb_args) final; + HashIterator* HashIteratorCreate(StringView key, Snapshot* snapshot, + Status* s) final; + void HashIteratorRelease(HashIterator*) final; + + // BatchWrite + // It takes 3 stages + // Stage 1: Preparation + // BatchWrite() sort the keys and remove duplicants, + // lock the keys/fields in HashTable, + // and allocate spaces and persist BatchWriteLog + // Stage 2: Execution + // Batches are dispatched to different data types + // Each data type update keys/fields + // Outdated records are not purged in this stage. + // Stage 3: Publish + // Each data type commits its batch, clean up outdated data. + Status BatchWrite(std::unique_ptr const& batch) final; + std::unique_ptr WriteBatchCreate() final { + return std::unique_ptr{new WriteBatchImpl{}}; + } + + // For test cases + const std::unordered_map>& + GetSkiplists() { + return skiplists_; + }; + Cleaner* EngineCleaner() { return &cleaner_; } + HashTable* GetHashTable() { return hash_table_.get(); } + void TestCleanOutDated(size_t start_slot_idx, size_t end_slot_idx); + + private: + friend OldRecordsCleaner; + friend Cleaner; + + KVEngine(const Configs& configs) + : access_thread_cv_(configs.max_access_threads), + engine_thread_cache_(configs.max_access_threads), + cleaner_thread_cache_(configs.max_access_threads), + version_controller_(configs.max_access_threads), + old_records_cleaner_(this, configs.max_access_threads), + cleaner_(this, configs.clean_threads), + comparators_(configs.comparator){}; + + struct EngineThreadCache { + EngineThreadCache() = default; + + char* batch_log = nullptr; + + // Info used in recovery + uint64_t newest_restored_ts = 0; + std::unordered_map visited_skiplist_ids{}; + }; + + struct CleanerThreadCache { + template + struct OutdatedRecord { + OutdatedRecord(T* _record, TimestampType _release_time) + : record(_record), release_time(_release_time) {} + + T* record; + TimestampType release_time; + }; + + CleanerThreadCache() = default; + std::deque> outdated_string_records; + std::deque> outdated_dl_records; + SpinMutex mtx; + }; + + struct AccessThreadCV { + public: + struct Holder { + public: + Holder(AccessThreadCV* cv) : cv_(cv) { cv->Acquire(); } + ~Holder() { cv_->Release(); } + + private: + AccessThreadCV* cv_; + }; + + private: + void Acquire() { + std::unique_lock ul(spin_); + while (holder_id_ != ThreadManager::ThreadID() && holder_id_ != -1) { + cv_.wait(ul); + } + holder_id_ = ThreadManager::ThreadID(); + } + + void Release() { + std::unique_lock ul(spin_); + holder_id_ = -1; + cv_.notify_one(); + } + + int64_t holder_id_ = -1; + SpinMutex spin_; + std::condition_variable_any cv_; + }; + + AccessThreadCV::Holder AcquireAccessThread() { + return AccessThreadCV::Holder(&access_thread_cv_[ThreadManager::ThreadID() % + access_thread_cv_.size()]); + } + + bool checkKeySize(const StringView& key) { return key.size() <= UINT16_MAX; } + + bool checkValueSize(const StringView& value) { + return value.size() <= UINT32_MAX; + } + + // Init basic components of the engine + Status init(const std::string& name, const Configs& configs); + + Status hashGetImpl(const StringView& key, std::string* value, + uint16_t type_mask); + + bool registerComparator(const StringView& collection_name, + Comparator comp_func) { + return comparators_.RegisterComparator(collection_name, comp_func); + } + + // Look up a first level key in hash table(e.g. collections or string, not + // collection elems), the first level key should be unique among all types + // + // Store a copy of hash entry in LookupResult::entry, and a pointer to the + // hash entry in LookupResult::entry_ptr + // If may_insert is true and key not found, then store + // pointer of a free-to-write hash entry in LookupResult::entry_ptr. + // + // return status: + // Status::Ok if key exist and alive + // Status::NotFound is key is not found. + // Status::WrongType if type_mask does not match. + // Status::Outdated if key has been expired or deleted + // Status::MemoryOverflow if may_insert is true but failed to allocate new + // hash entry + // + // Notice: key should be locked if set may_insert to true + template + HashTable::LookupResult lookupKey(StringView key, uint8_t type_mask); + + // Look up a collection element in hash table + // + // Store a copy of hash entry in LookupResult::entry, and a pointer to the + // hash entry in LookupResult::entry_ptr + // If may_insert is true and key not found, then store + // pointer of a free-to-write hash entry in LookupResult::entry_ptr. + // + // return status: + // Status::Ok if key exist + // Status::NotFound is key is not found. + // Status::MemoryOverflow if may_insert is true but failed to allocate new + // hash entry + // + // Notice: elem should be locked if set may_insert to true + template + HashTable::LookupResult lookupElem(StringView key, uint8_t type_mask); + + // Remove a key or elem from hash table, ret should be return of + // lookupKey/lookupElem + void removeKeyOrElem(HashTable::LookupResult ret) { + kvdk_assert(ret.s == Status::Ok || ret.s == Status::Outdated, ""); + hash_table_->Erase(ret.entry_ptr); + } + + // insert/update key or elem to hashtable, ret must be return value of + // lookupElem or lookupKey + void insertKeyOrElem(HashTable::LookupResult ret, RecordType type, + RecordStatus status, void* addr) { + hash_table_->Insert(ret, type, status, addr, pointerType(type)); + } + + template + static constexpr RecordType collectionType() { + static_assert(std::is_same::value || + std::is_same::value || + std::is_same::value, + "Invalid type!"); + return std::is_same::value + ? RecordType::SortedHeader + : std::is_same::value + ? RecordType::ListHeader + : std::is_same::value + ? RecordType::HashHeader + : RecordType::Empty; + } + + static PointerType pointerType(RecordType rtype) { + switch (rtype) { + case RecordType::Empty: { + return PointerType::Empty; + } + case RecordType::String: { + return PointerType::StringRecord; + } + case RecordType::SortedElem: { + kvdk_assert(false, "Not supported!"); + return PointerType::Invalid; + } + case RecordType::SortedHeader: { + return PointerType::Skiplist; + } + case RecordType::ListHeader: { + return PointerType::List; + } + case RecordType::HashHeader: { + return PointerType::HashList; + } + case RecordType::HashElem: { + return PointerType::HashElem; + } + case RecordType::ListElem: + default: { + kvdk_assert(false, "Invalid type!"); + return PointerType::Invalid; + } + } + } + + // Lockless. It's up to caller to lock the HashTable + template + Status registerCollection(CollectionType* coll) { + auto type = collectionType(); + auto ret = lookupKey(coll->Name(), type); + if (ret.s == Status::Ok) { + kvdk_assert(false, "Collection already registered!"); + return Status::Abort; + } + if (ret.s != Status::NotFound && ret.s != Status::Outdated) { + return ret.s; + } + insertKeyOrElem(ret, type, RecordStatus::Normal, coll); + return Status::Ok; + } + + Status maybeInitBatchLogFile(); + + Status stringPutImpl(const StringView& key, const StringView& value, + const WriteOptions& write_options); + + Status stringDeleteImpl(const StringView& key); + + Status stringWritePrepare(StringWriteArgs& args, TimestampType ts); + Status stringWrite(StringWriteArgs& args); + Status stringWritePublish(StringWriteArgs const& args); + Status stringRollback(TimestampType ts, + BatchWriteLog::StringLogEntry const& entry); + + Status sortedPutImpl(Skiplist* skiplist, const StringView& collection_key, + const StringView& value); + + Status sortedDeleteImpl(Skiplist* skiplist, const StringView& user_key); + + Status restoreDataFromBackup(const std::string& backup_log); + Status sortedWritePrepare(SortedWriteArgs& args, TimestampType ts); + Status sortedWrite(SortedWriteArgs& args); + Status sortedWritePublish(SortedWriteArgs const& args); + + Status batchWriteImpl(WriteBatchImpl const& batch); + + /// List helper functions + // Find and lock the list. Initialize non-existing if required. + // Guarantees always return a valid List and lockes it if returns Status::Ok + Status listFind(StringView key, List** list); + + Status listBatchPushImpl(StringView key, ListPos pos, + std::vector const& elems); + Status listBatchPopImpl(StringView list_name, ListPos pos, size_t n, + std::vector* elems); + + /// Hash helper funtions + Status hashListFind(StringView key, HashList** hlist); + + Status hashListWrite(HashWriteArgs& args); + Status hashWritePrepare(HashWriteArgs& args, TimestampType ts); + Status hashListPublish(HashWriteArgs const& args); + + /// Other + Status checkGeneralConfigs(const Configs& configs); + + void purgeAndFreeStringRecords(const std::vector& old_offset); + + void purgeAndFreeDLRecords(const std::vector& old_offset); + + // remove outdated records which without snapshot hold. + template + T* removeOutDatedVersion(T* record, TimestampType min_snapshot_ts); + + template + void removeOutdatedCollection(T* collection); + + // find delete and old records in skiplist with no hash index + void cleanNoHashIndexedSkiplist(Skiplist* skiplist, + std::vector& purge_dl_records); + + void cleanList(List* list, std::vector& purge_dl_records); + + double cleanOutDated(PendingCleanRecords& pending_clean_records, + size_t start_slot_idx, size_t slot_block_size); + + void purgeAndFree(PendingCleanRecords& pending_clean_records); + + TimestampType getTimestamp() { + return version_controller_.GetCurrentTimestamp(); + } + + void removeSkiplist(CollectionIDType id) { + std::lock_guard lg(skiplists_mu_); + skiplists_.erase(id); + } + + void addSkiplistToMap(std::shared_ptr skiplist) { + std::lock_guard lg(skiplists_mu_); + skiplists_.emplace(skiplist->ID(), skiplist); + } + + std::shared_ptr getSkiplist(CollectionIDType id) { + std::lock_guard lg(skiplists_mu_); + return skiplists_[id]; + } + + void removeHashlist(CollectionIDType id) { + std::lock_guard lg(hlists_mu_); + hlists_.erase(id); + } + + void addHashlistToMap(std::shared_ptr hlist) { + std::lock_guard lg(hlists_mu_); + hlists_.emplace(hlist->ID(), hlist); + } + + std::shared_ptr getHashlist(CollectionIDType id) { + std::lock_guard lg(hlists_mu_); + return hlists_[id]; + } + + void removeList(CollectionIDType id) { + std::lock_guard lg(lists_mu_); + lists_.erase(id); + } + + void addListToMap(std::shared_ptr list) { + std::lock_guard lg(hlists_mu_); + lists_.emplace(list->ID(), list); + } + + std::shared_ptr getList(CollectionIDType id) { + std::lock_guard lg(lists_mu_); + return lists_[id]; + } + + Status buildSkiplist(const StringView& name, + const SortedCollectionConfigs& s_configs, + std::shared_ptr& skiplist); + + Status buildHashlist(const StringView& name, + std::shared_ptr& hlist); + + Status buildList(const StringView& name, std::shared_ptr& list); + + inline std::string data_file() { return data_file(dir_); } + + inline static std::string data_file(const std::string& instance_path) { + return format_dir_path(instance_path) + "data"; + } + + inline std::string checkpoint_file() { return checkpoint_file(dir_); } + + inline static std::string checkpoint_file(const std::string& instance_path) { + return format_dir_path(instance_path) + "checkpoint"; + } + + inline std::string config_file() { return config_file(dir_); } + + inline static std::string config_file(const std::string& instance_path) { + return format_dir_path(instance_path) + "configs"; + } + + // Run in background to report DRAM/PMem usage regularly + void backgroundMemoryUsageReporter(); + + /* functions for cleaner thread cache */ + // Remove old version records from version chain of new_record and cache it + template + void removeAndCacheOutdatedVersion(T* new_record); + // Clean a outdated record in cleaner_thread_cache_ + void tryCleanCachedOutdatedRecord(); + template + void cleanOutdatedRecordImpl(T* record); + // Cleaner thread fetches cached outdated records + void fetchCachedOutdatedVersion( + PendingCleanRecords& pending_clean_records, + std::vector& purge_string_records, + std::vector& purge_dl_records); + /* functions for cleaner thread cache */ + + void startBackgroundWorks(); + + void terminateBackgroundWorks(); + + std::unique_ptr kv_allocator_; + std::unique_ptr skiplist_node_allocator_; + std::unique_ptr hashtable_new_bucket_allocator_; + + Array access_thread_cv_; + Array engine_thread_cache_; + Array cleaner_thread_cache_; + + // restored kvs in reopen + std::atomic restored_{0}; + std::atomic collection_id_{0}; + + std::unique_ptr hash_table_; + + std::mutex skiplists_mu_; + std::unordered_map> skiplists_; + std::set expirable_skiplists_; + + std::mutex lists_mu_; + std::unordered_map> lists_; + std::set expirable_lists_; + + std::mutex hlists_mu_; + std::unordered_map> hlists_; + std::set expirable_hlists_; + + std::unique_ptr dllist_locks_; + + std::string dir_; + std::string batch_log_dir_; + std::string data_file_; + + Configs configs_; + bool closing_{false}; + std::vector bg_threads_; + + VersionController version_controller_; + OldRecordsCleaner old_records_cleaner_; + Cleaner cleaner_; + + ComparatorTable comparators_; + + struct BackgroundWorkSignals { + BackgroundWorkSignals() = default; + BackgroundWorkSignals(const BackgroundWorkSignals&) = delete; + + std::condition_variable_any memory_usage_reporter_cv; + std::condition_variable_any dram_cleaner_cv; + + SpinMutex terminating_lock; + bool terminating = false; + }; + + BackgroundWorkSignals bg_work_signals_; + + std::atomic round_robin_id_{0}; + + // We manually allocate recovery thread id for no conflict in multi-thread + // recovering + // Todo: do not hard code + std::atomic next_recovery_tid_{0}; +}; + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/kv_engine_cleaner.cpp b/volatile/engine/kv_engine_cleaner.cpp new file mode 100644 index 00000000..74416475 --- /dev/null +++ b/volatile/engine/kv_engine_cleaner.cpp @@ -0,0 +1,866 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ +#include + +#include "kv_engine.hpp" +#include "utils/sync_point.hpp" + +namespace KVDK_NAMESPACE { + +constexpr uint64_t kForegroundUpdateSnapshotInterval = 1000; + +template +void KVEngine::removeOutdatedCollection(T* collection) { + static_assert(std::is_same::value || + std::is_same::value || + std::is_same::value); + auto cur_id = collection->ID(); + auto cur_head_record = collection->HeaderRecord(); + while (cur_head_record) { + auto old_head_record = + kv_allocator_->offset2addr(cur_head_record->old_version); + if (old_head_record) { + auto old_collection_id = T::FetchID(old_head_record); + if (old_collection_id != cur_id) { + if (getSkiplist(old_collection_id) == nullptr && + getHashlist(old_collection_id) == nullptr && + getList(old_collection_id) == nullptr) { + GlobalLogger.Debug("Collection already been destroyed\n"); + } + kvdk_assert(getSkiplist(old_collection_id) != nullptr || + getHashlist(old_collection_id) != nullptr || + getList(old_collection_id) != nullptr, + "collection should not be destroyed yet!"); + cur_head_record->SetOldVersion(kNullMemoryOffset); + cur_id = old_collection_id; + } + } + cur_head_record = old_head_record; + } +} + +template +void KVEngine::removeAndCacheOutdatedVersion(T* record) { + static_assert(std::is_same::value || + std::is_same::value); + kvdk_assert(ThreadManager::ThreadID() >= 0, ""); + auto& tc = cleaner_thread_cache_[ThreadManager::ThreadID() % + configs_.max_access_threads]; + if (std::is_same::value) { + StringRecord* old_record = removeOutDatedVersion( + (StringRecord*)record, version_controller_.GlobalOldestSnapshotTs()); + if (old_record) { + std::lock_guard lg(tc.mtx); + tc.outdated_string_records.emplace_back( + old_record, version_controller_.GetCurrentTimestamp()); + } + } else { + DLRecord* old_record = removeOutDatedVersion( + (DLRecord*)record, version_controller_.GlobalOldestSnapshotTs()); + if (old_record) { + std::lock_guard lg(tc.mtx); + tc.outdated_dl_records.emplace_back( + old_record, version_controller_.GetCurrentTimestamp()); + } + } +} + +template void KVEngine::removeAndCacheOutdatedVersion( + StringRecord* record); +template void KVEngine::removeAndCacheOutdatedVersion( + DLRecord* record); + +template +void KVEngine::cleanOutdatedRecordImpl(T* old_record) { + static_assert(std::is_same::value || + std::is_same::value); + while (old_record) { + T* next = kv_allocator_->offset2addr(old_record->old_version); + auto record_size = old_record->GetRecordSize(); + if (old_record->GetRecordStatus() == RecordStatus::Normal) { + old_record->Destroy(); + } + kv_allocator_->Free(SpaceEntry( + kv_allocator_->addr2offset_checked(old_record), record_size)); + old_record = next; + } +} + +void KVEngine::tryCleanCachedOutdatedRecord() { + kvdk_assert(ThreadManager::ThreadID() >= 0, ""); + auto& tc = cleaner_thread_cache_[ThreadManager::ThreadID() % + configs_.max_access_threads]; + // Regularly update local oldest snapshot + thread_local uint64_t round = 0; + if (++round % kForegroundUpdateSnapshotInterval == 0) { + version_controller_.UpdateLocalOldestSnapshot(); + } + auto release_time = version_controller_.LocalOldestSnapshotTS(); + if (!tc.outdated_string_records.empty()) { + std::unique_lock ul(tc.mtx); + if (!tc.outdated_string_records.empty() && + tc.outdated_string_records.front().release_time < release_time) { + auto to_clean = tc.outdated_string_records.front(); + tc.outdated_string_records.pop_front(); + ul.unlock(); + cleanOutdatedRecordImpl(to_clean.record); + return; + } + } + + if (!tc.outdated_dl_records.empty()) { + std::unique_lock ul(tc.mtx); + if (!tc.outdated_dl_records.empty() && + tc.outdated_dl_records.front().release_time < release_time) { + auto to_clean = tc.outdated_dl_records.front(); + tc.outdated_dl_records.pop_front(); + ul.unlock(); + cleanOutdatedRecordImpl(to_clean.record); + } + } +} + +void KVEngine::purgeAndFreeStringRecords( + const std::vector& old_records) { + std::vector entries; + for (auto old_record : old_records) { + while (old_record) { + StringRecord* next = + kv_allocator_->offset2addr(old_record->old_version); + if (old_record->GetRecordStatus() == RecordStatus::Normal) { + old_record->Destroy(); + } + entries.emplace_back(kv_allocator_->addr2offset(old_record), + old_record->GetRecordSize()); + old_record = next; + } + } + kv_allocator_->BatchFree(entries); +} + +void KVEngine::purgeAndFreeDLRecords( + const std::vector& old_records) { + std::vector entries; + std::vector outdated_skiplists; + for (auto data_record : old_records) { + while (data_record) { + DLRecord* next_record = + kv_allocator_->offset2addr(data_record->old_version); + RecordType type = data_record->GetRecordType(); + RecordStatus record_status = data_record->GetRecordStatus(); + switch (type) { + case RecordType::HashElem: + case RecordType::SortedElem: + case RecordType::ListElem: { + entries.emplace_back(kv_allocator_->addr2offset(data_record), + data_record->GetRecordSize()); + if (record_status == RecordStatus::Normal) { + data_record->Destroy(); + } + break; + } + case RecordType::SortedHeader: { + if (record_status != RecordStatus::Outdated && + !data_record->HasExpired()) { + entries.emplace_back( + kv_allocator_->addr2offset_checked(data_record), + data_record->GetRecordSize()); + data_record->Destroy(); + } else { + auto skiplist_id = Skiplist::FetchID(data_record); + kvdk_assert(skiplists_.find(skiplist_id) != skiplists_.end(), + "Skiplist should not be removed."); + auto head_record = getSkiplist(skiplist_id)->HeaderRecord(); + if (head_record != data_record) { + entries.emplace_back( + kv_allocator_->addr2offset_checked(data_record), + data_record->GetRecordSize()); + data_record->Destroy(); + } else { + data_record->SetOldVersion(kNullMemoryOffset); + } + } + break; + } + case RecordType::HashHeader: { + if (record_status != RecordStatus::Outdated && + !data_record->HasExpired()) { + entries.emplace_back( + kv_allocator_->addr2offset_checked(data_record), + data_record->GetRecordSize()); + data_record->Destroy(); + } else { + auto hash_id = HashList::FetchID(data_record); + kvdk_assert(hlists_.find(hash_id) != hlists_.end(), + "Hashlist should not be removed."); + auto head_record = getHashlist(hash_id)->HeaderRecord(); + if (head_record != data_record) { + entries.emplace_back( + kv_allocator_->addr2offset_checked(data_record), + data_record->GetRecordSize()); + data_record->Destroy(); + } else { + data_record->SetOldVersion(kNullMemoryOffset); + } + } + break; + } + case RecordType::ListHeader: { + if (record_status != RecordStatus::Outdated && + !data_record->HasExpired()) { + entries.emplace_back( + kv_allocator_->addr2offset_checked(data_record), + data_record->GetRecordSize()); + data_record->Destroy(); + } else { + auto list_id = List::FetchID(data_record); + kvdk_assert(lists_.find(list_id) != lists_.end(), + "Hashlist should not be removed."); + auto header_record = getList(list_id)->HeaderRecord(); + if (header_record != data_record) { + entries.emplace_back( + kv_allocator_->addr2offset_checked(data_record), + data_record->GetRecordSize()); + data_record->Destroy(); + } else { + data_record->SetOldVersion(kNullMemoryOffset); + } + } + break; + } + default: + GlobalLogger.Error("Cleaner abort on record type %u\n", type); + std::abort(); + } + data_record = next_record; + } + } + kv_allocator_->BatchFree(entries); +} + +void KVEngine::cleanNoHashIndexedSkiplist( + Skiplist* skiplist, std::vector& purge_dl_records) { + auto prev_node = skiplist->HeaderNode(); + auto iter = skiplist->GetDLList()->GetRecordIterator(); + iter->SeekToFirst(); + while (iter->Valid() && !closing_) { + DLRecord* cur_record = iter->Record(); + iter->Next(); + auto min_snapshot_ts = version_controller_.GlobalOldestSnapshotTs(); + auto ul = hash_table_->AcquireLock(cur_record->Key()); + // iter old version list + auto old_record = + removeOutDatedVersion(cur_record, min_snapshot_ts); + if (old_record) { + purge_dl_records.emplace_back(old_record); + } + + // check record has dram skiplist node and update skiplist node; + SkiplistNode* dram_node = nullptr; + SkiplistNode* cur_node = prev_node->Next(1).RawPointer(); + while (cur_node) { + if (cur_node->Next(1).GetTag() == SkiplistNode::NodeStatus::Deleted) { + // cur_node already been deleted + cur_node = cur_node->Next(1).RawPointer(); + } else { + kvdk_assert(cur_node->record->GetRecordType() == RecordType::SortedElem, + ""); + if (skiplist->Compare(cur_node->UserKey(), + Skiplist::UserKey(cur_record)) < 0) { + prev_node = cur_node; + cur_node = cur_node->Next(1).RawPointer(); + } else { + break; + } + } + } + + if (cur_node && cur_node->record == cur_record) { + dram_node = cur_node; + } + + if (cur_record->GetRecordType() == RecordType::SortedElem && + cur_record->GetRecordStatus() == RecordStatus::Outdated && + cur_record->GetTimestamp() < min_snapshot_ts) { + TEST_SYNC_POINT( + "KVEngine::BackgroundCleaner::IterSkiplist::" + "UnlinkDeleteRecord"); + /* Notice: a user thread firstly update this key, its old version + * record is delete record(cur_record). So the cur_record is not in + * this skiplist, `Remove` function returns false. Nothing to do for + * this cur_record which will be purged and freed in the next + * iteration. + */ + if (Skiplist::Remove(cur_record, dram_node, kv_allocator_.get(), + dllist_locks_.get())) { + purge_dl_records.emplace_back(cur_record); + } + } + } +} + +void KVEngine::cleanList(List* list, std::vector& purge_dl_records) { + auto iter = list->GetDLList()->GetRecordIterator(); + iter->SeekToFirst(); + while (iter->Valid() && !closing_) { + DLRecord* cur_record = iter->Record(); + iter->Next(); + auto ul = list->AcquireLock(); + auto min_snapshot_ts = + std::min(version_controller_.GlobalOldestSnapshotTs(), + version_controller_.LocalOldestSnapshotTS()); + auto old_record = + removeOutDatedVersion(cur_record, min_snapshot_ts); + bool clean_cur = false; + if (cur_record->GetRecordType() == RecordType::ListElem && + cur_record->GetRecordStatus() == RecordStatus::Outdated && + cur_record->GetTimestamp() < min_snapshot_ts) { + TEST_SYNC_POINT( + "KVEngine::BackgroundCleaner::IterList::" + "UnlinkDeleteRecord"); + if (list->GetDLList()->Remove(cur_record)) { + clean_cur = true; + } + } + ul.unlock(); + if (old_record) { + purge_dl_records.emplace_back(old_record); + } + if (clean_cur) { + purge_dl_records.emplace_back(cur_record); + } + } +} + +void KVEngine::purgeAndFree(PendingCleanRecords& pending_clean_records) { + { // purge and free pending string records + while (!pending_clean_records.pending_purge_strings.empty()) { + auto& pending_strings = + pending_clean_records.pending_purge_strings.front(); + if (pending_strings.release_time < + version_controller_.LocalOldestSnapshotTS()) { + purgeAndFreeStringRecords(pending_strings.records); + pending_clean_records.pending_purge_strings.pop_front(); + } else { + break; + } + } + } + + { // Destroy skiplist + while (!pending_clean_records.outdated_skiplists.empty()) { + auto& ts_skiplist = pending_clean_records.outdated_skiplists.front(); + auto skiplist = ts_skiplist.second; + if (ts_skiplist.first < version_controller_.LocalOldestSnapshotTS() && + skiplist->TryCleaningLock()) { + skiplist->DestroyAll(); + removeSkiplist(skiplist->ID()); + skiplist->ReleaseCleaningLock(); + pending_clean_records.outdated_skiplists.pop_front(); + } else { + break; + } + } + } + + { // Destroy list + while (!pending_clean_records.outdated_lists.empty()) { + auto& ts_list = pending_clean_records.outdated_lists.front(); + auto list = ts_list.second; + if (ts_list.first < version_controller_.LocalOldestSnapshotTS() && + list->TryCleaningLock()) { + { + auto ul = list->AcquireLock(); + list->DestroyAll(); + } + removeList(list->ID()); + list->ReleaseCleaningLock(); + pending_clean_records.outdated_lists.pop_front(); + } else { + break; + } + } + } + + { // Destroy hash + while (!pending_clean_records.outdated_hlists.empty()) { + auto& ts_hlist = pending_clean_records.outdated_hlists.front(); + auto hlist = ts_hlist.second; + if (ts_hlist.first < version_controller_.LocalOldestSnapshotTS() && + hlist->TryCleaningLock()) { + hlist->DestroyAll(); + removeHashlist(hlist->ID()); + hlist->ReleaseCleaningLock(); + pending_clean_records.outdated_hlists.pop_front(); + } else { + break; + } + } + } + + { // purge and free pending old dl records + while (!pending_clean_records.pending_purge_dls.empty()) { + auto& pending_dls = pending_clean_records.pending_purge_dls.front(); + if (pending_dls.release_time < + version_controller_.LocalOldestSnapshotTS()) { + purgeAndFreeDLRecords(pending_dls.records); + pending_clean_records.pending_purge_dls.pop_front(); + } else { + break; + } + } + } +} + +void KVEngine::fetchCachedOutdatedVersion( + PendingCleanRecords& pending_clean_records, + std::vector& purge_string_records, + std::vector& purge_dl_records) { + size_t i = round_robin_id_.fetch_add(1) % configs_.max_access_threads; + auto& tc = cleaner_thread_cache_[i]; + std::deque> + outdated_string_records; + std::deque> outdated_dl_records; + if (tc.outdated_dl_records.size() > kMaxCachedOldRecords || + tc.outdated_string_records.size() > kMaxCachedOldRecords) { + std::lock_guard lg(tc.mtx); + if (tc.outdated_string_records.size() > kMaxCachedOldRecords) { + outdated_string_records.swap(tc.outdated_string_records); + } + if (tc.outdated_dl_records.size() > kMaxCachedOldRecords) { + outdated_dl_records.swap(tc.outdated_dl_records); + } + } + if (outdated_string_records.size() > 0) { + std::vector to_free_strings; + version_controller_.UpdateLocalOldestSnapshot(); + auto release_time = version_controller_.LocalOldestSnapshotTS(); + auto iter = outdated_string_records.begin(); + while (iter != outdated_string_records.end()) { + if (iter->release_time < release_time) { + to_free_strings.emplace_back(iter->record); + } else { + purge_string_records.push_back(iter->record); + } + iter++; + } + pending_clean_records.pending_purge_strings.emplace_front( + std::move(to_free_strings), release_time); + } + if (outdated_dl_records.size() > 0) { + std::vector to_free_dls; + version_controller_.UpdateLocalOldestSnapshot(); + auto release_time = version_controller_.LocalOldestSnapshotTS(); + auto iter = outdated_dl_records.begin(); + + while (iter != outdated_dl_records.end()) { + if (iter->release_time < release_time) { + to_free_dls.emplace_back(iter->record); + } else { + purge_dl_records.push_back(iter->record); + } + iter++; + } + pending_clean_records.pending_purge_dls.emplace_front( + std::move(to_free_dls), release_time); + } +} + +double KVEngine::cleanOutDated(PendingCleanRecords& pending_clean_records, + size_t start_slot_idx, size_t slot_block_size) { + size_t total_num = 0; + size_t need_purge_num = 0; + version_controller_.UpdateLocalOldestSnapshot(); + + std::vector purge_string_records; + std::vector purge_dl_records; + + fetchCachedOutdatedVersion(pending_clean_records, purge_string_records, + purge_dl_records); + double outdated_collection_ratio = cleaner_.SearchOutdatedCollections(); + cleaner_.FetchOutdatedCollections(pending_clean_records); + + // Iterate hash table + size_t end_slot_idx = start_slot_idx + slot_block_size; + if (end_slot_idx > hash_table_->GetSlotsNum()) { + end_slot_idx = hash_table_->GetSlotsNum(); + } + auto hashtable_iter = hash_table_->GetIterator(start_slot_idx, end_slot_idx); + while (hashtable_iter.Valid() && !closing_) { + { // Slot lock section + auto min_snapshot_ts = version_controller_.GlobalOldestSnapshotTs(); + auto now = TimeUtils::millisecond_time(); + + auto slot_lock(hashtable_iter.AcquireSlotLock()); + auto slot_iter = hashtable_iter.Slot(); + while (slot_iter.Valid()) { + if (!slot_iter->Empty()) { + switch (slot_iter->GetIndexType()) { + case PointerType::StringRecord: { + total_num++; + auto string_record = slot_iter->GetIndex().string_record; + auto old_record = removeOutDatedVersion( + string_record, min_snapshot_ts); + if (old_record) { + purge_string_records.emplace_back(old_record); + need_purge_num++; + } + if ((string_record->GetRecordStatus() == RecordStatus::Outdated || + string_record->GetExpireTime() <= now) && + string_record->GetTimestamp() < min_snapshot_ts) { + hash_table_->Erase(&(*slot_iter)); + purge_string_records.emplace_back(string_record); + need_purge_num++; + } + break; + } + case PointerType::SkiplistNode: { + total_num++; + auto node = slot_iter->GetIndex().skiplist_node; + auto dl_record = node->record; + auto old_record = + removeOutDatedVersion(dl_record, min_snapshot_ts); + if (old_record) { + purge_dl_records.emplace_back(old_record); + need_purge_num++; + } + if (slot_iter->GetRecordStatus() == RecordStatus::Outdated && + dl_record->GetTimestamp() < min_snapshot_ts) { + bool success = Skiplist::Remove( + dl_record, node, kv_allocator_.get(), dllist_locks_.get()); + kvdk_assert(success, ""); + hash_table_->Erase(&(*slot_iter)); + purge_dl_records.emplace_back(dl_record); + need_purge_num++; + } + break; + } + case PointerType::DLRecord: { + total_num++; + auto dl_record = slot_iter->GetIndex().dl_record; + auto old_record = + removeOutDatedVersion(dl_record, min_snapshot_ts); + if (old_record) { + purge_dl_records.emplace_back(old_record); + need_purge_num++; + } + if (slot_iter->GetRecordStatus() == RecordStatus::Outdated && + dl_record->GetTimestamp() < min_snapshot_ts) { + bool success = DLList::Remove(dl_record, kv_allocator_.get(), + dllist_locks_.get()); + kvdk_assert(success, ""); + hash_table_->Erase(&(*slot_iter)); + purge_dl_records.emplace_back(dl_record); + need_purge_num++; + } + break; + } + case PointerType::Skiplist: { + Skiplist* skiplist = slot_iter->GetIndex().skiplist; + total_num++; + if (!skiplist->IndexWithHashtable() && + slot_iter->GetRecordStatus() != RecordStatus::Outdated && + !skiplist->HasExpired()) { + need_purge_num++; + pending_clean_records.no_hash_skiplists.emplace_back(skiplist); + } + skiplist->CleanObsoletedNodes(); + break; + } + case PointerType::List: { + List* list = slot_iter->GetIndex().list; + total_num++; + if (slot_iter->GetRecordStatus() != RecordStatus::Outdated && + !list->HasExpired()) { + need_purge_num++; + pending_clean_records.valid_lists.emplace_back(list); + } + break; + } + default: + break; + } + } + slot_iter++; + } + hashtable_iter.Next(); + } // Finish a slot. + + if (!pending_clean_records.no_hash_skiplists.empty()) { + for (auto& skiplist : pending_clean_records.no_hash_skiplists) { + if (skiplist && skiplist->TryCleaningLock()) { + cleanNoHashIndexedSkiplist(skiplist, purge_dl_records); + skiplist->ReleaseCleaningLock(); + } + } + pending_clean_records.no_hash_skiplists.clear(); + } + + if (!pending_clean_records.valid_lists.empty()) { + for (auto& list : pending_clean_records.valid_lists) { + if (list && list->TryCleaningLock()) { + cleanList(list, purge_dl_records); + list->ReleaseCleaningLock(); + } + } + pending_clean_records.valid_lists.clear(); + } + + auto new_ts = version_controller_.GetCurrentTimestamp(); + if (purge_string_records.size() > kMaxCachedOldRecords) { + pending_clean_records.pending_purge_strings.emplace_back( + std::move(purge_string_records), new_ts); + purge_string_records = std::vector(); + } + + if (purge_dl_records.size() > kMaxCachedOldRecords) { + pending_clean_records.pending_purge_dls.emplace_back( + std::move(purge_dl_records), new_ts); + purge_dl_records = std::vector(); + } + + purgeAndFree(pending_clean_records); + + } // Finsh iterating hash table + + // Push the remaining need purged records to global pool. + auto new_ts = version_controller_.GetCurrentTimestamp(); + if (!purge_string_records.empty()) { + pending_clean_records.pending_purge_strings.emplace_back( + std::move(purge_string_records), new_ts); + purge_string_records = std::vector(); + } + + if (!purge_dl_records.empty()) { + pending_clean_records.pending_purge_dls.emplace_back( + std::move(purge_dl_records), new_ts); + purge_dl_records = std::vector(); + } + return total_num == 0 + ? 0.0f + outdated_collection_ratio + : (need_purge_num / (double)total_num) + outdated_collection_ratio; +} + +void KVEngine::TestCleanOutDated(size_t start_slot_idx, size_t end_slot_idx) { + PendingCleanRecords pending_clean_records; + while (!bg_work_signals_.terminating) { + auto cur_slot_idx = start_slot_idx; + while (cur_slot_idx < end_slot_idx && !bg_work_signals_.terminating) { + cleanOutDated(pending_clean_records, cur_slot_idx, 1024); + cur_slot_idx += 1024; + } + } +} +// Space Cleaner + +Cleaner::OutDatedCollections::~OutDatedCollections() {} + +void Cleaner::cleanWork() { + PendingCleanRecords pending_clean_records; + std::int64_t start_pos = start_slot_.fetch_add(kSlotBlockUnit) % + (kv_engine_->hash_table_->GetSlotsNum()); + kv_engine_->cleanOutDated(pending_clean_records, start_pos, kSlotBlockUnit); + + while (pending_clean_records.Size() != 0 && !close_.load()) { + kv_engine_->version_controller_.UpdateLocalOldestSnapshot(); + kv_engine_->purgeAndFree(pending_clean_records); + } +} + +void Cleaner::AdjustCleanWorkers(size_t advice_wokers_num) { + kvdk_assert(advice_wokers_num <= clean_workers_.size(), ""); + auto active_workers_num = active_clean_workers_.load(); + if (active_workers_num < advice_wokers_num) { + for (size_t i = active_workers_num; i < advice_wokers_num; ++i) { + clean_workers_[i].Run(); + } + } else if (active_workers_num > advice_wokers_num) { + for (size_t i = advice_wokers_num; i < active_workers_num; ++i) { + clean_workers_[i].Stop(); + } + } + active_clean_workers_ = advice_wokers_num; +} + +double Cleaner::SearchOutdatedCollections() { + size_t limited_fetch_num = 0; + std::unique_lock queue_lock(outdated_collections_.queue_mtx); + int64_t before_queue_size = + static_cast(outdated_collections_.hashlists.size() + + outdated_collections_.lists.size() + + outdated_collections_.skiplists.size()); + { + std::unique_lock skiplist_lock(kv_engine_->skiplists_mu_); + auto iter = kv_engine_->expirable_skiplists_.begin(); + while (!kv_engine_->expirable_skiplists_.empty() && (*iter)->HasExpired() && + limited_fetch_num < max_thread_num_) { + auto outdated_skiplist = *iter; + iter = kv_engine_->expirable_skiplists_.erase(iter); + outdated_collections_.skiplists.emplace( + outdated_skiplist, + kv_engine_->version_controller_.GetCurrentTimestamp()); + limited_fetch_num++; + } + } + + { + std::unique_lock list_lock(kv_engine_->lists_mu_); + auto iter = kv_engine_->expirable_lists_.begin(); + while (!kv_engine_->expirable_lists_.empty() && (*iter)->HasExpired() && + limited_fetch_num < max_thread_num_) { + auto outdated_list = *iter; + iter = kv_engine_->expirable_lists_.erase(iter); + outdated_collections_.lists.emplace( + outdated_list, kv_engine_->version_controller_.GetCurrentTimestamp()); + limited_fetch_num++; + } + } + + { + std::unique_lock hlist_lock(kv_engine_->hlists_mu_); + auto iter = kv_engine_->expirable_hlists_.begin(); + while (!kv_engine_->expirable_hlists_.empty() && (*iter)->HasExpired() && + limited_fetch_num < max_thread_num_) { + auto expired_hash_list = *iter; + iter = kv_engine_->expirable_hlists_.erase(iter); + outdated_collections_.hashlists.emplace( + expired_hash_list, + kv_engine_->version_controller_.GetCurrentTimestamp()); + limited_fetch_num++; + } + } + + int64_t after_queue_size = + static_cast(outdated_collections_.hashlists.size() + + outdated_collections_.lists.size() + + outdated_collections_.skiplists.size()); + + double diff_size_ratio = + (after_queue_size - before_queue_size) / max_thread_num_; + if (diff_size_ratio > 0) { + outdated_collections_.increase_ratio = + outdated_collections_.increase_ratio == 0 + ? diff_size_ratio + : diff_size_ratio / outdated_collections_.increase_ratio; + } + return outdated_collections_.increase_ratio; +} + +void Cleaner::FetchOutdatedCollections( + PendingCleanRecords& pending_clean_records) { + Collection* outdated_collection = nullptr; + RecordType record_type; + auto min_snapshot_ts = + kv_engine_->version_controller_.GlobalOldestSnapshotTs(); + { + std::unique_lock queue_lock(outdated_collections_.queue_mtx); + if (!outdated_collection && !outdated_collections_.skiplists.empty()) { + auto skiplist_iter = outdated_collections_.skiplists.begin(); + if (skiplist_iter->second < min_snapshot_ts) { + outdated_collection = skiplist_iter->first; + record_type = RecordType::SortedHeader; + outdated_collections_.skiplists.erase(skiplist_iter); + } + } + + if (!outdated_collection && !outdated_collections_.lists.empty()) { + auto list_iter = outdated_collections_.lists.begin(); + if (list_iter->second < min_snapshot_ts) { + outdated_collection = list_iter->first; + record_type = RecordType::ListHeader; + outdated_collections_.lists.erase(list_iter); + } + } + + if (!outdated_collection && !outdated_collections_.hashlists.empty()) { + auto hash_list_iter = outdated_collections_.hashlists.begin(); + if (hash_list_iter->second < min_snapshot_ts) { + outdated_collection = hash_list_iter->first; + record_type = RecordType::HashHeader; + outdated_collections_.hashlists.erase(hash_list_iter); + } + } + } + + if (outdated_collection) { + auto guard = + kv_engine_->hash_table_->AcquireLock(outdated_collection->Name()); + auto lookup_result = + kv_engine_->lookupKey(outdated_collection->Name(), record_type); + switch (lookup_result.entry_ptr->GetIndexType()) { + case PointerType::HashList: { + auto outdated_hlist = static_cast(outdated_collection); + if (lookup_result.entry_ptr->GetIndex().hlist->ID() == + outdated_collection->ID()) { + kv_engine_->hash_table_->Erase(lookup_result.entry_ptr); + } + kv_engine_->removeOutdatedCollection( + lookup_result.entry_ptr->GetIndex().hlist); + pending_clean_records.outdated_hlists.emplace_back(std::make_pair( + kv_engine_->version_controller_.GetCurrentTimestamp(), + outdated_hlist)); + break; + } + case PointerType::List: { + auto outdated_list = static_cast(outdated_collection); + if (lookup_result.entry_ptr->GetIndex().list->ID() == + outdated_collection->ID()) { + kv_engine_->hash_table_->Erase(lookup_result.entry_ptr); + } + kv_engine_->removeOutdatedCollection( + lookup_result.entry_ptr->GetIndex().list); + pending_clean_records.outdated_lists.emplace_back(std::make_pair( + kv_engine_->version_controller_.GetCurrentTimestamp(), + outdated_list)); + break; + } + case PointerType::Skiplist: { + auto outdated_skiplist = static_cast(outdated_collection); + if (lookup_result.entry_ptr->GetIndex().skiplist->ID() == + outdated_collection->ID()) { + kv_engine_->hash_table_->Erase(lookup_result.entry_ptr); + } + kv_engine_->removeOutdatedCollection( + lookup_result.entry_ptr->GetIndex().skiplist); + pending_clean_records.outdated_skiplists.emplace_back(std::make_pair( + kv_engine_->version_controller_.GetCurrentTimestamp(), + outdated_skiplist)); + break; + } + default: + break; + } + } +} + +void Cleaner::mainWork() { + PendingCleanRecords pending_clean_records; + std::int64_t start_pos = start_slot_.fetch_add(kSlotBlockUnit) % + (kv_engine_->hash_table_->GetSlotsNum()); + + double outdated_ratio = kv_engine_->cleanOutDated(pending_clean_records, + start_pos, kSlotBlockUnit); + + size_t advice_thread_num = min_thread_num_; + if (outdated_ratio >= kWakeUpThreshold) { + advice_thread_num = std::ceil(outdated_ratio * max_thread_num_); + advice_thread_num = + std::min(std::max(min_thread_num_, advice_thread_num), max_thread_num_); + } + TEST_SYNC_POINT_CALLBACK("KVEngine::Cleaner::AdjustCleanWorkers", + &advice_thread_num); + size_t advice_clean_workers = + advice_thread_num > 0 ? advice_thread_num - 1 : 0; + + AdjustCleanWorkers(advice_clean_workers); + if (outdated_ratio < kWakeUpThreshold) { + sleep(1); + } + + while (pending_clean_records.Size() != 0 && !close_.load()) { + kv_engine_->version_controller_.UpdateLocalOldestSnapshot(); + kv_engine_->purgeAndFree(pending_clean_records); + } +} +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/kv_engine_cleaner.hpp b/volatile/engine/kv_engine_cleaner.hpp new file mode 100644 index 00000000..0e236a29 --- /dev/null +++ b/volatile/engine/kv_engine_cleaner.hpp @@ -0,0 +1,225 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alias.hpp" +#include "collection.hpp" +#include "hash_table.hpp" +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { + +struct PendingFreeSpaceEntries { + std::vector entries; + // Indicate timestamp of the oldest refered snapshot of kvdk instance while we + // could safely free these entries + TimestampType release_time; +}; + +struct PendingFreeSpaceEntry { + SpaceEntry entry; + // Indicate timestamp of the oldest refered snapshot of kvdk instance while we + // could safely free this entry + TimestampType release_time; +}; + +struct PendingPurgeStrRecords { + PendingPurgeStrRecords(std::vector&& _records, + TimestampType _release_time) + : records(_records), release_time(_release_time) {} + + std::vector records; + TimestampType release_time; +}; + +struct PendingPurgeDLRecords { + PendingPurgeDLRecords(std::vector&& _records, + TimestampType _release_time) + : records(_records), release_time(_release_time) {} + + std::vector records; + TimestampType release_time; +}; + +struct PendingCleanRecords { + std::deque> outdated_lists; + std::deque> outdated_hlists; + std::deque> outdated_skiplists; + std::deque pending_purge_strings; + std::deque pending_purge_dls; + std::deque no_hash_skiplists; + std::deque valid_lists; + size_t Size() { + return outdated_lists.size() + outdated_hlists.size() + + outdated_skiplists.size() + pending_purge_strings.size() + + pending_purge_dls.size() + no_hash_skiplists.size() + + valid_lists.size(); + } +}; + +class KVEngine; + +class Cleaner { + public: + static constexpr int64_t kSlotBlockUnit = 1024; + static constexpr double kWakeUpThreshold = 0.1; + + Cleaner(KVEngine* kv_engine, int64_t max_cleaner_threads) + : kv_engine_(kv_engine), + max_thread_num_(max_cleaner_threads), + min_thread_num_(1), + close_(false), + start_slot_(0), + active_clean_workers_(0), + main_worker_([this]() { this->mainWork(); }), + clean_workers_(max_thread_num_ - 1 /*1 for main worker*/) { + for (auto& w : clean_workers_) { + w.Init([this]() { this->cleanWork(); }); + } + } + + ~Cleaner() { Close(); } + + void Start() { + if (!close_ && min_thread_num_ > 0) { + main_worker_.Run(); + } + } + + void Close() { + close_ = true; + main_worker_.Join(); + for (auto& w : clean_workers_) { + w.Join(); + } + } + + void AdjustCleanWorkers(size_t advice_workers_num); + + size_t ActiveThreadNum() { return active_clean_workers_.load() + 1; } + + double SearchOutdatedCollections(); + void FetchOutdatedCollections(PendingCleanRecords& pending_clean_records); + + private: + class Worker { + public: + ~Worker() { Join(); } + + Worker(std::function f) { Init(f); } + + Worker() = default; + + void Init(std::function f) { + Join(); + func = f; + std::unique_lock ul(spin); + join_ = false; + keep_work_ = false; + + auto work = [&]() { + while (true) { + { + std::unique_lock ul(spin); + while (!keep_work_ && !join_) { + cv.wait(ul); + } + if (join_) { + return; + } + } + func(); + } + }; + worker_thread = std::thread(work); + } + + void Run() { + std::unique_lock ul(spin); + keep_work_ = true; + cv.notify_all(); + } + + void Stop() { + std::unique_lock ul(spin); + keep_work_ = false; + } + + void Join() { + { + std::unique_lock ul(spin); + keep_work_ = false; + join_ = true; + cv.notify_all(); + } + if (worker_thread.joinable()) { + worker_thread.join(); + } + } + + private: + bool keep_work_; + bool join_; + std::condition_variable_any cv; + SpinMutex spin; + std::thread worker_thread; + std::function func; + }; + + KVEngine* kv_engine_; + + size_t max_thread_num_; + size_t min_thread_num_; + std::atomic close_; + std::atomic start_slot_; + std::atomic active_clean_workers_; + Worker main_worker_; + std::vector clean_workers_; + + struct OutDatedCollections { + struct TimeStampCmp { + public: + bool operator()(const std::pair a, + const std::pair b) const { + if (a.second < b.second) return true; + if (a.second == b.second && a.first->ID() < b.first->ID()) return true; + return false; + } + }; + using ListQueue = std::set, TimeStampCmp>; + using HashListQueue = + std::set, TimeStampCmp>; + + using SkiplistQueue = + std::set, TimeStampCmp>; + + SpinMutex queue_mtx; + ListQueue lists; + HashListQueue hashlists; + SkiplistQueue skiplists; + double increase_ratio = 0; + ~OutDatedCollections(); + }; + + OutDatedCollections outdated_collections_; + + private: + void cleanWork(); + void mainWork(); +}; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/kv_engine_hash.cpp b/volatile/engine/kv_engine_hash.cpp new file mode 100644 index 00000000..c849bc56 --- /dev/null +++ b/volatile/engine/kv_engine_hash.cpp @@ -0,0 +1,272 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "hash_collection/iterator.hpp" +#include "kv_engine.hpp" + +namespace KVDK_NAMESPACE { +Status KVEngine::HashCreate(StringView collection) { + auto thread_holder = AcquireAccessThread(); + + if (!checkKeySize(collection)) { + return Status::InvalidDataSize; + } + + std::shared_ptr hlist = nullptr; + return buildHashlist(collection, hlist); +} + +Status KVEngine::buildHashlist(const StringView& collection, + std::shared_ptr& hlist) { + auto ul = hash_table_->AcquireLock(collection); + auto holder = version_controller_.GetLocalSnapshotHolder(); + TimestampType new_ts = holder.Timestamp(); + auto lookup_result = lookupKey(collection, RecordType::HashHeader); + if (lookup_result.s == Status::NotFound || + lookup_result.s == Status::Outdated) { + DLRecord* existing_header = + lookup_result.s == Outdated + ? lookup_result.entry.GetIndex().hlist->HeaderRecord() + : nullptr; + CollectionIDType id = collection_id_.fetch_add(1); + std::string value_str = HashList::EncodeID(id); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(collection, value_str)); + if (space.size == 0) { + return Status::MemoryOverflow; + } + // dl list is circular, so the next and prev pointers of + // header point to itself + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space.offset), space.size, new_ts, + RecordType::HashHeader, RecordStatus::Normal, + kv_allocator_->addr2offset(existing_header), space.offset, space.offset, + collection, value_str); + hlist = std::make_shared(data_record, collection, id, + kv_allocator_.get(), hash_table_.get(), + dllist_locks_.get()); + kvdk_assert(hlist != nullptr, ""); + addHashlistToMap(hlist); + insertKeyOrElem(lookup_result, RecordType::HashHeader, RecordStatus::Normal, + hlist.get()); + return Status::Ok; + } else { + return lookup_result.s == Status::Ok ? Status::Existed : lookup_result.s; + } +} + +Status KVEngine::HashDestroy(StringView collection) { + auto thread_holder = AcquireAccessThread(); + + if (!checkKeySize(collection)) { + return Status::InvalidDataSize; + } + + auto ul = hash_table_->AcquireLock(collection); + auto snapshot_holder = version_controller_.GetLocalSnapshotHolder(); + auto new_ts = snapshot_holder.Timestamp(); + HashList* hlist; + Status s = hashListFind(collection, &hlist); + if (s == Status::Ok) { + DLRecord* header = hlist->HeaderRecord(); + kvdk_assert(header->GetRecordType() == RecordType::HashHeader, ""); + StringView value = header->Value(); + auto request_size = DLRecord::RecordSize(collection, value); + SpaceEntry space = kv_allocator_->Allocate(request_size); + if (space.size == 0) { + return Status::MemoryOverflow; + } + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space.offset), space.size, new_ts, + RecordType::HashHeader, RecordStatus::Outdated, + kv_allocator_->addr2offset_checked(header), header->prev, header->next, + collection, value); + bool success = hlist->Replace(header, data_record); + kvdk_assert(success, "existing header should be linked on its hlist"); + hash_table_->Insert(collection, RecordType::HashHeader, + RecordStatus::Outdated, hlist, PointerType::HashList); + { + std::unique_lock hlist_lock(hlists_mu_); + expirable_hlists_.emplace(hlist); + } + } + return s; +} + +Status KVEngine::HashSize(StringView collection, size_t* len) { + if (!checkKeySize(collection)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + HashList* hlist; + Status s = hashListFind(collection, &hlist); + if (s != Status::Ok) { + return s; + } + *len = hlist->Size(); + return Status::Ok; +} + +Status KVEngine::HashGet(StringView collection, StringView key, + std::string* value) { + auto thread_holder = AcquireAccessThread(); + + // Hold current snapshot in this thread + auto holder = version_controller_.GetLocalSnapshotHolder(); + + HashList* hlist; + Status s = hashListFind(collection, &hlist); + if (s == Status::Ok) { + s = hlist->Get(key, value); + } + return s; +} + +Status KVEngine::HashPut(StringView collection, StringView key, + StringView value) { + auto thread_holder = AcquireAccessThread(); + + // Hold current snapshot in this thread + auto holder = version_controller_.GetLocalSnapshotHolder(); + + HashList* hlist; + Status s = hashListFind(collection, &hlist); + if (s == Status::Ok) { + std::string collection_key(hlist->InternalKey(key)); + if (!checkKeySize(collection_key) || !checkValueSize(value)) { + s = Status::InvalidDataSize; + } else { + auto ul = hash_table_->AcquireLock(collection_key); + auto ret = + hlist->Put(key, value, version_controller_.GetCurrentTimestamp()); + if (ret.s == Status::Ok && ret.existing_record && + hlist->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + hlist->ReleaseCleaningLock(); + } + tryCleanCachedOutdatedRecord(); + s = ret.s; + } + } + return s; +} + +Status KVEngine::HashDelete(StringView collection, StringView key) { + auto thread_holder = AcquireAccessThread(); + + // Hold current snapshot in this thread + auto holder = version_controller_.GetLocalSnapshotHolder(); + + HashList* hlist; + Status s = hashListFind(collection, &hlist); + if (s == Status::Ok) { + std::string collection_key(hlist->InternalKey(key)); + if (!checkKeySize(collection_key)) { + s = Status::InvalidDataSize; + } else { + auto ul = hash_table_->AcquireLock(collection_key); + auto ret = hlist->Delete(key, version_controller_.GetCurrentTimestamp()); + if (ret.s == Status::Ok && ret.existing_record && ret.write_record && + hlist->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + hlist->ReleaseCleaningLock(); + } + tryCleanCachedOutdatedRecord(); + s = ret.s; + } + } + return s; +} + +Status KVEngine::HashModify(StringView collection, StringView key, + ModifyFunc modify_func, void* cb_args) { + auto thread_holder = AcquireAccessThread(); + + // Hold current snapshot in this thread + auto holder = version_controller_.GetLocalSnapshotHolder(); + HashList* hlist; + Status s = hashListFind(collection, &hlist); + if (s == Status::Ok) { + std::string internal_key(hlist->InternalKey(key)); + auto ul = hash_table_->AcquireLock(internal_key); + auto ret = hlist->Modify(key, modify_func, cb_args, + version_controller_.GetCurrentTimestamp()); + s = ret.s; + if (s == Status::Ok && ret.existing_record && ret.write_record && + hlist->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + hlist->ReleaseCleaningLock(); + } + tryCleanCachedOutdatedRecord(); + } + return s; +} + +HashIterator* KVEngine::HashIteratorCreate(StringView collection, + Snapshot* snapshot, Status* status) { + Status s{Status::Ok}; + HashIterator* ret(nullptr); + if (!checkKeySize(collection)) { + s = Status::InvalidDataSize; + } + + if (s == Status::Ok) { + bool create_snapshot = snapshot == nullptr; + if (create_snapshot) { + snapshot = GetSnapshot(false); + } + HashList* hlist; + Status s = hashListFind(collection, &hlist); + if (s == Status::Ok) { + ret = new HashIteratorImpl(hlist, static_cast(snapshot), + create_snapshot); + } else if (create_snapshot) { + ReleaseSnapshot(snapshot); + } + } + if (status) { + *status = s; + } + return ret; +} + +void KVEngine::HashIteratorRelease(HashIterator* hash_iter) { + if (hash_iter == nullptr) { + GlobalLogger.Info("pass a nullptr in KVEngine::HashIteratorRelease!\n"); + return; + } + HashIteratorImpl* iter = static_cast(hash_iter); + if (iter->own_snapshot_) { + ReleaseSnapshot(iter->snapshot_); + } + delete iter; +} + +Status KVEngine::hashListFind(StringView collection, HashList** hlist) { + // Callers should acquire the access token or snapshot. + // Lockless lookup for the collection + auto result = lookupKey(collection, RecordType::HashHeader); + if (result.s == Status::Outdated) { + return Status::NotFound; + } + if (result.s != Status::Ok) { + return result.s; + } + (*hlist) = result.entry.GetIndex().hlist; + return Status::Ok; +} + +Status KVEngine::hashWritePrepare(HashWriteArgs& args, TimestampType ts) { + return args.hlist->PrepareWrite(args, ts); +} + +Status KVEngine::hashListWrite(HashWriteArgs& args) { + return args.hlist->Write(args).s; +} + +Status KVEngine::hashListPublish(HashWriteArgs const&) { return Status::Ok; } + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/kv_engine_list.cpp b/volatile/engine/kv_engine_list.cpp new file mode 100644 index 00000000..28e6d040 --- /dev/null +++ b/volatile/engine/kv_engine_list.cpp @@ -0,0 +1,607 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "kv_engine.hpp" +#include "list_collection/iterator.hpp" +#include "utils/sync_point.hpp" + +namespace KVDK_NAMESPACE { +Status KVEngine::ListCreate(StringView list_name) { + auto thread_holder = AcquireAccessThread(); + + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + + std::shared_ptr list = nullptr; + return buildList(list_name, list); +} + +Status KVEngine::buildList(const StringView& list_name, + std::shared_ptr& list) { + auto ul = hash_table_->AcquireLock(list_name); + auto holder = version_controller_.GetLocalSnapshotHolder(); + TimestampType new_ts = holder.Timestamp(); + auto lookup_result = lookupKey(list_name, RecordType::ListHeader); + if (lookup_result.s == Status::NotFound || + lookup_result.s == Status::Outdated) { + DLRecord* existing_header = + lookup_result.s == Outdated + ? lookup_result.entry.GetIndex().hlist->HeaderRecord() + : nullptr; + CollectionIDType id = collection_id_.fetch_add(1); + std::string value_str = List::EncodeID(id); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(list_name, value_str)); + if (space.size == 0) { + return Status::MemoryOverflow; + } + // dl list is circular, so the next and prev pointers of + // header point to itself + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space.offset), space.size, new_ts, + RecordType::ListHeader, RecordStatus::Normal, + kv_allocator_->addr2offset(existing_header), space.offset, space.offset, + list_name, value_str); + list = std::make_shared(data_record, list_name, id, + kv_allocator_.get(), dllist_locks_.get()); + kvdk_assert(list != nullptr, ""); + addListToMap(list); + insertKeyOrElem(lookup_result, RecordType::ListHeader, RecordStatus::Normal, + list.get()); + return Status::Ok; + } else { + return lookup_result.s == Status::Ok ? Status::Existed : lookup_result.s; + } +} + +Status KVEngine::ListDestroy(StringView collection) { + if (!checkKeySize(collection)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto ul = hash_table_->AcquireLock(collection); + auto snapshot_holder = version_controller_.GetLocalSnapshotHolder(); + auto new_ts = snapshot_holder.Timestamp(); + List* list; + Status s = listFind(collection, &list); + if (s == Status::Ok) { + DLRecord* header = list->HeaderRecord(); + kvdk_assert(header->GetRecordType() == RecordType::ListHeader, ""); + StringView value = header->Value(); + auto request_size = DLRecord::RecordSize(collection, value); + SpaceEntry space = kv_allocator_->Allocate(request_size); + if (space.size == 0) { + return Status::MemoryOverflow; + } + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space.offset), space.size, new_ts, + RecordType::ListHeader, RecordStatus::Outdated, + kv_allocator_->addr2offset_checked(header), header->prev, header->next, + collection, value); + bool success = list->Replace(header, data_record); + kvdk_assert(success, "existing header should be linked on its list"); + hash_table_->Insert(collection, RecordType::ListHeader, + RecordStatus::Outdated, list, PointerType::List); + { + std::unique_lock list_lock(lists_mu_); + expirable_lists_.emplace(list); + } + } + return s; +} + +Status KVEngine::ListSize(StringView list_name, size_t* sz) { + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + *sz = list->Size(); + return Status::Ok; +} + +Status KVEngine::ListPushFront(StringView collection, StringView elem) { + if (!checkKeySize(collection) || !checkValueSize(elem)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(collection, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + return list->PushFront(elem, version_controller_.GetCurrentTimestamp()).s; +} + +Status KVEngine::ListPushBack(StringView list_name, StringView elem) { + if (!checkKeySize(list_name) || !checkValueSize(elem)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + return list->PushBack(elem, version_controller_.GetCurrentTimestamp()).s; +} + +Status KVEngine::ListPopFront(StringView list_name, std::string* elem) { + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + auto ret = list->PopFront(version_controller_.GetCurrentTimestamp()); + + if (ret.s == Status::Ok) { + kvdk_assert(ret.existing_record && ret.write_record, ""); + if (elem) { + elem->assign(ret.existing_record->Value().data(), + ret.existing_record->Value().size()); + } + if (list->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + list->ReleaseCleaningLock(); + } + } + tryCleanCachedOutdatedRecord(); + return ret.s; +} + +Status KVEngine::ListPopBack(StringView list_name, std::string* elem) { + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + auto ret = list->PopBack(version_controller_.GetCurrentTimestamp()); + if (ret.existing_record == nullptr) { + /// TODO: NotFound does not properly describe the situation + return Status::NotFound; + } + + if (ret.s == Status::Ok) { + kvdk_assert(ret.existing_record && ret.write_record, ""); + if (elem) { + elem->assign(ret.existing_record->Value().data(), + ret.existing_record->Value().size()); + } + kvdk_assert(ret.existing_record && ret.write_record, ""); + + if (list->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + list->ReleaseCleaningLock(); + } + } + tryCleanCachedOutdatedRecord(); + return ret.s; +} + +Status KVEngine::ListBatchPushFront(StringView list_name, + std::vector const& elems) { + return ListBatchPushFront( + list_name, std::vector{elems.begin(), elems.end()}); +} + +Status KVEngine::ListBatchPushFront(StringView list_name, + std::vector const& elems) { + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + if (elems.size() > BatchWriteLog::Capacity()) { + return Status::InvalidBatchSize; + } + for (auto const& elem : elems) { + if (!checkValueSize(elem)) { + return Status::InvalidDataSize; + } + } + auto thread_holder = AcquireAccessThread(); + + Status s = maybeInitBatchLogFile(); + if (s != Status::Ok) { + return s; + } + return listBatchPushImpl(list_name, ListPos::Front, elems); +} + +Status KVEngine::ListBatchPushBack(StringView list_name, + std::vector const& elems) { + return ListBatchPushBack(list_name, + std::vector{elems.begin(), elems.end()}); +} + +Status KVEngine::ListBatchPushBack(StringView list_name, + std::vector const& elems) { + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + if (elems.size() > BatchWriteLog::Capacity()) { + return Status::InvalidBatchSize; + } + for (auto const& elem : elems) { + if (!checkValueSize(elem)) { + return Status::InvalidDataSize; + } + } + auto thread_holder = AcquireAccessThread(); + + Status s = maybeInitBatchLogFile(); + if (s != Status::Ok) { + return s; + } + return listBatchPushImpl(list_name, ListPos::Back, elems); +} + +Status KVEngine::ListBatchPopFront(StringView list_name, size_t n, + std::vector* elems) { + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + Status s = maybeInitBatchLogFile(); + if (s != Status::Ok) { + return s; + } + return listBatchPopImpl(list_name, ListPos::Front, n, elems); +} + +Status KVEngine::ListBatchPopBack(StringView list_name, size_t n, + std::vector* elems) { + if (!checkKeySize(list_name)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + Status s = maybeInitBatchLogFile(); + if (s != Status::Ok) { + return s; + } + + return listBatchPopImpl(list_name, ListPos::Back, n, elems); +} + +Status KVEngine::ListMove(StringView src, ListPos src_pos, StringView dst, + ListPos dst_pos, std::string* elem) { + if (!checkKeySize(src) || !checkKeySize(dst)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + Status s = maybeInitBatchLogFile(); + if (s != Status::Ok) { + return s; + } + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* src_list; + List* dst_list; + /// TODO: we must guarantee a consistent view for the List. + /// The same holds for other collections. + /// No collection should be expired, created or deleted during BatchWrite. + s = listFind(src, &src_list); + if (s != Status::Ok) { + return s; + } + if (src != dst) { + s = listFind(dst, &dst_list); + if (s != Status::Ok) { + return s; + } + } else { + dst_list = src_list; + } + + std::unique_lock guard1; + std::unique_lock guard2; + if (src_list < dst_list) { + guard1 = src_list->AcquireLock(); + guard2 = dst_list->AcquireLock(); + } else if (src_list > dst_list) { + guard1 = dst_list->AcquireLock(); + guard2 = src_list->AcquireLock(); + } else { + kvdk_assert(src == dst, ""); + guard1 = src_list->AcquireLock(); + } + + if (src == dst && src_pos == dst_pos) { + s = src_pos == ListPos::Front ? src_list->Front(elem) + : src_list->Back(elem); + return s; + } + + auto bw_token = version_controller_.GetBatchWriteToken(); + BatchWriteLog log; + log.SetTimestamp(bw_token.Timestamp()); + std::vector elems; + + auto pop_args = + src_list->PreparePopN((ListPos)src_pos, 1, bw_token.Timestamp(), &elems); + if (pop_args.s != Status::Ok) { + return pop_args.s; + } + kvdk_assert(elems.size() == 1, ""); + elem->swap(elems[0]); + std::vector elems_view{StringView(elem->data(), elem->size())}; + auto push_args = dst_list->PreparePushN((ListPos)dst_pos, elems_view, + bw_token.Timestamp()); + if (push_args.s != Status::Ok) { + return push_args.s; + } + + log.ListDelete(pop_args.spaces[0].offset); + log.ListEmplace(push_args.spaces[0].offset); + auto& tc = engine_thread_cache_[ThreadManager::ThreadID() % + configs_.max_access_threads]; + log.EncodeTo(tc.batch_log); + + BatchWriteLog::MarkProcessing(tc.batch_log); + + s = src_list->PopN(pop_args); + kvdk_assert(s == Status::Ok, "pop n always success"); + TEST_CRASH_POINT("KVEngine::ListMove", ""); + s = dst_list->PushN(push_args); + kvdk_assert(s == Status::Ok, "push n always success"); + + BatchWriteLog::MarkCommitted(tc.batch_log); + return Status::Ok; +} + +Status KVEngine::ListInsertAt(StringView list_name, StringView elem, + long index) { + if (!checkValueSize(elem)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + return list->InsertAt(elem, index, version_controller_.GetCurrentTimestamp()) + .s; +} + +Status KVEngine::ListInsertBefore(StringView list_name, StringView elem, + StringView pos) { + if (!checkValueSize(elem)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + return list + ->InsertBefore(elem, pos, version_controller_.GetCurrentTimestamp()) + .s; +} + +Status KVEngine::ListInsertAfter(StringView collection, StringView elem, + StringView dst) { + if (!checkValueSize(elem)) { + return Status::InvalidDataSize; + } + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(collection, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + return list->InsertAfter(elem, dst, version_controller_.GetCurrentTimestamp()) + .s; +} + +Status KVEngine::ListErase(StringView list_name, long index, + std::string* elem) { + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + auto ret = list->Erase(index, version_controller_.GetCurrentTimestamp()); + if (ret.s == Status::Ok) { + kvdk_assert(ret.existing_record && ret.write_record, ""); + if (elem) { + elem->assign(ret.existing_record->Value().data(), + ret.existing_record->Value().size()); + } + if (list->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + list->ReleaseCleaningLock(); + } + } + tryCleanCachedOutdatedRecord(); + return ret.s; +} + +// Replace the element at pos +Status KVEngine::ListReplace(StringView collection, long index, + StringView elem) { + auto thread_holder = AcquireAccessThread(); + + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(collection, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + return list->Update(index, elem, version_controller_.GetCurrentTimestamp()).s; +} + +ListIterator* KVEngine::ListIteratorCreate(StringView collection, + Snapshot* snapshot, Status* status) { + Status s{Status::Ok}; + ListIterator* ret(nullptr); + if (!checkKeySize(collection)) { + s = Status::InvalidDataSize; + } + + if (s == Status::Ok) { + bool create_snapshot = snapshot == nullptr; + if (create_snapshot) { + snapshot = GetSnapshot(false); + } + List* list; + Status s = listFind(collection, &list); + if (s == Status::Ok) { + ret = new ListIteratorImpl(list, static_cast(snapshot), + create_snapshot); + } else if (create_snapshot) { + ReleaseSnapshot(snapshot); + } + } + + if (status) { + *status = s; + } + return ret; +} + +void KVEngine::ListIteratorRelease(ListIterator* list) { + if (list == nullptr) { + GlobalLogger.Info("pass a nullptr in KVEngine::SortedIteratorRelease!\n"); + return; + } + ListIteratorImpl* iter = static_cast(list); + if (iter->own_snapshot_) { + ReleaseSnapshot(iter->snapshot_); + } + delete iter; +} + +Status KVEngine::listFind(StringView list_name, List** list) { + auto result = lookupKey(list_name, RecordType::ListHeader); + if (result.s == Status::Outdated) { + return Status::NotFound; + } + if (result.s != Status::Ok) { + return result.s; + } + (*list) = result.entry.GetIndex().list; + return Status::Ok; +} + +Status KVEngine::listBatchPushImpl(StringView list_name, ListPos pos, + std::vector const& elems) { + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + auto bw_token = version_controller_.GetBatchWriteToken(); + BatchWriteLog log; + log.SetTimestamp(bw_token.Timestamp()); + + auto push_n_args = list->PreparePushN(pos, elems, bw_token.Timestamp()); + if (push_n_args.s != Status::Ok) { + return push_n_args.s; + } + + for (auto& space : push_n_args.spaces) { + log.ListEmplace(space.offset); + } + + auto& tc = engine_thread_cache_[ThreadManager::ThreadID() % + configs_.max_access_threads]; + log.EncodeTo(tc.batch_log); + BatchWriteLog::MarkProcessing(tc.batch_log); + + s = list->PushN(push_n_args); + kvdk_assert(s == Status::Ok, "PushN always success"); + BatchWriteLog::MarkCommitted(tc.batch_log); + return s; +} + +Status KVEngine::listBatchPopImpl(StringView list_name, ListPos pos, size_t n, + std::vector* elems) { + auto token = version_controller_.GetLocalSnapshotHolder(); + List* list; + Status s = listFind(list_name, &list); + if (s != Status::Ok) { + return s; + } + auto guard = list->AcquireLock(); + + auto bw_token = version_controller_.GetBatchWriteToken(); + BatchWriteLog log; + log.SetTimestamp(bw_token.Timestamp()); + + auto pop_n_args = list->PreparePopN(pos, n, bw_token.Timestamp(), elems); + if (pop_n_args.s != Status::Ok) { + return pop_n_args.s; + } + + for (auto& space : pop_n_args.spaces) { + log.ListDelete(space.offset); + } + + auto& tc = engine_thread_cache_[ThreadManager::ThreadID() % + configs_.max_access_threads]; + log.EncodeTo(tc.batch_log); + BatchWriteLog::MarkProcessing(tc.batch_log); + + s = list->PopN(pop_n_args); + kvdk_assert(s == Status::Ok, "PopN always success with lock"); + BatchWriteLog::MarkCommitted(tc.batch_log); + return s; +} + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/kv_engine_sorted.cpp b/volatile/engine/kv_engine_sorted.cpp new file mode 100644 index 00000000..7bee661f --- /dev/null +++ b/volatile/engine/kv_engine_sorted.cpp @@ -0,0 +1,283 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "kv_engine.hpp" +#include "sorted_collection/iterator.hpp" + +namespace KVDK_NAMESPACE { +Status KVEngine::SortedCreate(const StringView collection_name, + const SortedCollectionConfigs& s_configs) { + auto thread_holder = AcquireAccessThread(); + + if (!checkKeySize(collection_name)) { + return Status::InvalidDataSize; + } + + std::shared_ptr skiplist = nullptr; + + return buildSkiplist(collection_name, s_configs, skiplist); +} + +Status KVEngine::buildSkiplist(const StringView& collection_name, + const SortedCollectionConfigs& s_configs, + std::shared_ptr& skiplist) { + auto ul = hash_table_->AcquireLock(collection_name); + auto holder = version_controller_.GetLocalSnapshotHolder(); + TimestampType new_ts = holder.Timestamp(); + auto lookup_result = + lookupKey(collection_name, RecordType::SortedHeader); + if (lookup_result.s == NotFound || lookup_result.s == Outdated) { + DLRecord* existing_header = + lookup_result.s == Outdated + ? lookup_result.entry.GetIndex().skiplist->HeaderRecord() + : nullptr; + auto comparator = comparators_.GetComparator(s_configs.comparator_name); + if (comparator == nullptr) { + GlobalLogger.Error("Compare function %s is not registered\n", + s_configs.comparator_name); + return Status::Abort; + } + CollectionIDType id = collection_id_.fetch_add(1); + std::string value_str = + Skiplist::EncodeSortedCollectionValue(id, s_configs); + uint32_t request_size = + sizeof(DLRecord) + collection_name.size() + value_str.size(); + SpaceEntry space_entry = kv_allocator_->Allocate(request_size); + if (space_entry.size == 0) { + return Status::MemoryOverflow; + } + + // Data level of dl list is circular, so the next and prev pointers of + // header point to itself + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr(space_entry.offset), space_entry.size, + new_ts, RecordType::SortedHeader, RecordStatus::Normal, + kv_allocator_->addr2offset(existing_header), space_entry.offset, + space_entry.offset, collection_name, value_str); + + skiplist = std::make_shared( + data_record, string_view_2_string(collection_name), id, comparator, + kv_allocator_.get(), skiplist_node_allocator_.get(), hash_table_.get(), + dllist_locks_.get(), s_configs.index_with_hashtable); + addSkiplistToMap(skiplist); + insertKeyOrElem(lookup_result, RecordType::SortedHeader, + RecordStatus::Normal, skiplist.get()); + } else { + return lookup_result.s == Status::Ok ? Status::Existed : lookup_result.s; + } + return Status::Ok; +} + +Status KVEngine::SortedDestroy(const StringView collection_name) { + auto thread_holder = AcquireAccessThread(); + + auto ul = hash_table_->AcquireLock(collection_name); + auto snapshot_holder = version_controller_.GetLocalSnapshotHolder(); + auto new_ts = snapshot_holder.Timestamp(); + auto lookup_result = + lookupKey(collection_name, RecordType::SortedHeader); + if (lookup_result.s == Status::Ok) { + Skiplist* skiplist = lookup_result.entry.GetIndex().skiplist; + DLRecord* header = skiplist->HeaderRecord(); + assert(header->GetRecordType() == RecordType::SortedHeader); + StringView value = header->Value(); + auto request_size = + sizeof(DLRecord) + collection_name.size() + value.size(); + SpaceEntry space_entry = kv_allocator_->Allocate(request_size); + if (space_entry.size == 0) { + return Status::MemoryOverflow; + } + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space_entry.offset), + space_entry.size, new_ts, RecordType::SortedHeader, + RecordStatus::Outdated, kv_allocator_->addr2offset_checked(header), + header->prev, header->next, collection_name, value, 0); + bool success = + Skiplist::Replace(header, data_record, skiplist->HeaderNode(), + kv_allocator_.get(), dllist_locks_.get()); + kvdk_assert(success, "existing header should be linked on its skiplist"); + insertKeyOrElem(lookup_result, RecordType::SortedHeader, + RecordStatus::Outdated, skiplist); + { + std::unique_lock skiplist_lock(skiplists_mu_); + expirable_skiplists_.emplace(skiplist); + } + } else if (lookup_result.s == Status::Outdated || + lookup_result.s == Status::NotFound) { + lookup_result.s = Status::Ok; + } + return lookup_result.s; +} + +Status KVEngine::SortedSize(const StringView collection, size_t* size) { + auto thread_holder = AcquireAccessThread(); + + auto holder = version_controller_.GetLocalSnapshotHolder(); + + Skiplist* skiplist = nullptr; + auto ret = lookupKey(collection, RecordType::SortedHeader); + if (ret.s != Status::Ok) { + return ret.s == Status::Outdated ? Status::NotFound : ret.s; + } + + kvdk_assert(ret.entry.GetIndexType() == PointerType::Skiplist, + "pointer type of skiplist in hash entry should be skiplist"); + skiplist = ret.entry.GetIndex().skiplist; + *size = skiplist->Size(); + return Status::Ok; +} + +Status KVEngine::SortedGet(const StringView collection, + const StringView user_key, std::string* value) { + auto thread_holder = AcquireAccessThread(); + + // Hold current snapshot in this thread + auto holder = version_controller_.GetLocalSnapshotHolder(); + + Skiplist* skiplist = nullptr; + auto ret = lookupKey(collection, RecordType::SortedHeader); + if (ret.s != Status::Ok) { + return ret.s == Status::Outdated ? Status::NotFound : ret.s; + } + + kvdk_assert(ret.entry.GetIndexType() == PointerType::Skiplist, + "pointer type of skiplist in hash entry should be skiplist"); + skiplist = ret.entry.GetIndex().skiplist; + + assert(skiplist); + return skiplist->Get(user_key, value); +} + +Status KVEngine::SortedPut(const StringView collection, + const StringView user_key, const StringView value) { + auto thread_holder = AcquireAccessThread(); + + auto snapshot_holder = version_controller_.GetLocalSnapshotHolder(); + + Skiplist* skiplist = nullptr; + + auto ret = lookupKey(collection, RecordType::SortedHeader); + if (ret.s != Status::Ok) { + return ret.s == Status::Outdated ? Status::NotFound : ret.s; + } + + kvdk_assert(ret.entry.GetIndexType() == PointerType::Skiplist, + "pointer type of skiplist in hash entry should be skiplist"); + skiplist = ret.entry.GetIndex().skiplist; + return sortedPutImpl(skiplist, user_key, value); +} + +Status KVEngine::SortedDelete(const StringView collection, + const StringView user_key) { + auto thread_holder = AcquireAccessThread(); + + // Hold current snapshot in this thread + auto holder = version_controller_.GetLocalSnapshotHolder(); + + Skiplist* skiplist = nullptr; + auto ret = lookupKey(collection, RecordType::SortedHeader); + if (ret.s != Status::Ok) { + return (ret.s == Status::Outdated || ret.s == Status::NotFound) ? Status::Ok + : ret.s; + } + + kvdk_assert(ret.entry.GetIndexType() == PointerType::Skiplist, + "pointer type of skiplist in hash entry should be skiplist"); + skiplist = ret.entry.GetIndex().skiplist; + + return sortedDeleteImpl(skiplist, user_key); +} + +SortedIterator* KVEngine::SortedIteratorCreate(const StringView collection, + Snapshot* snapshot, Status* s) { + Skiplist* skiplist; + bool create_snapshot = snapshot == nullptr; + if (create_snapshot) { + snapshot = GetSnapshot(false); + } + // find collection + auto res = lookupKey(collection, RecordType::SortedHeader); + if (s != nullptr) { + *s = (res.s == Status::Outdated) ? Status::NotFound : res.s; + } + if (res.s == Status::Ok) { + skiplist = res.entry_ptr->GetIndex().skiplist; + return new SortedIteratorImpl(skiplist, kv_allocator_.get(), + static_cast(snapshot), + create_snapshot); + } else { + if (create_snapshot) { + ReleaseSnapshot(snapshot); + } + return nullptr; + } +} + +void KVEngine::SortedIteratorRelease(SortedIterator* sorted_iterator) { + if (sorted_iterator == nullptr) { + GlobalLogger.Info("pass a nullptr in KVEngine::SortedIteratorRelease!\n"); + return; + } + SortedIteratorImpl* iter = static_cast(sorted_iterator); + if (iter->own_snapshot_) { + ReleaseSnapshot(iter->snapshot_); + } + delete iter; +} + +Status KVEngine::sortedDeleteImpl(Skiplist* skiplist, + const StringView& user_key) { + std::string collection_key(skiplist->InternalKey(user_key)); + if (!checkKeySize(collection_key)) { + return Status::InvalidDataSize; + } + + auto ul = hash_table_->AcquireLock(collection_key); + TimestampType new_ts = version_controller_.GetCurrentTimestamp(); + + auto ret = skiplist->Delete(user_key, new_ts); + + if (ret.existing_record && ret.write_record && skiplist->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + skiplist->ReleaseCleaningLock(); + } + tryCleanCachedOutdatedRecord(); + + return ret.s; +} + +Status KVEngine::sortedPutImpl(Skiplist* skiplist, const StringView& user_key, + const StringView& value) { + std::string collection_key(skiplist->InternalKey(user_key)); + if (!checkKeySize(collection_key) || !checkValueSize(value)) { + return Status::InvalidDataSize; + } + + auto ul = hash_table_->AcquireLock(collection_key); + TimestampType new_ts = version_controller_.GetCurrentTimestamp(); + auto ret = skiplist->Put(user_key, value, new_ts); + + // Collect outdated version records + if (ret.existing_record && skiplist->TryCleaningLock()) { + removeAndCacheOutdatedVersion(ret.write_record); + skiplist->ReleaseCleaningLock(); + } + tryCleanCachedOutdatedRecord(); + + return ret.s; +} + +Status KVEngine::sortedWritePrepare(SortedWriteArgs& args, TimestampType ts) { + return args.skiplist->PrepareWrite(args, ts); +} + +Status KVEngine::sortedWrite(SortedWriteArgs& args) { + // Notice Jiayu: we do not handle delay free here as is no longer need soon + return args.skiplist->Write(args).s; +} + +Status KVEngine::sortedWritePublish(SortedWriteArgs const&) { + return Status::Ok; +} +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/kv_engine_string.cpp b/volatile/engine/kv_engine_string.cpp new file mode 100644 index 00000000..25074985 --- /dev/null +++ b/volatile/engine/kv_engine_string.cpp @@ -0,0 +1,291 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "kv_engine.hpp" +#include "utils/sync_point.hpp" + +namespace KVDK_NAMESPACE { + +Status KVEngine::Modify(const StringView key, ModifyFunc modify_func, + void* modify_args, const WriteOptions& write_options) { + int64_t base_time = TimeUtils::millisecond_time(); + if (!TimeUtils::CheckTTL(write_options.ttl_time, base_time)) { + return Status::InvalidArgument; + } + + auto thread_holder = AcquireAccessThread(); + + auto ul = hash_table_->AcquireLock(key); + auto holder = version_controller_.GetLocalSnapshotHolder(); + TimestampType new_ts = holder.Timestamp(); + auto lookup_result = lookupKey(key, RecordType::String); + + StringRecord* existing_record = nullptr; + std::string existing_value; + std::string new_value; + // push it into cleaner + if (lookup_result.s == Status::Ok) { + existing_record = lookup_result.entry.GetIndex().string_record; + existing_value.assign(existing_record->Value().data(), + existing_record->Value().size()); + } else if (lookup_result.s == Status::Outdated) { + existing_record = lookup_result.entry.GetIndex().string_record; + } else if (lookup_result.s == Status::NotFound) { + // nothing todo + } else { + return lookup_result.s; + } + + auto modify_operation = + modify_func(lookup_result.s == Status::Ok ? &existing_value : nullptr, + &new_value, modify_args); + switch (modify_operation) { + case ModifyOperation::Write: { + if (!checkValueSize(new_value)) { + return Status::InvalidDataSize; + } + + ExpireTimeType expired_time = + lookup_result.s == Status::Ok && !write_options.update_ttl + ? existing_record->GetExpireTime() + : TimeUtils::TTLToExpireTime(write_options.ttl_time, base_time); + + SpaceEntry space_entry = + kv_allocator_->Allocate(StringRecord::RecordSize(key, new_value)); + if (space_entry.size == 0) { + return Status::MemoryOverflow; + } + + StringRecord* new_record = + kv_allocator_->offset2addr_checked(space_entry.offset); + StringRecord::ConstructStringRecord( + new_record, space_entry.size, new_ts, RecordType::String, + RecordStatus::Normal, + existing_record == nullptr + ? kNullMemoryOffset + : kv_allocator_->addr2offset_checked(existing_record), + key, new_value, expired_time); + insertKeyOrElem(lookup_result, RecordType::String, RecordStatus::Normal, + new_record); + break; + } + case ModifyOperation::Delete: { + if (lookup_result.s == Status::Ok) { + SpaceEntry space_entry = + kv_allocator_->Allocate(StringRecord::RecordSize(key, "")); + if (space_entry.size == 0) { + return Status::MemoryOverflow; + } + + void* data_ptr = kv_allocator_->offset2addr_checked(space_entry.offset); + StringRecord::ConstructStringRecord( + data_ptr, space_entry.size, new_ts, RecordType::String, + RecordStatus::Outdated, + kv_allocator_->addr2offset_checked(existing_record), key, ""); + insertKeyOrElem(lookup_result, RecordType::String, + RecordStatus::Outdated, data_ptr); + break; + } + case ModifyOperation::Abort: { + return Status::Abort; + } + case ModifyOperation::Noop: { + return Status::Ok; + } + } + } + + return Status::Ok; +} + +Status KVEngine::Put(const StringView key, const StringView value, + const WriteOptions& options) { + auto thread_holder = AcquireAccessThread(); + + if (!checkKeySize(key) || !checkValueSize(value)) { + return Status::InvalidDataSize; + } + + return stringPutImpl(key, value, options); +} + +Status KVEngine::Get(const StringView key, std::string* value) { + auto thread_holder = AcquireAccessThread(); + + if (!checkKeySize(key)) { + return Status::InvalidDataSize; + } + auto holder = version_controller_.GetLocalSnapshotHolder(); + auto ret = lookupKey(key, RecordType::String); + if (ret.s == Status::Ok) { + StringRecord* string_record = ret.entry.GetIndex().string_record; + kvdk_assert(string_record->GetRecordType() == RecordType::String && + string_record->GetRecordStatus() != RecordStatus::Outdated, + "Got wrong data type in string get"); + kvdk_assert(string_record->ValidOrDirty(), "Corrupted data in string get"); + value->assign(string_record->Value().data(), string_record->Value().size()); + return Status::Ok; + } else { + return ret.s == Status::Outdated ? Status::NotFound : ret.s; + } +} + +Status KVEngine::Delete(const StringView key) { + auto thread_holder = AcquireAccessThread(); + + if (!checkKeySize(key)) { + return Status::InvalidDataSize; + } + + return stringDeleteImpl(key); +} + +Status KVEngine::stringDeleteImpl(const StringView& key) { + auto ul = hash_table_->AcquireLock(key); + auto holder = version_controller_.GetLocalSnapshotHolder(); + TimestampType new_ts = holder.Timestamp(); + + auto lookup_result = lookupKey(key, RecordType::String); + if (lookup_result.s == Status::Ok) { + // We only write delete record if key exist + auto request_size = key.size() + sizeof(StringRecord); + SpaceEntry space_entry = kv_allocator_->Allocate(request_size); + if (space_entry.size == 0) { + return Status::MemoryOverflow; + } + + StringRecord* data_ptr = + kv_allocator_->offset2addr_checked(space_entry.offset); + StringRecord::ConstructStringRecord( + data_ptr, space_entry.size, new_ts, RecordType::String, + RecordStatus::Outdated, + kv_allocator_->addr2offset_checked( + lookup_result.entry.GetIndex().string_record), + key, ""); + insertKeyOrElem(lookup_result, RecordType::String, RecordStatus::Outdated, + data_ptr); + + removeAndCacheOutdatedVersion(data_ptr); + } + tryCleanCachedOutdatedRecord(); + + return (lookup_result.s == Status::NotFound || + lookup_result.s == Status::Outdated) + ? Status::Ok + : lookup_result.s; +} + +Status KVEngine::stringPutImpl(const StringView& key, const StringView& value, + const WriteOptions& write_options) { + int64_t base_time = TimeUtils::millisecond_time(); + if (!TimeUtils::CheckTTL(write_options.ttl_time, base_time)) { + return Status::InvalidArgument; + } + + TEST_SYNC_POINT("KVEngine::stringPutImpl::BeforeLock"); + auto ul = hash_table_->AcquireLock(key); + auto holder = version_controller_.GetLocalSnapshotHolder(); + TimestampType new_ts = holder.Timestamp(); + + // Lookup key in hashtable + auto lookup_result = lookupKey(key, RecordType::String); + if (lookup_result.s == Status::MemoryOverflow || + lookup_result.s == Status::WrongType) { + return lookup_result.s; + } + + kvdk_assert(lookup_result.s == Status::NotFound || + lookup_result.s == Status::Ok || + lookup_result.s == Status::Outdated, + "Wrong return status in lookupKey in stringPutImpl"); + StringRecord* existing_record = + lookup_result.s == Status::NotFound + ? nullptr + : lookup_result.entry.GetIndex().string_record; + kvdk_assert(!existing_record || new_ts > existing_record->GetTimestamp(), + "existing record has newer timestamp or wrong return status in " + "string set"); + ExpireTimeType expired_time = + lookup_result.s == Status::Ok && !write_options.update_ttl + ? existing_record->GetExpireTime() + : TimeUtils::TTLToExpireTime(write_options.ttl_time, base_time); + + // Save key-value pair + SpaceEntry space_entry = + kv_allocator_->Allocate(StringRecord::RecordSize(key, value)); + if (space_entry.size == 0) { + return Status::MemoryOverflow; + } + + StringRecord* new_record = + kv_allocator_->offset2addr_checked(space_entry.offset); + StringRecord::ConstructStringRecord( + new_record, space_entry.size, new_ts, RecordType::String, + RecordStatus::Normal, kv_allocator_->addr2offset(existing_record), key, + value, expired_time); + + insertKeyOrElem(lookup_result, RecordType::String, RecordStatus::Normal, + new_record); + + if (existing_record) { + removeAndCacheOutdatedVersion(new_record); + } + tryCleanCachedOutdatedRecord(); + + return Status::Ok; +} + +Status KVEngine::stringWritePrepare(StringWriteArgs& args, TimestampType ts) { + args.res = lookupKey(args.key, RecordType::String); + if (args.res.s != Status::Ok && args.res.s != Status::NotFound && + args.res.s != Status::Outdated) { + return args.res.s; + } + args.ts = ts; + if (args.op == WriteOp::Delete && args.res.s != Status::Ok) { + return Status::Ok; + } + args.space = + kv_allocator_->Allocate(StringRecord::RecordSize(args.key, args.value)); + if (args.space.size == 0) { + return Status::MemoryOverflow; + } + return Status::Ok; +} + +Status KVEngine::stringWrite(StringWriteArgs& args) { + RecordStatus record_status = + args.op == WriteOp::Put ? RecordStatus::Normal : RecordStatus::Outdated; + void* new_addr = kv_allocator_->offset2addr_checked(args.space.offset); + MemoryOffsetType old_off; + if (args.res.s == Status::NotFound) { + kvdk_assert(args.op == WriteOp::Put, ""); + old_off = kNullMemoryOffset; + } else { + kvdk_assert(args.res.s == Status::Ok || args.res.s == Status::Outdated, ""); + old_off = kv_allocator_->addr2offset_checked( + args.res.entry.GetIndex().string_record); + } + args.new_rec = StringRecord::ConstructStringRecord( + new_addr, args.space.size, args.ts, RecordType::String, record_status, + old_off, args.key, args.value); + return Status::Ok; +} + +Status KVEngine::stringWritePublish(StringWriteArgs const& args) { + RecordStatus record_status = + args.op == WriteOp::Put ? RecordStatus::Normal : RecordStatus::Outdated; + insertKeyOrElem(args.res, RecordType::String, record_status, + const_cast(args.new_rec)); + return Status::Ok; +} + +Status KVEngine::stringRollback(TimestampType, + BatchWriteLog::StringLogEntry const& log) { + static_cast(kv_allocator_->offset2addr_checked(log.offset)) + ->Destroy(); + return Status::Ok; +} + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/list_collection/iterator.hpp b/volatile/engine/list_collection/iterator.hpp new file mode 100644 index 00000000..98b4aa2b --- /dev/null +++ b/volatile/engine/list_collection/iterator.hpp @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#pragma once + +#include "../version/version_controller.hpp" +#include "kvdk/volatile/engine.hpp" +#include "kvdk/volatile/iterator.hpp" +#include "list.hpp" + +namespace KVDK_NAMESPACE { +class ListIteratorImpl final : public ListIterator { + public: + ListIteratorImpl(List* list, const SnapshotImpl* snapshot, bool own_snapshot) + : list_(list), + snapshot_(snapshot), + own_snapshot_(own_snapshot), + dl_iter_(&list->dl_list_, list->kv_allocator_, snapshot) {} + + void Seek(long index) final { + if (index < 0) { + SeekToLast(); + long cur = -1; + while (cur-- > index && Valid()) { + Prev(); + } + } else { + SeekToFirst(); + long cur = 0; + while (cur++ < index && Valid()) { + Next(); + } + } + } + + void SeekToFirst() final { dl_iter_.SeekToFirst(); } + + void SeekToLast() final { dl_iter_.SeekToLast(); } + + void SeekToFirst(StringView elem) final { + SeekToFirst(); + Next(elem); + } + + void SeekToLast(StringView elem) final { + SeekToLast(); + Prev(elem); + } + + bool Valid() const final { return dl_iter_.Valid(); } + + void Next() final { dl_iter_.Next(); } + + void Prev() final { dl_iter_.Prev(); } + + void Next(StringView elem) final { + while (Valid()) { + Next(); + if (!Valid() || equal_string_view(elem, dl_iter_.Value())) { + break; + } + } + } + + void Prev(StringView elem) final { + while (Valid()) { + Prev(); + if (!Valid() || equal_string_view(elem, dl_iter_.Value())) { + break; + } + } + } + + std::string Value() const final { + if (!Valid()) { + kvdk_assert(false, "Accessing data with invalid ListIterator!"); + return std::string{}; + } + auto sw = dl_iter_.Value(); + return std::string{sw.data(), sw.size()}; + } + + private: + friend KVEngine; + + List* list_; + const SnapshotImpl* snapshot_; + bool own_snapshot_; + DLListDataIterator dl_iter_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/list_collection/list.cpp b/volatile/engine/list_collection/list.cpp new file mode 100644 index 00000000..ffb7cd1d --- /dev/null +++ b/volatile/engine/list_collection/list.cpp @@ -0,0 +1,464 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "list.hpp" + +namespace KVDK_NAMESPACE { +List::WriteResult List::SetExpireTime(ExpireTimeType expired_time, + TimestampType timestamp) { + WriteResult ret; + DLRecord* header = HeaderRecord(); + SpaceEntry space = kv_allocator_->Allocate( + DLRecord::RecordSize(header->Key(), header->Value())); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space.offset), space.size, timestamp, + RecordType::ListHeader, RecordStatus::Normal, + kv_allocator_->addr2offset_checked(header), header->prev, header->next, + header->Key(), header->Value(), expired_time); + bool success = dl_list_.Replace(header, data_record); + kvdk_assert(success, "existing header should be linked on its list"); + ret.existing_record = header; + ret.write_record = data_record; + return ret; +} + +List::WriteResult List::PushFront(const StringView& elem, TimestampType ts) { + WriteResult ret; + std::string internal_key(InternalKey("")); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, elem)); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + + DLList::WriteArgs args(internal_key, elem, RecordType::ListElem, + RecordStatus::Normal, ts, space); + ret.s = dl_list_.PushFront(args); + kvdk_assert(ret.s == Status::Ok, "Push front should alwasy success"); + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + live_records_.push_front(ret.write_record); + return ret; +} + +List::WriteResult List::PushBack(const StringView& elem, TimestampType ts) { + WriteResult ret; + std::string internal_key(InternalKey("")); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, elem)); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + + DLList::WriteArgs args(internal_key, elem, RecordType::ListElem, + RecordStatus::Normal, ts, space); + ret.s = dl_list_.PushBack(args); + kvdk_assert(ret.s == Status::Ok, "Push front should alwasy success"); + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + live_records_.push_back(ret.write_record); + return ret; +} + +List::WriteResult List::PopFront(TimestampType ts) { + WriteResult ret; + if (Size() == 0) { + ret.s = Status::NotFound; + } else { + DLRecord* record = live_records_.front(); + kvdk_assert(record->GetRecordStatus() == RecordStatus::Normal, ""); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(record->Key(), "")); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLList::WriteArgs args(record->Key(), "", RecordType::ListElem, + RecordStatus::Outdated, ts, space); + while ((ret.s = dl_list_.Update(args, record)) != Status::Ok) { + kvdk_assert(ret.s == Status::Fail, ""); + }; + ret.write_record = + kv_allocator_->offset2addr_checked(space.offset); + ret.existing_record = record; + live_records_.pop_front(); + } + return ret; +} + +List::WriteResult List::PopBack(TimestampType ts) { + WriteResult ret; + if (Size() == 0) { + ret.s = Status::NotFound; + } else { + DLRecord* record = live_records_.back(); + kvdk_assert(record->GetRecordStatus() == RecordStatus::Normal, ""); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(record->Key(), "")); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLList::WriteArgs args(record->Key(), "", RecordType::ListElem, + RecordStatus::Outdated, ts, space); + while ((ret.s = dl_list_.Update(args, record)) != Status::Ok) { + kvdk_assert(ret.s == Status::Fail, ""); + }; + ret.write_record = + kv_allocator_->offset2addr_checked(space.offset); + ret.existing_record = record; + live_records_.pop_back(); + } + return ret; +} + +List::WriteResult List::InsertBefore(const StringView& elem, + const StringView& existing_elem, + TimestampType ts) { + WriteResult ret; + auto iter = findLiveRecord(existing_elem); + if (iter == live_records_.end()) { + ret.s = Status::NotFound; + } else { + std::string internal_key(InternalKey("")); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, elem)); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLList::WriteArgs args(internal_key, elem, RecordType::ListElem, + RecordStatus::Normal, ts, space); + ret.s = dl_list_.InsertBefore(args, *iter); + kvdk_assert(ret.s == Status::Ok, + "the whole list is locked, so the insertion must be success"); + ret.write_record = + kv_allocator_->offset2addr_checked(space.offset); + live_records_.insert(iter, ret.write_record); + } + return ret; +} + +List::WriteResult List::InsertAfter(const StringView& elem, + const StringView& existing_elem, + TimestampType ts) { + WriteResult ret; + auto iter = findLiveRecord(existing_elem); + if (iter == live_records_.end()) { + ret.s = Status::NotFound; + } else { + std::string internal_key(InternalKey("")); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, elem)); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLList::WriteArgs args(internal_key, elem, RecordType::ListElem, + RecordStatus::Normal, ts, space); + ret.s = dl_list_.InsertAfter(args, *iter); + kvdk_assert(ret.s == Status::Ok, + "the whole list is locked, so the insertion must be success"); + ret.write_record = + kv_allocator_->offset2addr_checked(space.offset); + live_records_.insert(iter + 1, ret.write_record); + } + return ret; +} + +List::WriteResult List::InsertAt(const StringView& elem, long index, + TimestampType ts) { + WriteResult ret; + size_t required_size = index < 0 ? std::abs(index) - 1 : index; + if (required_size > Size()) { + ret.s = Status::NotFound; + return ret; + } + if (index < 0) { + index = live_records_.size() + index; + } + auto iter = live_records_.begin() + index; + DLRecord* next = *iter; + std::string internal_key(InternalKey("")); + kvdk_assert(next->GetRecordType() == RecordType::ListElem && + next->GetRecordStatus() == RecordStatus::Normal, + ""); + + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, elem)); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLList::WriteArgs args(internal_key, elem, RecordType::ListElem, + RecordStatus::Normal, ts, space); + ret.s = dl_list_.InsertBefore(args, next); + kvdk_assert(ret.s == Status::Ok, + "the whole list is locked, so the insertion must be success"); + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + live_records_.insert(iter, ret.write_record); + return ret; +} + +List::WriteResult List::Erase(long index, TimestampType ts) { + WriteResult ret; + size_t required_size = index < 0 ? std::abs(index) - 1 : index; + if (required_size > Size()) { + ret.s = Status::NotFound; + return ret; + } + if (index < 0) { + index = live_records_.size() + index; + } + auto iter = live_records_.begin() + index; + DLRecord* record = *iter; + kvdk_assert(record->GetRecordType() == RecordType::ListElem && + record->GetRecordStatus() == RecordStatus::Normal, + ""); + + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(record->Key(), "")); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLList::WriteArgs args(record->Key(), "", RecordType::ListElem, + RecordStatus::Outdated, ts, space); + while ((ret.s = dl_list_.Update(args, record)) != Status::Ok) { + kvdk_assert(ret.s == Status::Fail, ""); + }; + ret.existing_record = record; + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + live_records_.erase(iter); + return ret; +} + +Status List::Front(std::string* elem) { + if (Size() == 0) { + return Status::NotFound; + } + DLRecord* record = live_records_.front(); + StringView sw = record->Value(); + elem->assign(sw.data(), sw.size()); + return Status::Ok; +} + +Status List::Back(std::string* elem) { + if (Size() == 0) { + return Status::NotFound; + } + DLRecord* record = live_records_.back(); + StringView sw = record->Value(); + elem->assign(sw.data(), sw.size()); + return Status::Ok; +} + +List::WriteResult List::Update(long index, const StringView& elem, + TimestampType ts) { + WriteResult ret; + size_t required_size = index < 0 ? std::abs(index) - 1 : index; + if (required_size > Size()) { + ret.s = Status::NotFound; + return ret; + } + if (index < 0) { + index = live_records_.size() + index; + } + auto iter = live_records_.begin() + index; + DLRecord* record = *iter; + kvdk_assert(record->GetRecordType() == RecordType::ListElem && + record->GetRecordStatus() == RecordStatus::Normal, + ""); + std::string internal_key(InternalKey("")); + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, elem)); + if (space.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLList::WriteArgs args(internal_key, elem, RecordType::ListElem, + RecordStatus::Normal, ts, space); + + while ((ret.s = dl_list_.Update(args, record)) != Status::Ok) { + kvdk_assert(ret.s == Status::Fail, ""); + } + ret.existing_record = record; + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + *iter = ret.write_record; + return ret; +} + +List::PushNArgs List::PreparePushN(ListPos pos, + const std::vector& elems, + TimestampType ts) { + PushNArgs args; + args.pos = pos; + args.ts = ts; + if (elems.size() > 0) { + std::string internal_key(InternalKey("")); + for (auto& elem : elems) { + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(internal_key, elem)); + if (space.size == 0) { + GlobalLogger.Error("Try allocate %lu error\n", + DLRecord::RecordSize(internal_key, elem)); + for (auto& sp : args.spaces) { + kv_allocator_->Free(sp); + } + args.s = Status::MemoryOverflow; + break; + } + args.spaces.emplace_back(space); + } + args.elems = elems; + } + args.s = Status::Ok; + return args; +} + +List::PopNArgs List::PreparePopN(ListPos pos, size_t n, TimestampType ts, + std::vector* elems) { + size_t nn = n; + PopNArgs args; + args.timestamp_ = ts; + if (Size() > 0) { + auto iter = + pos == ListPos::Front ? live_records_.begin() : live_records_.end() - 1; + while (nn > 0) { + DLRecord* record = *iter; + SpaceEntry space = + kv_allocator_->Allocate(DLRecord::RecordSize(record->Key(), "")); + if (space.size == 0) { + for (auto& sp : args.spaces) { + kv_allocator_->Free(sp); + } + args.s = Status::MemoryOverflow; + return args; + } + + if (elems) { + StringView sw = record->Value(); + elems->emplace_back(sw.data(), sw.size()); + } + args.spaces.emplace_back(space); + args.to_pop_.emplace_back(iter); + nn--; + if (pos == ListPos::Front) { + iter++; + if (iter == live_records_.end()) { + break; + } + } else { + if (iter == live_records_.begin()) { + break; + } + iter--; + } + } + args.s = Status::Ok; + } + return args; +} + +Status List::PushN(const List::PushNArgs& args) { + if (args.s != Status::Ok) { + return args.s; + } + std::string internal_key(InternalKey("")); + kvdk_assert(args.elems.size() == args.spaces.size(), ""); + for (size_t i = 0; i < args.elems.size(); i++) { + DLList::WriteArgs wa(internal_key, args.elems[i], RecordType::ListElem, + RecordStatus::Normal, args.ts, args.spaces[i]); + Status s; + if (args.pos == ListPos::Front) { + s = dl_list_.PushFront(wa); + live_records_.push_front( + kv_allocator_->offset2addr_checked(wa.space.offset)); + } else { + s = dl_list_.PushBack(wa); + live_records_.push_back( + kv_allocator_->offset2addr_checked(wa.space.offset)); + } + kvdk_assert(s == Status::Ok, "Push back/front should always success"); + TEST_CRASH_POINT("List::PushN", ""); + } + return Status::Ok; +} + +Status List::PopN(const List::PopNArgs& args) { + if (args.s != Status::Ok) { + return args.s; + } + std::string internal_key(InternalKey("")); + kvdk_assert(args.spaces.size() == args.to_pop_.size(), ""); + for (size_t i = 0; i < args.to_pop_.size(); i++) { + DLList::WriteArgs wa(internal_key, "", RecordType::ListElem, + RecordStatus::Outdated, args.timestamp_, + args.spaces[i]); + Status s; + while ((s = dl_list_.Update(wa, *args.to_pop_[i])) != Status::Ok) { + kvdk_assert(s == Status::Fail, ""); + } + live_records_.erase(args.to_pop_[i]); + TEST_CRASH_POINT("List::PopN", ""); + } + return Status::Ok; +} + +void List::Destroy() { + std::vector to_free; + DLRecord* header = HeaderRecord(); + if (header) { + DLRecord* to_destroy = nullptr; + do { + to_destroy = kv_allocator_->offset2addr_checked(header->next); + if (dl_list_.Remove(to_destroy)) { + to_destroy->Destroy(); + to_free.emplace_back(kv_allocator_->addr2offset_checked(to_destroy), + to_destroy->GetRecordSize()); + if (to_free.size() > kMaxCachedOldRecords) { + kv_allocator_->BatchFree(to_free); + to_free.clear(); + } + } + } while (to_destroy != header); + } + kv_allocator_->BatchFree(to_free); +} + +void List::DestroyAll() { + std::vector to_free; + DLRecord* header = HeaderRecord(); + if (header) { + DLRecord* to_destroy = nullptr; + do { + to_destroy = kv_allocator_->offset2addr_checked(header->next); + if (dl_list_.Remove(to_destroy)) { + auto old_record = + kv_allocator_->offset2addr(to_destroy->old_version); + while (old_record) { + auto old_version = old_record->old_version; + old_record->Destroy(); + to_free.emplace_back(kv_allocator_->addr2offset_checked(old_record), + old_record->GetRecordSize()); + old_record = kv_allocator_->offset2addr(old_version); + } + + to_destroy->Destroy(); + to_free.emplace_back(kv_allocator_->addr2offset_checked(to_destroy), + to_destroy->GetRecordSize()); + if (to_free.size() > kMaxCachedOldRecords) { + kv_allocator_->BatchFree(to_free); + to_free.clear(); + } + } + } while (to_destroy != header); + } + kv_allocator_->BatchFree(to_free); +} +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/list_collection/list.hpp b/volatile/engine/list_collection/list.hpp new file mode 100644 index 00000000..d3dd5402 --- /dev/null +++ b/volatile/engine/list_collection/list.hpp @@ -0,0 +1,171 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include "../dl_list.hpp" +#include "../logger.hpp" +#include "kvdk/volatile/types.hpp" + +namespace KVDK_NAMESPACE { +class ListIteratorImpl; + +class List : public Collection { + public: + List(DLRecord* header, const StringView& name, CollectionIDType id, + Allocator* kv_allocator, LockTable* lock_table) + : Collection(name, id), + list_lock_(), + dl_list_(header, kv_allocator, lock_table), + kv_allocator_(kv_allocator), + live_records_() {} + + struct WriteResult { + Status s = Status::Ok; + DLRecord* write_record = nullptr; + DLRecord* existing_record = nullptr; + }; + + struct PopNArgs { + public: + Status s{Status::InvalidArgument}; + std::vector spaces{}; + + private: + friend List; + std::vector::iterator> to_pop_{}; + TimestampType timestamp_; + }; + + struct PushNArgs { + public: + Status s{Status::InvalidArgument}; + std::vector spaces; + std::vector elems; + ListPos pos; + TimestampType ts; + }; + + const DLRecord* HeaderRecord() const { return dl_list_.Header(); } + + DLRecord* HeaderRecord() { return dl_list_.Header(); } + + ExpireTimeType GetExpireTime() const final { + return HeaderRecord()->GetExpireTime(); + } + + TimestampType GetTimeStamp() const { return HeaderRecord()->GetTimestamp(); } + + bool HasExpired() const final { return HeaderRecord()->HasExpired(); } + + WriteResult SetExpireTime(ExpireTimeType expired_time, + TimestampType timestamp); + + WriteResult PushFront(const StringView& elem, TimestampType ts); + + WriteResult PushBack(const StringView& elem, TimestampType ts); + + WriteResult PopFront(TimestampType ts); + + WriteResult PopBack(TimestampType ts); + + WriteResult InsertBefore(const StringView& elem, + const StringView& existing_elem, TimestampType ts); + + WriteResult InsertAfter(const StringView& elem, + const StringView& existing_elem, TimestampType ts); + + WriteResult InsertAt(const StringView& elem, long index, TimestampType ts); + + WriteResult Erase(long index, TimestampType ts); + + Status Front(std::string* elem); + + Status Back(std::string* elem); + + bool Replace(DLRecord* old_record, DLRecord* new_record) { + return dl_list_.Replace(old_record, new_record); + } + + WriteResult Update(long index, const StringView& elem, TimestampType ts); + + void AddLiveRecord(DLRecord* elem, ListPos pos) { + if (pos == ListPos::Front) { + live_records_.push_front(elem); + } else { + live_records_.push_back(elem); + } + } + + size_t Size() { return live_records_.size(); } + + std::unique_lock AcquireLock() { + return std::unique_lock(list_lock_); + } + + DLList* GetDLList() { return &dl_list_; } + + void DestroyAll(); + + void Destroy(); + + PushNArgs PreparePushN(ListPos pos, const std::vector& elems, + TimestampType ts); + + PopNArgs PreparePopN(ListPos pos, size_t n, TimestampType ts, + std::vector* elems); + + Status PushN(const PushNArgs& args); + + Status PopN(const PopNArgs& args); + + bool TryCleaningLock() { return cleaning_lock_.try_lock(); } + + void ReleaseCleaningLock() { cleaning_lock_.unlock(); } + + static CollectionIDType FetchID(DLRecord* record) { + assert(record != nullptr); + switch (record->GetRecordType()) { + case RecordType::ListElem: + return ExtractID(record->Key()); + case RecordType::ListHeader: + return DecodeID(record->Value()); + default: + GlobalLogger.Error("Wrong record type %u in ListID", + record->GetRecordType()); + kvdk_assert(false, "Wrong type in ListID"); + return 0; + } + } + + static bool MatchType(const DLRecord* record) { + RecordType type = record->GetRecordType(); + return type == RecordType::ListElem || type == RecordType::ListHeader; + } + + private: + // find the first live record of elem + std::deque::iterator findLiveRecord(StringView elem) { + auto iter = live_records_.begin(); + while (iter != live_records_.end()) { + if (equal_string_view((*iter)->Value(), elem)) { + return iter; + } + ++iter; + } + return live_records_.end(); + } + + friend ListIteratorImpl; + std::recursive_mutex list_lock_; + DLList dl_list_; + Allocator* kv_allocator_; + std::atomic size_; + // to avoid illegal access caused by cleaning skiplist by multi-thread + SpinMutex cleaning_lock_; + // we keep outdated records on list to support mvcc, so we track live records + // in a deque to support fast write operations + std::deque live_records_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/lock_table.hpp b/volatile/engine/lock_table.hpp new file mode 100644 index 00000000..453bac1a --- /dev/null +++ b/volatile/engine/lock_table.hpp @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#pragma once + +#include +#include + +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { + +// This class manages a table of locks, which indexed by hash value. It is able +// to lock multiple locks in the table by sequence to avoid dead lock +class LockTable { + public: + using HashValueType = std::uint64_t; + using MutexType = SpinMutex; + using ULockType = std::unique_lock; + using MultiGuardType = std::vector; + + LockTable(size_t n) : mutexes_{n} {} + + std::unique_lock AcquireLock(HashValueType hash) { + return std::unique_lock(*Mutex(hash)); + } + + void Lock(HashValueType hash) { Mutex(hash)->lock(); } + + void Unlock(HashValueType hash) { Mutex(hash)->unlock(); } + + void MultiLock(std::vector const& hashes) { + auto sorted = rearrange(hashes); + for (HashValueType hash : sorted) { + Lock(hash); + } + } + + void MultiLock(std::initializer_list hashes) { + MultiLock(std::vector{hashes}); + } + + void MultiUnlock(std::vector const& hashes) { + auto sorted = rearrange(hashes); + for (HashValueType hash : sorted) { + Unlock(hash); + } + } + + void MultiUnlock(std::initializer_list hashes) { + MultiUnlock(std::vector{hashes}); + } + + MultiGuardType MultiGuard(std::vector const& hashes) { + auto sorted = rearrange(hashes); + MultiGuardType guard; + for (HashValueType hash : sorted) { + guard.emplace_back(*Mutex(hash)); + } + return guard; + } + + MultiGuardType MultiGuard(std::initializer_list hashes) { + return MultiGuard(std::vector{hashes}); + } + + MutexType* Mutex(HashValueType hash) { + return &mutexes_[hash % mutexes_.size()]; + } + + private: + std::vector rearrange( + std::vector const& hashes) { + std::vector ret{hashes}; + size_t N = mutexes_.size(); + for (auto& hash : ret) { + hash %= N; + } + std::sort(ret.begin(), ret.end()); + size_t new_sz = std::unique(ret.begin(), ret.end()) - ret.begin(); + ret.resize(new_sz); + return ret; + } + + std::vector mutexes_; +}; + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/logger.cpp b/volatile/engine/logger.cpp new file mode 100644 index 00000000..b0f6c9f6 --- /dev/null +++ b/volatile/engine/logger.cpp @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ +#include "logger.hpp" + +namespace KVDK_NAMESPACE { + +void Logger::Debug(const char* format, ...) { + if (level_ <= LogLevel::Debug) { + va_list args; + va_start(args, format); + Log("[DEBUG]", format, args); + va_end(args); + } +} + +void Logger::Info(const char* format, ...) { + if (level_ <= LogLevel::Info) { + va_list args; + va_start(args, format); + Log("[INFO]", format, args); + va_end(args); + } +} + +void Logger::Error(const char* format, ...) { + if (level_ <= LogLevel::Error) { + va_list args; + va_start(args, format); + Log("[ERROR]", format, args); + va_end(args); + } +} + +void Logger::Log(const char* log_type, const char* format, va_list& args) { + if (log_file_ != nullptr) { + std::lock_guard lg(mut_); + auto now = std::chrono::system_clock::now(); + auto duration = + std::chrono::duration_cast(now - start_ts_); + fprintf(log_file_, "%s time %ld ms: ", log_type, duration.count()); + vfprintf(log_file_, format, args); + fflush(log_file_); + fsync(fileno(log_file_)); + } +} + +void Logger::Init(FILE* fp, LogLevel level) { + log_file_ = fp; + level_ = level; + start_ts_ = std::chrono::system_clock::now(); +} + +Logger GlobalLogger; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/engine/logger.hpp b/volatile/engine/logger.hpp similarity index 94% rename from engine/logger.hpp rename to volatile/engine/logger.hpp index 6bf978a1..315f7d8d 100644 --- a/engine/logger.hpp +++ b/volatile/engine/logger.hpp @@ -11,7 +11,7 @@ #include #include "alias.hpp" -#include "kvdk/configs.hpp" +#include "kvdk/volatile/configs.hpp" #define DO_LOG 1 diff --git a/volatile/engine/macros.hpp b/volatile/engine/macros.hpp new file mode 100644 index 00000000..e6e73520 --- /dev/null +++ b/volatile/engine/macros.hpp @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#pragma once + +#include +#include +#include + +#define to_hex(x) \ + std::hex << std::setfill('0') << std::setw(sizeof(decltype(x)) * 2) << x \ + << std::dec + +#ifndef KVDK_DEBUG_LEVEL +#pragma GCC warning "KVDK_DEBUG_LEVEL not defined, defaulted to 0" +#define KVDK_DEBUG_LEVEL 0 +#endif + +#define kvdk_assert(cond, msg) \ + { \ + if (KVDK_DEBUG_LEVEL > 0 && !(cond)) { \ + throw std::runtime_error{__FILE__ ":" + std::to_string(__LINE__) + \ + ":\t" + std::string{msg}}; \ + } \ + } + +#ifdef __GNUC__ +#define KVDK_LIKELY(x) __builtin_expect(!!(x), 1) +#define KVDK_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else +#define KVDK_LIKELY(x) (x) +#define KVDK_UNLIKELY(x) (x) +#endif diff --git a/volatile/engine/snapshot.hpp b/volatile/engine/snapshot.hpp new file mode 100644 index 00000000..a16ce1c2 --- /dev/null +++ b/volatile/engine/snapshot.hpp @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { +struct Snapshot { + TimestampType timestamp; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/sorted_collection/iterator.hpp b/volatile/engine/sorted_collection/iterator.hpp new file mode 100644 index 00000000..477594f6 --- /dev/null +++ b/volatile/engine/sorted_collection/iterator.hpp @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include "../alias.hpp" +#include "skiplist.hpp" + +namespace KVDK_NAMESPACE { + +class KVEngine; + +class SortedIteratorImpl : public SortedIterator { + public: + SortedIteratorImpl(Skiplist* skiplist, const Allocator* kv_allocator, + const SnapshotImpl* snapshot, bool own_snapshot) + : skiplist_(skiplist), + snapshot_(snapshot), + own_snapshot_(own_snapshot), + dl_iter_(&skiplist->dl_list_, kv_allocator, snapshot) {} + + virtual ~SortedIteratorImpl() = default; + + virtual void Seek(const std::string& key) override { + assert(skiplist_); + Splice splice(skiplist_); + skiplist_->Seek(key, &splice); + dl_iter_.Locate(splice.next_data_record, true); + } + + virtual void SeekToFirst() override { dl_iter_.SeekToFirst(); } + + virtual void SeekToLast() override { dl_iter_.SeekToLast(); } + + virtual bool Valid() override { return dl_iter_.Valid(); } + + virtual void Next() override { dl_iter_.Next(); } + + virtual void Prev() override { dl_iter_.Prev(); } + + virtual std::string Key() override { + if (!Valid()) return ""; + return string_view_2_string(Skiplist::ExtractUserKey(dl_iter_.Key())); + } + + virtual std::string Value() override { + if (!Valid()) return ""; + return string_view_2_string(dl_iter_.Value()); + } + + private: + friend KVEngine; + + Skiplist* skiplist_; + const SnapshotImpl* snapshot_; + bool own_snapshot_; + DLListDataIterator dl_iter_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/sorted_collection/skiplist.cpp b/volatile/engine/sorted_collection/skiplist.cpp new file mode 100644 index 00000000..80035096 --- /dev/null +++ b/volatile/engine/sorted_collection/skiplist.cpp @@ -0,0 +1,950 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "skiplist.hpp" + +#include +#include + +#include "../kv_engine.hpp" +#include "../utils/codec.hpp" +#include "../utils/sync_point.hpp" +#include "../write_batch_impl.hpp" + +namespace KVDK_NAMESPACE { + +StringView SkiplistNode::UserKey() { return Skiplist::UserKey(this); } + +uint64_t SkiplistNode::SkiplistID() { return Skiplist::FetchID(this); } + +Skiplist::~Skiplist() { + destroyNodes(); + std::lock_guard lg_a(pending_delete_nodes_spin_); + for (SkiplistNode* node : pending_deletion_nodes_) { + SkiplistNode::DeleteNode(node, node_allocator_); + } + pending_deletion_nodes_.clear(); + std::lock_guard lg_b(obsolete_nodes_spin_); + for (SkiplistNode* node : obsolete_nodes_) { + SkiplistNode::DeleteNode(node, node_allocator_); + } + obsolete_nodes_.clear(); +} + +Skiplist::Skiplist(DLRecord* h, const std::string& name, CollectionIDType id, + Comparator comparator, Allocator* kv_allocator, + Allocator* node_allocator, HashTable* hash_table, + LockTable* lock_table, bool index_with_hashtable) + : Collection(name, id), + dl_list_(h, kv_allocator, lock_table), + size_(0), + comparator_(comparator), + kv_allocator_(kv_allocator), + node_allocator_(node_allocator), + hash_table_(hash_table), + record_locks_(lock_table), + index_with_hashtable_(index_with_hashtable) { + header_ = SkiplistNode::NewNode(name, h, kMaxHeight, node_allocator_); + + for (uint8_t i = 1; i <= kMaxHeight; i++) { + header_->RelaxedSetNext(i, nullptr); + } +}; + +Skiplist::WriteResult Skiplist::SetExpireTime(ExpireTimeType expired_time, + TimestampType timestamp) { + WriteResult ret; + DLRecord* header = HeaderRecord(); + auto request_size = + sizeof(DLRecord) + header->Key().size() + header->Value().size(); + SpaceEntry space_entry = kv_allocator_->Allocate(request_size); + if (space_entry.size == 0) { + ret.s = Status::MemoryOverflow; + return ret; + } + DLRecord* data_record = DLRecord::ConstructDLRecord( + kv_allocator_->offset2addr_checked(space_entry.offset), space_entry.size, + timestamp, RecordType::SortedHeader, RecordStatus::Normal, + kv_allocator_->addr2offset_checked(header), header->prev, header->next, + header->Key(), header->Value(), expired_time); + bool success = Skiplist::Replace(header, data_record, HeaderNode(), + kv_allocator_, record_locks_); + kvdk_assert(success, "existing header should be linked on its skiplist"); + ret.existing_record = header; + ret.dram_node = HeaderNode(); + ret.write_record = data_record; + return ret; +} + +void Skiplist::SeekNode(const StringView& key, SkiplistNode* start_node, + uint8_t start_height, uint8_t end_height, + Splice* result_splice) { + std::vector to_delete; + assert(start_node->height >= start_height && end_height >= 1); + SkiplistNode* prev = start_node; + PointerWithTag next; + for (uint8_t i = start_height; i >= end_height; i--) { + while (1) { + next = prev->Next(i); + // prev is logically deleted, roll back to prev height. + if (next.GetTag() == SkiplistNode::NodeStatus::Deleted) { + if (i < start_height) { + i++; + prev = result_splice->prevs[i]; + } else if (prev != start_node) { + // re-seek from this node for start height + i = start_height; + prev = start_node; + } else { + // this node has been deleted, so seek from header + kvdk_assert(result_splice->seeking_list != nullptr, + "skiplist must be set for seek operation!"); + return SeekNode(key, result_splice->seeking_list->HeaderNode(), + kMaxHeight, end_height, result_splice); + } + continue; + } + + if (next.Null()) { + result_splice->nexts[i] = nullptr; + result_splice->prevs[i] = prev; + break; + } + + // Physically remove deleted "next" nodes from skiplist + auto next_next = next->Next(i); + if (next_next.GetTag() == SkiplistNode::NodeStatus::Deleted) { + if (prev->CASNext(i, next, next_next.RawPointer())) { + if (--next->valid_links == 0) { + to_delete.push_back(next.RawPointer()); + } + } + // if prev is marked deleted before cas, cas will be failed, and prev + // will be roll back in next round + continue; + } + + DLRecord* next_data_record = next->record; + int cmp = Compare(key, next->UserKey()); + // pmem record maybe updated before comparing string, then the compare + // result will be invalid, so we need to do double check + if (next->record != next_data_record) { + continue; + } + + if (cmp > 0) { + prev = next.RawPointer(); + } else { + result_splice->nexts[i] = next.RawPointer(); + result_splice->prevs[i] = prev; + break; + } + } + } + if (to_delete.size() > 0) { + result_splice->seeking_list->obsoleteNodes(to_delete); + } +} + +void Skiplist::linkDLRecord(DLRecord* prev, DLRecord* next, DLRecord* linking, + Allocator* kv_allocator) { + uint64_t inserting_record_offset = kv_allocator->addr2offset(linking); + prev->next = inserting_record_offset; + + TEST_SYNC_POINT("KVEngine::DLList::LinkDLRecord::HalfLink"); + next->prev = inserting_record_offset; +} + +void Skiplist::Seek(const StringView& key, Splice* result_splice) { + result_splice->seeking_list = this; + SeekNode(key, header_, header_->Height(), 1, result_splice); + assert(result_splice->prevs[1] != nullptr); + DLRecord* prev_record = result_splice->prevs[1]->record; + DLRecord* next_record = nullptr; + while (1) { + next_record = kv_allocator_->offset2addr(prev_record->next); + if (next_record == HeaderRecord()) { + break; + } + + if (next_record == nullptr) { + return Seek(key, result_splice); + } + int cmp = Compare(key, UserKey(next_record)); + // pmem record maybe updated before comparing string, then the comparing + // result will be invalid, so we need to do double check + // + // Notice: In current implementation with the guard of snapshot mechanism, + // the record won't be freed during this operation, so this should not be + // happen + // if (!validateDLRecord(next_record)) { + // return Seek(key, result_splice); + // } + + if (cmp > 0) { + prev_record = next_record; + } else { + break; + } + } + result_splice->next_data_record = next_record; + result_splice->prev_data_record = prev_record; +} + +Status Skiplist::CheckIndex() { + DLListRecoveryUtils recovery_utils(kv_allocator_); + Splice splice(this); + splice.prev_data_record = HeaderRecord(); + for (uint8_t i = 1; i <= kMaxHeight; i++) { + splice.prevs[i] = header_; + } + + while (true) { + DLRecord* next_record = kv_allocator_->offset2addr_checked( + splice.prev_data_record->next); + if (next_record == HeaderRecord()) { + break; + } + SkiplistNode* next_node = splice.prevs[1]->RelaxedNext(1).RawPointer(); + if (IndexWithHashtable()) { + StringView key = next_record->Key(); + auto ret = hash_table_->Lookup(key, next_record->GetRecordType()); + if (ret.s != Status::Ok) { + GlobalLogger.Error( + "Check skiplist index error: record not exist in hash table\n"); + return Status::Abort; + } + + if (ret.entry.GetIndexType() == PointerType::SkiplistNode) { + if (ret.entry.GetIndex().skiplist_node != next_node) { + GlobalLogger.Error( + "Check skiplist index error: Dram node miss-match with hash " + "table\n"); + return Status::Abort; + } + } else { + if (ret.entry.GetIndex().dl_record != next_record) { + GlobalLogger.Error( + "Check skiplist index error: Dlrecord miss-match with hash " + "table\n"); + return Status::Abort; + } + } + } + + // Check dram linkage + if (next_node && next_node->record == next_record) { + for (uint8_t i = 1; i <= next_node->Height(); i++) { + if (splice.prevs[i]->RelaxedNext(i).RawPointer() != next_node) { + GlobalLogger.Error( + "Check skiplist index error: node linkage error\n"); + return Status::Abort; + } + splice.prevs[i] = next_node; + } + } + if (!recovery_utils.CheckLinkage(next_record)) { + return Status::Abort; + } + splice.prev_data_record = next_record; + } + + return Status::Ok; +} + +LockTable::MultiGuardType Skiplist::lockRecordPosition(const DLRecord* record, + Allocator* kv_allocator, + LockTable* lock_table) { + while (1) { + MemoryOffsetType prev_offset = record->prev; + MemoryOffsetType next_offset = record->next; + DLRecord* prev = kv_allocator->offset2addr_checked(prev_offset); + + auto guard = lock_table->MultiGuard({recordHash(prev), recordHash(record)}); + + // Check if the linkage has changed before we successfully acquire lock. + if (record->prev != prev_offset || record->next != next_offset) { + continue; + } + + return guard; + } +} + +bool Skiplist::lockInsertPosition(const StringView& inserting_key, + DLRecord* prev_record, DLRecord* next_record, + LockTable::ULockType* prev_record_lock) { + MemoryOffsetType prev_offset = + kv_allocator_->addr2offset_checked(prev_record); + MemoryOffsetType next_offset = + kv_allocator_->addr2offset_checked(next_record); + *prev_record_lock = record_locks_->AcquireLock(recordHash(prev_record)); + + // Check if the linkage has changed before we successfully acquire lock. + auto check_linkage = [&]() { + return prev_record->next == next_offset && next_record->prev == prev_offset; + }; + // Check id and order as prev and next may be both freed, then inserted + // to another position while keep linkage, before we lock them + // For example: + // Before lock: + // this skip list: record1 -> "prev" -> "next" -> record2 + // After lock: + // this skip list: "new record reuse prev" -> "new record reuse next" -> + // record1 -> record2 + // or: + // this skip list: record1 -> record2 + // another skip list:"new record reuse prev" -> "new record reuse next" -> + // In this case, inserting record will be mis-inserted between "new record + // reuse prev" and "new record reuse next" + // + // Notice: In current implementation with the guard of snapshot mechanism, the + // prev and next won't be freed during this operation, so id and order won't + // be changed anymore. We only check id and order in debug mode + auto check_id = [&]() { + return FetchID(next_record) == ID() && FetchID(prev_record) == ID(); + }; + + auto check_order = [&]() { + bool res = + /*check next*/ (next_record->GetRecordType() == + RecordType::SortedHeader || + Compare(inserting_key, UserKey(next_record)) <= 0) && + /*check prev*/ (prev_record->GetRecordType() == + RecordType::SortedHeader || + Compare(inserting_key, UserKey(prev_record)) > 0); + return res; + }; + if (!check_linkage()) { + *prev_record_lock = std::unique_lock(); + return false; + } + + kvdk_assert(check_id(), + "Check id of prev and next failed during skiplist insert\n"); + kvdk_assert(check_order(), + "Check key order of prev and next failed during skiplist " + "insert\n"); + + assert(prev_record->next == next_offset); + assert(next_record->prev == prev_offset); + + return true; +} + +Skiplist::WriteResult Skiplist::Write(SortedWriteArgs& args) { + WriteResult ret; + if (args.skiplist != this) { + ret.s = Status::InvalidArgument; + return ret; + } + if (args.op == WriteOp::Put) { + if (IndexWithHashtable()) { + ret = putPreparedWithHash(args.lookup_result, args.key, args.value, + args.ts, args.space); + } else { + kvdk_assert(args.seek_result != nullptr, ""); + ret = putPreparedNoHash(*args.seek_result, args.key, args.value, args.ts, + args.space); + } + if (ret.existing_record == nullptr || + ret.existing_record->GetRecordStatus() == RecordStatus::Outdated) { + UpdateSize(1); + } + } else { + if (IndexWithHashtable()) { + ret = deletePreparedWithHash(args.lookup_result, args.key, args.ts, + args.space); + } else { + kvdk_assert(args.seek_result != nullptr, ""); + DLRecord* existing_record = args.seek_result->next_data_record; + SkiplistNode* dram_node = nullptr; + if (args.seek_result->nexts[1] && + args.seek_result->nexts[1]->record == existing_record) { + dram_node = args.seek_result->nexts[1]; + } + ret = deletePreparedNoHash(existing_record, dram_node, args.key, args.ts, + args.space); + } + + if (ret.existing_record != nullptr && + ret.existing_record->GetRecordStatus() == RecordStatus::Normal) { + UpdateSize(-1); + } + } + return ret; +} + +SortedWriteArgs Skiplist::InitWriteArgs(const StringView& key, + const StringView& value, WriteOp op) { + SortedWriteArgs args; + args.collection = Name(); + args.skiplist = this; + args.key = key; + args.value = value; + args.op = op; + return args; +} + +Status Skiplist::PrepareWrite(SortedWriteArgs& args, TimestampType ts) { + kvdk_assert(args.op == WriteOp::Put || args.value.size() == 0, + "value of delete operation should be empty"); + if (args.skiplist != this) { + return Status::InvalidArgument; + } + bool op_delete = args.op == WriteOp::Delete; + std::string internal_key(InternalKey(args.key)); + bool allocate_space = true; + if (IndexWithHashtable()) { + if (op_delete) { + args.lookup_result = + hash_table_->Lookup(internal_key, RecordType::SortedElem); + } else { + args.lookup_result = + hash_table_->Lookup(internal_key, RecordType::SortedElem); + } + switch (args.lookup_result.s) { + case Status::Ok: { + if (op_delete && args.lookup_result.entry.GetRecordStatus() == + RecordStatus::Outdated) { + allocate_space = false; + } + break; + } + case Status::NotFound: { + if (op_delete) { + allocate_space = false; + } + break; + } + case Status::MemoryOverflow: { + return args.lookup_result.s; + } + default: + std::abort(); // never should reach + } + } else { + args.seek_result = std::unique_ptr(new Splice(args.skiplist)); + Seek(args.key, args.seek_result.get()); + auto key_exist = [&]() { + auto type = args.seek_result->next_data_record->GetRecordType(); + auto status = args.seek_result->next_data_record->GetRecordStatus(); + return type == RecordType::SortedElem && status == RecordStatus::Normal && + equal_string_view(args.seek_result->next_data_record->Key(), + internal_key); + }; + if (op_delete && !key_exist()) { + allocate_space = false; + } + } + + if (allocate_space) { + auto request_size = DLRecord::RecordSize(internal_key, args.value); + args.space = kv_allocator_->Allocate(request_size); + if (args.space.size == 0) { + return Status::MemoryOverflow; + } + } + + args.ts = ts; + return Status::Ok; +} + +Skiplist::WriteResult Skiplist::Delete(const StringView& key, + TimestampType timestamp) { + WriteResult ret; + SortedWriteArgs args = InitWriteArgs(key, "", WriteOp::Delete); + ret.s = PrepareWrite(args, timestamp); + if (ret.s == Status::Ok && args.space.size > 0) { + ret = Write(args); + } + return ret; +} + +Skiplist::WriteResult Skiplist::Put(const StringView& key, + const StringView& value, + TimestampType timestamp) { + WriteResult ret; + SortedWriteArgs args = InitWriteArgs(key, value, WriteOp::Put); + ret.s = PrepareWrite(args, timestamp); + if (ret.s == Status::Ok) { + ret = Write(args); + } + return ret; +} + +bool Skiplist::Replace(DLRecord* old_record, DLRecord* new_record, + SkiplistNode* dram_node, Allocator* kv_allocator, + LockTable* lock_table) { + bool ok = DLList::Replace(old_record, new_record, kv_allocator, lock_table); + if (ok && dram_node != nullptr) { + kvdk_assert(dram_node->record == old_record, + "Dram node not belong to old record in Skiplist::Replace"); + dram_node->record = new_record; + } + return ok; +} + +bool Skiplist::Remove(DLRecord* removing_record, SkiplistNode* dram_node, + Allocator* kv_allocator, LockTable* lock_table) { + bool ok = DLList::Remove(removing_record, kv_allocator, lock_table); + if (ok && dram_node) { + dram_node->MarkAsDeleted(); + } + return ok; +} + +SkiplistNode* Skiplist::NewNodeBuild(DLRecord* data_record, Allocator* alloc) { + SkiplistNode* dram_node = nullptr; + auto height = Skiplist::randomHeight(); + if (height > 0) { + StringView user_key = UserKey(data_record); + dram_node = SkiplistNode::NewNode(user_key, data_record, height, alloc); + if (dram_node == nullptr) { + GlobalLogger.Error("Memory overflow in Skiplist::NewNodeBuild\n"); + } + } + return dram_node; +} + +std::string Skiplist::EncodeSortedCollectionValue( + CollectionIDType id, const SortedCollectionConfigs& s_configs) { + std::string value_str; + + AppendUint64(&value_str, id); + AppendFixedString(&value_str, s_configs.comparator_name); + AppendUint32(&value_str, s_configs.index_with_hashtable); + + return value_str; +} + +Status Skiplist::DecodeSortedCollectionValue( + StringView value_str, CollectionIDType& id, + SortedCollectionConfigs& s_configs) { + if (!FetchUint64(&value_str, &id)) { + return Status::Abort; + } + if (!FetchFixedString(&value_str, &s_configs.comparator_name)) { + return Status::Abort; + } + if (!FetchUint32(&value_str, (uint32_t*)&s_configs.index_with_hashtable)) { + return Status::Abort; + } + + return Status::Ok; +} + +Status Skiplist::Get(const StringView& key, std::string* value) { + if (!IndexWithHashtable()) { + Splice splice(this); + Seek(key, &splice); + auto type = splice.next_data_record->GetRecordType(); + auto status = splice.next_data_record->GetRecordStatus(); + if (type == RecordType::SortedElem && status != RecordStatus::Outdated && + equal_string_view(key, UserKey(splice.next_data_record))) { + value->assign(splice.next_data_record->Value().data(), + splice.next_data_record->Value().size()); + return Status::Ok; + } else { + return Status::NotFound; + } + } else { + std::string internal_key = InternalKey(key); + auto ret = hash_table_->Lookup(internal_key, RecordType::SortedElem); + if (ret.s != Status::Ok || + ret.entry.GetRecordStatus() == RecordStatus::Outdated) { + return Status::NotFound; + } + + DLRecord* data_record; + switch (ret.entry.GetIndexType()) { + case PointerType::SkiplistNode: { + data_record = ret.entry.GetIndex().skiplist_node->record; + break; + } + case PointerType::DLRecord: { + data_record = ret.entry.GetIndex().dl_record; + break; + } + default: { + GlobalLogger.Error( + "Wrong hash index type while search sorted data in hash table\n"); + return Status::Abort; + } + } + kvdk_assert(data_record->GetRecordType() == RecordType::SortedElem, ""); + // As get is lockless, skiplist node may point to a new elem delete record + // after we get it from hashtable + if (data_record->GetRecordStatus() == RecordStatus::Outdated) { + return Status::NotFound; + } else { + value->assign(data_record->Value().data(), data_record->Value().size()); + return Status::Ok; + } + } +} + +Skiplist::WriteResult Skiplist::deletePreparedNoHash(DLRecord* existing_record, + SkiplistNode* dram_node, + const StringView& key, + TimestampType timestamp, + const SpaceEntry& space) { + kvdk_assert(existing_record != nullptr, ""); + WriteResult ret; + std::string internal_key(InternalKey(key)); + kvdk_assert(equal_string_view(existing_record->Key(), internal_key), ""); + ret.existing_record = existing_record; + ret.dram_node = dram_node; + DLList::WriteArgs args(internal_key, "", RecordType::SortedElem, + RecordStatus::Outdated, timestamp, space); + while (dl_list_.Update(args, existing_record) != Status::Ok) { + } + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + + if (dram_node) { + dram_node->record = ret.write_record; + } + return ret; +} + +Skiplist::WriteResult Skiplist::deletePreparedWithHash( + const HashTable::LookupResult& lookup_result, const StringView& key, + TimestampType timestamp, const SpaceEntry& space) { + std::string internal_key(InternalKey(key)); + assert(IndexWithHashtable()); + assert(lookup_result.s == Status::Ok); + assert(lookup_result.entry.GetRecordType() == RecordType::SortedElem && + lookup_result.entry.GetRecordStatus() == RecordStatus::Normal); + assert(space.size >= DLRecord::RecordSize(internal_key, "")); + DLRecord* existing_record; + SkiplistNode* dram_node; + + if (lookup_result.entry.GetIndexType() == PointerType::SkiplistNode) { + dram_node = lookup_result.entry.GetIndex().skiplist_node; + existing_record = dram_node->record; + } else { + dram_node = nullptr; + assert(lookup_result.entry.GetIndexType() == PointerType::DLRecord); + existing_record = lookup_result.entry.GetIndex().dl_record; + } + assert(timestamp > existing_record->GetTimestamp()); + + auto ret = + deletePreparedNoHash(existing_record, dram_node, key, timestamp, space); + + // until here, new record is already inserted to list + assert(ret.write_record != nullptr); + if (ret.dram_node == nullptr) { + hash_table_->Insert(lookup_result, RecordType::SortedElem, + RecordStatus::Outdated, ret.write_record, + PointerType::DLRecord); + } else { + ret.dram_node->record = ret.write_record; + hash_table_->Insert(lookup_result, RecordType::SortedElem, + RecordStatus::Outdated, ret.dram_node, + PointerType::SkiplistNode); + } + + return ret; +} + +Skiplist::WriteResult Skiplist::putPreparedWithHash( + const HashTable::LookupResult& lookup_result, const StringView& key, + const StringView& value, TimestampType timestamp, const SpaceEntry& space) { + WriteResult ret; + assert(IndexWithHashtable()); + std::string internal_key(InternalKey(key)); + DLList::WriteArgs args(internal_key, value, RecordType::SortedElem, + RecordStatus::Normal, timestamp, space); + + switch (lookup_result.s) { + case Status::Ok: { + if (lookup_result.entry.GetIndexType() == PointerType::SkiplistNode) { + ret.dram_node = lookup_result.entry.GetIndex().skiplist_node; + ret.existing_record = ret.dram_node->record; + } else { + ret.dram_node = nullptr; + assert(lookup_result.entry.GetIndexType() == PointerType::DLRecord); + ret.existing_record = lookup_result.entry.GetIndex().dl_record; + } + assert(timestamp > ret.existing_record->GetTimestamp()); + while (dl_list_.Update(args, ret.existing_record) != Status::Ok) { + } + + ret.write_record = + kv_allocator_->offset2addr_checked(space.offset); + ret.hash_entry_ptr = lookup_result.entry_ptr; + break; + } + case Status::NotFound: { + Splice splice(this); + Seek(key, &splice); + ret = putPreparedNoHash(splice, key, value, timestamp, space); + if (ret.s != Status::Ok) { + return ret; + } + + break; + } + case Status::MemoryOverflow: { + return ret; + } + default: + std::abort(); // never should reach + } + + // until here, new record is already inserted to list + assert(ret.write_record != nullptr); + if (ret.dram_node == nullptr) { + hash_table_->Insert(lookup_result, RecordType::SortedElem, + RecordStatus::Normal, ret.write_record, + PointerType::DLRecord); + } else { + ret.dram_node->record = ret.write_record; + hash_table_->Insert(lookup_result, RecordType::SortedElem, + RecordStatus::Normal, ret.dram_node, + PointerType::SkiplistNode); + } + + return ret; +} + +Skiplist::WriteResult Skiplist::putPreparedNoHash(Splice& seek_result, + const StringView& key, + const StringView& value, + TimestampType timestamp, + const SpaceEntry& space) { + WriteResult ret; + std::string internal_key(InternalKey(key)); + bool key_exist; + DLList::WriteArgs args(internal_key, value, RecordType::SortedElem, + RecordStatus::Normal, timestamp, space); + +seek_write_position: + key_exist = + !IndexWithHashtable() /* a hash indexed skiplist call this + function only if key not exist */ + && + seek_result.next_data_record->GetRecordType() == RecordType::SortedElem && + equal_string_view(seek_result.next_data_record->Key(), internal_key); + + if (key_exist) { + ret.existing_record = seek_result.next_data_record; + if (seek_result.nexts[1] && + seek_result.nexts[1]->record == ret.existing_record) { + ret.dram_node = seek_result.nexts[1]; + } + + while (dl_list_.Update(args, ret.existing_record) != Status::Ok) { + } + } else { + ret.existing_record = nullptr; + if (dl_list_.InsertBetween(args, seek_result.prev_data_record, + seek_result.next_data_record) != Status::Ok) { + seek_result = Splice(this); + Seek(key, &seek_result); + goto seek_write_position; + } + } + + ret.write_record = kv_allocator_->offset2addr_checked(space.offset); + + if (!key_exist) { + // create dram node for new record + ret.dram_node = Skiplist::NewNodeBuild(ret.write_record, node_allocator_); + if (ret.dram_node != nullptr) { + auto height = ret.dram_node->Height(); + for (int i = 1; i <= height; i++) { + while (1) { + auto now_next = seek_result.prevs[i]->Next(i); + // if next has been changed or been deleted, re-compute + if (now_next.RawPointer() == seek_result.nexts[i] && + now_next.GetTag() == SkiplistNode::NodeStatus::Normal) { + ret.dram_node->RelaxedSetNext(i, seek_result.nexts[i]); + if (seek_result.prevs[i]->CASNext(i, seek_result.nexts[i], + ret.dram_node)) { + break; + } + } else { + seek_result.Recompute(key, i); + } + } + } + } + } else if (ret.dram_node) { + ret.dram_node->record = ret.write_record; + } + return ret; +} + +void Skiplist::CleanObsoletedNodes() { + std::lock_guard lg_a(pending_delete_nodes_spin_); + if (pending_deletion_nodes_.size() > 0) { + for (SkiplistNode* node : pending_deletion_nodes_) { + // TODO: make sure the node is not referenced + SkiplistNode::DeleteNode(node, node_allocator_); + } + pending_deletion_nodes_.clear(); + } + + std::lock_guard lg_b(obsolete_nodes_spin_); + obsolete_nodes_.swap(pending_deletion_nodes_); +} + +void Skiplist::destroyAllRecords() { + std::vector to_free; + if (header_) { + DLRecord* header_record = HeaderRecord(); + DLRecord* to_destroy = nullptr; + do { + to_destroy = + kv_allocator_->offset2addr_checked(header_record->next); + StringView key = to_destroy->Key(); + auto ul = hash_table_->AcquireLock(key); + // We need to purge destroyed records one by one in case engine crashed + // during destroy + if (Skiplist::Remove(to_destroy, nullptr, kv_allocator_, record_locks_)) { + if (IndexWithHashtable()) { + auto lookup_result = + hash_table_->Lookup(key, to_destroy->GetRecordType()); + if (lookup_result.s == Status::Ok) { + DLRecord* hash_indexed_record = nullptr; + auto hash_index = lookup_result.entry.GetIndex(); + switch (lookup_result.entry.GetIndexType()) { + case PointerType::Skiplist: + hash_indexed_record = hash_index.skiplist->HeaderRecord(); + break; + case PointerType::SkiplistNode: + hash_indexed_record = hash_index.skiplist_node->record; + break; + case PointerType::DLRecord: + hash_indexed_record = hash_index.dl_record; + break; + default: + kvdk_assert(false, "Wrong hash index type of sorted record"); + } + + if (hash_indexed_record == to_destroy) { + hash_table_->Erase(lookup_result.entry_ptr); + } + } + } + + auto old_record = static_cast( + kv_allocator_->offset2addr(to_destroy->old_version)); + while (old_record) { + auto old_version = old_record->old_version; + to_free.emplace_back(kv_allocator_->addr2offset(old_record), + old_record->GetRecordSize()); + old_record->Destroy(); + old_record = kv_allocator_->offset2addr(old_version); + } + + to_free.emplace_back(kv_allocator_->addr2offset_checked(to_destroy), + to_destroy->GetRecordSize()); + to_destroy->Destroy(); + if (to_free.size() > kMaxCachedOldRecords) { + kv_allocator_->BatchFree(to_free); + to_free.clear(); + } + } + } while (to_destroy != + header_record /* header record should be the last detroyed one */); + } + kv_allocator_->BatchFree(to_free); +} + +void Skiplist::Destroy() { + destroyRecords(); + destroyNodes(); +} + +void Skiplist::DestroyAll() { + destroyAllRecords(); + destroyNodes(); +} + +void Skiplist::destroyNodes() { + if (header_) { + // To avoid memory leak (don't free created skiplist node), we should + // iterate the skiplist to find all deleted skiplist nodes. + // Notice: sometimes, a thread has just marked deleted skiplist node A, but + // another thread found this deleted skiplist node A and updated its linkage + // of lower height whiling seeking nodes. It is easy to ignore that the + // linkage of higher height of this deleted skiplist node A has not been + // changed. + for (int i = header_->Height(); i >= 1; --i) { + auto to_delete = header_->Next(i).RawPointer(); + while (to_delete) { + auto next = to_delete->Next(i).RawPointer(); + if (--to_delete->valid_links == 0) { + SkiplistNode::DeleteNode(to_delete, node_allocator_); + } + to_delete = next; + } + } + SkiplistNode::DeleteNode(header_, node_allocator_); + header_ = nullptr; + } +} + +void Skiplist::destroyRecords() { + std::vector to_free; + if (header_) { + DLRecord* header_record = HeaderRecord(); + DLRecord* to_destroy = nullptr; + do { + to_destroy = + kv_allocator_->offset2addr_checked(header_record->next); + StringView key = to_destroy->Key(); + auto ul = hash_table_->AcquireLock(key); + // We need to purge destroyed records one by one in case engine crashed + // during destroy + if (Skiplist::Remove(to_destroy, nullptr, kv_allocator_, record_locks_)) { + if (IndexWithHashtable()) { + auto lookup_result = + hash_table_->Lookup(key, to_destroy->GetRecordType()); + + if (lookup_result.s == Status::Ok) { + DLRecord* hash_indexed_record = nullptr; + auto hash_index = lookup_result.entry.GetIndex(); + switch (lookup_result.entry.GetIndexType()) { + case PointerType::Skiplist: + hash_indexed_record = hash_index.skiplist->HeaderRecord(); + break; + case PointerType::SkiplistNode: + hash_indexed_record = hash_index.skiplist_node->record; + break; + case PointerType::DLRecord: + hash_indexed_record = hash_index.dl_record; + break; + default: + kvdk_assert(false, "Wrong hash index type of sorted record"); + } + if (hash_indexed_record == to_destroy) { + hash_table_->Erase(lookup_result.entry_ptr); + } + } + } + to_destroy->Destroy(); + + to_free.emplace_back(kv_allocator_->addr2offset_checked(to_destroy), + to_destroy->GetRecordSize()); + } + + } while (to_destroy != + header_record /* header record should be the last detroyed one */); + } + + kv_allocator_->BatchFree(to_free); +} + +size_t Skiplist::Size() { return size_.load(std::memory_order_relaxed); } + +void Skiplist::UpdateSize(int64_t delta) { + kvdk_assert(delta >= 0 || size_.load() >= static_cast(-delta), + "Update skiplist size to negative"); + size_.fetch_add(delta, std::memory_order_relaxed); +} +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/sorted_collection/skiplist.hpp b/volatile/engine/sorted_collection/skiplist.hpp new file mode 100644 index 00000000..7cf39cd4 --- /dev/null +++ b/volatile/engine/sorted_collection/skiplist.hpp @@ -0,0 +1,582 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../alias.hpp" +#include "../collection.hpp" +#include "../dl_list.hpp" +#include "../hash_table.hpp" +#include "../lock_table.hpp" +#include "../structures.hpp" +#include "../utils/utils.hpp" +#include "../write_batch_impl.hpp" +#include "kvdk/volatile/engine.hpp" + +namespace KVDK_NAMESPACE { +static const uint8_t kMaxHeight = 32; +static const uint8_t kCacheHeight = 3; + +struct Splice; +class SortedIteratorImpl; + +struct SortedWriteArgs { + StringView collection; + StringView key; + StringView value; + WriteOp op; + Skiplist* skiplist; + SpaceEntry space; + TimestampType ts; + HashTable::LookupResult lookup_result; + std::unique_ptr seek_result; +}; + +/* Format: + * next pointers | DLRecord on kv memory | height | cached key size | + * cached key We only cache key if height > kCache height or there are enough + * space in the end of malloced space to cache the key (4B here). + * */ +struct SkiplistNode { + public: + enum class NodeStatus : uint8_t { + Normal = 0, + Deleted = 1, + }; + // Tagged pointers means this node has been logically removed from the list + std::atomic> next[0]; + // Doubly linked record on kv memory + DLRecord* record; + // TODO: save memory + uint16_t cached_key_size; + uint8_t height; + // How many height this node are linked on its skiplist. If this node is + // phisically remove from some height of a skiplist, then valid_links-=1. + // valid_links==0 means this node is removed from every height of the + // skiplist, free this node. + std::atomic valid_links{0}; + // 4 bytes for alignment, the actually allocated size may > 4 + char cached_key[4]; + + // Create a skiplist node, you should use same allocator for NewNode and + // DeleteNode + static SkiplistNode* NewNode(const StringView& key, DLRecord* data_record, + uint8_t height, Allocator* alloc) { + if (alloc == nullptr) { + alloc = global_memory_allocator(); + } + size_t size; + if (height >= kCacheHeight && key.size() > 4) { + size = sizeof(SkiplistNode) + 8 * height + key.size() - 4; + } else { + size = sizeof(SkiplistNode) + 8 * height; + } + SkiplistNode* node = nullptr; + + SpaceEntry entry = alloc->Allocate(size); + if (entry.size != 0) { + void* space = alloc->offset2addr(entry.offset); + node = (SkiplistNode*)((char*)space + 8 * height); + node->record = data_record; + node->height = height; + // make sure this will be linked to skiplist at all the height after + // creation + node->valid_links = height; + node->maybeCacheKey(key); + } + return node; + } + + // Destroy a skiplist node, you should use same allocator for NewNode and + // DeleteNode + static void DeleteNode(SkiplistNode* node, Allocator* alloc) { + if (alloc == nullptr) { + alloc = global_memory_allocator(); + } + uint64_t offset = alloc->addr2offset(node->heap_space_start()); + uint64_t size = node->allocated_size(); + alloc->Free(SpaceEntry(offset, size)); + } + + uint16_t Height() { return height; } + + StringView UserKey(); + + CollectionIDType SkiplistID(); + + PointerWithTag Next(int l) { + assert(l > 0 && l <= height && "should be less than node's height"); + return next[-l].load(std::memory_order_acquire); + } + + bool CASNext(int l, PointerWithTag expected, + PointerWithTag x) { + assert(l > 0 && l <= height && "should be less than node's height"); + return (next[-l].compare_exchange_strong(expected, x)); + } + + PointerWithTag RelaxedNext(int l) { + assert(l > 0 && l <= height && "should be less than node's height"); + return next[-l].load(std::memory_order_relaxed); + } + + void SetNext(int l, PointerWithTag x) { + assert(l > 0 && l <= height && "should be less than node's height"); + next[-l].store(x, std::memory_order_release); + } + + void RelaxedSetNext(int l, PointerWithTag x) { + assert(l > 0 && l <= height && "should be less than node's height"); + next[-l].store(x, std::memory_order_relaxed); + } + + // Logically delete node by tag next pointers from bottom to top + void MarkAsDeleted() { + for (int l = 1; l <= height; l++) { + while (1) { + auto next = RelaxedNext(l); + // This node alread tagged by another thread + if (next.GetTag() == NodeStatus::Deleted) { + break; + } + auto tagged = PointerWithTag( + next.RawPointer(), NodeStatus::Deleted); + if (CASNext(l, next, tagged)) { + break; + } + } + } + } + + bool IsDeleted() { return Next(1).GetTag() == NodeStatus::Deleted; } + + private: + SkiplistNode() {} + + void maybeCacheKey(const StringView& key) { + if (height >= kCacheHeight || key.size() <= 4) { + cached_key_size = key.size(); + memcpy(cached_key, key.data(), key.size()); + } else { + cached_key_size = 0; + } + } + + void* heap_space_start() { return (char*)this - height * 8; } + + uint64_t allocated_size() { + if (cached_key_size > 4) { + return sizeof(SkiplistNode) + 8 * height + cached_key_size - 4; + } else { + return sizeof(SkiplistNode) + 8 * height; + } + } +}; + +// A persistent sorted collection implemented as skiplist struct, data organized +// sorted by key +// +// The lowest level of the skiplist are stored on kv memory along with key and +// values, while higher level nodes are stored in DRAM, and re-construct at +// recovery. +// The insert and seek operations are indexed by the multi-level links and +// implemented in O(logn) time. Meanwhile, the skiplist nodes is also indexed by +// the global hash table, so the updates/delete and point read operations can be +// indexed by hash table and implemented in ~O(1) time +// Each skiplist has a header record stored on kv memory, the key of header +// record is the skiplist name, the value of header record is encoded by +// skiplist id and configs +class Skiplist : public Collection { + public: + // Result of a write operation + struct WriteResult { + Status s = Status::Ok; + DLRecord* existing_record = nullptr; + DLRecord* write_record = nullptr; + SkiplistNode* dram_node = nullptr; + HashEntry* hash_entry_ptr = nullptr; + }; + + Skiplist(DLRecord* h, const std::string& name, CollectionIDType id, + Comparator comparator, Allocator* kv_allocator, + Allocator* node_allocator, HashTable* hash_table, + LockTable* lock_table, bool index_with_hashtable); + + ~Skiplist() final; + + SkiplistNode* HeaderNode() { return header_; } + + DLRecord* HeaderRecord() { return header_->record; } + + const DLRecord* HeaderRecord() const { return header_->record; } + + bool IndexWithHashtable() { return index_with_hashtable_; } + + ExpireTimeType GetExpireTime() const final { + return HeaderRecord()->GetExpireTime(); + } + + bool HasExpired() const final { return HeaderRecord()->HasExpired(); } + + DLList* GetDLList() { return &dl_list_; } + + // Set this skiplist expire at expired_time + // + // Args: + // * expired_time: time to expire + // * timestamp: kvdk engine timestamp of calling this function + // + // Return Ok on success + WriteResult SetExpireTime(ExpireTimeType expired_time, + TimestampType timestamp); + + // Put "key, value" to the skiplist + // + // Args: + // * timestamp: kvdk engine timestamp of this operation + // + // Return Ok on success, with the writed data record, its dram node and + // updated data record if it exists + // + // Notice: the putting key should already been locked by engine + WriteResult Put(const StringView& key, const StringView& value, + TimestampType timestamp); + + // Get value of "key" from the skiplist + Status Get(const StringView& key, std::string* value); + + // Delete "key" from the skiplist by replace it with a delete record + // + // Args: + // * timestamp: kvdk engine timestamp of this operation + // + // Return Ok on success, with the writed delete record, its dram node and + // deleted record if it exists + // + // Notice: the deleting key should already been locked by engine + WriteResult Delete(const StringView& key, TimestampType timestamp); + + // Init args for put or delete operations + SortedWriteArgs InitWriteArgs(const StringView& key, const StringView& value, + WriteOp op); + + // Prepare neccessary resources for write, store lookup/seek result of key and + // required memory space to write new reocrd in args + // + // Args: + // * args: generated by InitWriteArgs + // + // Return: + // Ok on success + // MemoryOverflow if no enough kv memory space + // MemoryOverflow if no enough dram space + // + // Notice: args.key should already been locked by engine + Status PrepareWrite(SortedWriteArgs& args, TimestampType ts); + + // Do batch write according to args + // + // Args: + // * args: write args prepared by PrepareWrite() + // + // Return: + // Status Ok on success, with the writed delete record, its dram node and + // deleted record if existing + WriteResult Write(SortedWriteArgs& args); + + // Seek position of "key" on both dram and kv node in the skiplist, and + // store position in "result_splice". If "key" existing, the next pointers in + // splice point to node of "key" + void Seek(const StringView& key, Splice* result_splice); + + // Start seek from "start_node", find dram position of "key" in the skiplist + // between height "start_height" and "end"_height", and store position in + // "result_splice", if "key" existing, the next pointers in splice point to + // node of "key" + void SeekNode(const StringView& key, SkiplistNode* start_node, + uint8_t start_height, uint8_t end_height, + Splice* result_splice); + + // Destroy and free the whole skiplist, including skiplist nodes and kv + // records. + void Destroy(); + + // Destroy and free the whole skiplist with old version list. + void DestroyAll(); + + // check node linkage and hash index + Status CheckIndex(); + + void CleanObsoletedNodes(); + + // Return number of elements in skiplist + size_t Size(); + + void UpdateSize(int64_t delta); + + int Compare(const StringView& src_key, const StringView& target_key) { + return comparator_(src_key, target_key); + } + + static bool MatchType(DLRecord* record) { + RecordType type = record->GetRecordType(); + return type == RecordType::SortedElem || type == RecordType::SortedHeader; + } + + // Remove a dl record from its skiplist by unlinking + // + // Args: + // * purged_record:existing record to purge + // * dram_node:dram node of purging record, if it's a height 0 record, then + // pass nullptr + // + // Return: + // * true on success + // * false if purging_record not linked on a skiplist + // + // Notice: key of the purging record should already been locked by engine + static bool Remove(DLRecord* purging_record, SkiplistNode* dram_node, + Allocator* kv_allocator, LockTable* lock_table); + + // Replace "old_record" from its skiplist with "replacing_record", please make + // sure the key order is correct after replace + // + // Args: + // * old_record: existing record to be replaced + // * new_record: new reocrd to replace the older one + // * dram_node:dram node of old record, if it's a height 0 record, then + // pass nullptr + // + // Return: + // * true on success + // * false if old_record not linked on a skiplist + // + // Notice: key of the replacing record should already been locked by engine + static bool Replace(DLRecord* old_record, DLRecord* new_record, + SkiplistNode* dram_node, Allocator* kv_allocator, + LockTable* lock_table); + + // Build a skiplist node for "data_record" + static SkiplistNode* NewNodeBuild(DLRecord* data_record, Allocator* alloc); + + // Format: + // id (8 bytes) | configs + static std::string EncodeSortedCollectionValue( + CollectionIDType id, const SortedCollectionConfigs& s_configs); + + static Status DecodeSortedCollectionValue(StringView value_str, + CollectionIDType& id, + SortedCollectionConfigs& s_configs); + + inline static StringView UserKey(const SkiplistNode* node) { + assert(node != nullptr); + if (node->cached_key_size > 0) { + return StringView(node->cached_key, node->cached_key_size); + } + return ExtractUserKey(node->record->Key()); + } + + inline static StringView UserKey(const DLRecord* record) { + assert(record != nullptr); + return ExtractUserKey(record->Key()); + } + + inline static CollectionIDType FetchID(const SkiplistNode* node) { + assert(node != nullptr); + return FetchID(node->record); + } + + inline static CollectionIDType FetchID(const DLRecord* record) { + assert(record != nullptr); + switch (record->GetRecordType()) { + case RecordType::SortedElem: + return ExtractID(record->Key()); + break; + case RecordType::SortedHeader: + return DecodeID(record->Value()); + default: + GlobalLogger.Error("Wrong record type %u in SkiplistID", + record->GetRecordType()); + kvdk_assert(false, "Wrong type in SkiplistID"); + } + return 0; + } + + bool TryCleaningLock() { return cleaning_lock_.try_lock(); } + + void ReleaseCleaningLock() { cleaning_lock_.unlock(); } + + private: + friend SortedIteratorImpl; + + // put impl with prepared seek result and kv memory space + WriteResult putPreparedNoHash(Splice& seek_result, const StringView& key, + const StringView& value, + TimestampType timestamp, + const SpaceEntry& space); + + // put impl with prepared lookup result and kv memory space + WriteResult putPreparedWithHash(const HashTable::LookupResult& lookup_result, + const StringView& key, + const StringView& value, + TimestampType timestamp, + const SpaceEntry& space); + + // put impl with prepared existing record and kv memory space + WriteResult deletePreparedNoHash(DLRecord* existing_record, + SkiplistNode* dram_node, + const StringView& key, + TimestampType timestamp, + const SpaceEntry& space); + + // put impl with prepared lookup result of existing record and kv memory space + WriteResult deletePreparedWithHash( + const HashTable::LookupResult& lookup_result, const StringView& key, + TimestampType timestamp, const SpaceEntry& space); + + // Link DLRecord "linking" between "prev" and "next" + static void linkDLRecord(DLRecord* prev, DLRecord* next, DLRecord* linking, + Allocator* kv_allocator); + + inline void linkDLRecord(DLRecord* prev, DLRecord* next, DLRecord* linking) { + return linkDLRecord(prev, next, linking, kv_allocator_); + } + + // lock skiplist position to insert "key" by locking prev DLRecord and manage + // the lock with "prev_record_lock". + // + // Return true on success, return false if linkage of prev_record and + // next_record changed before succefully acquire lock + bool lockInsertPosition(const StringView& inserting_key, + DLRecord* prev_record, DLRecord* next_record, + LockTable::ULockType* prev_record_lock); + + // lock skiplist position of "record" by locking its prev DLRecord and the + // record itself + // Notice: we do not check if record is still correctly linked + static LockTable::MultiGuardType lockRecordPosition(const DLRecord* record, + Allocator* kv_allocator, + LockTable* lock_table); + + // lock skiplist position of "record" by locking its prev DLRecord and the + // record itself + // Notice: record must be a on list record, e.g. correctly linked by its + // predecessor + LockTable::MultiGuardType lockOnListRecord(const DLRecord* record) { + while (true) { + auto guard = lockRecordPosition(record, kv_allocator_, record_locks_); + DLRecord* prev = + kv_allocator_->offset2addr_checked(record->prev); + if (prev->next == kv_allocator_->addr2offset_checked(record)) { + return guard; + } + } + } + + void obsoleteNodes(const std::vector nodes) { + std::lock_guard lg(obsolete_nodes_spin_); + for (SkiplistNode* node : nodes) { + obsolete_nodes_.push_back(node); + } + } + + static uint8_t randomHeight() { + uint8_t height = 0; + while (height < kMaxHeight && fast_random_64() & 1) { + height++; + } + + return height; + } + + // Destroy sorted records, not including old version list. + void destroyRecords(); + + void destroyNodes(); + + // Destroy all sorted records including old version list. + void destroyAllRecords(); + + static LockTable::HashValueType recordHash(const DLRecord* record) { + kvdk_assert(record != nullptr, ""); + return XXH3_64bits(record, sizeof(const DLRecord*)); + } + + DLList dl_list_; + std::atomic size_; + Comparator comparator_ = compare_string_view; + // Allocate space for skiplist kv record + Allocator* kv_allocator_; + // Allocate space for skiplist high level nodes + Allocator* node_allocator_; + // TODO: use specified hash table for each skiplist + HashTable* hash_table_; + // locks to protect modification of records + LockTable* record_locks_; + bool index_with_hashtable_; + SkiplistNode* header_; + // nodes that unlinked on every height + std::vector obsolete_nodes_; + // to avoid concurrent access a just deleted node, a node can be safely + // deleted only if a certain interval is passes after being moved from + // obsolete_nodes_ to pending_deletion_nodes_, this is guaranteed by + // background thread of kvdk instance + std::vector pending_deletion_nodes_; + // protect obsolete_nodes_ + SpinMutex obsolete_nodes_spin_; + // protect pending_deletion_nodes_ + SpinMutex pending_delete_nodes_spin_; + // to avoid illegal access caused by cleaning skiplist by multi-thread + SpinMutex cleaning_lock_; +}; + +// A helper struct for locating a skiplist position +// +// nexts: next nodes on DRAM of a key position, or node of the key if it existed +// prevs: prev nodes on DRAM of a key position +// prev_data_record: previous record on kv memory of a key position +// next_data_record: next record on kv memory of a key position, or record of +// the key if it existed +// +// TODO: maybe we only need prev position +struct Splice { + // Seeking skiplist + Skiplist* seeking_list; + std::array nexts; + std::array prevs; + DLRecord* prev_data_record{nullptr}; + DLRecord* next_data_record{nullptr}; + + Splice(Skiplist* s) : seeking_list(s) {} + + void Recompute(const StringView& key, uint8_t l) { + SkiplistNode* start_node; + uint8_t start_height = l; + while (1) { + if (start_height > kMaxHeight || prevs[start_height] == nullptr) { + assert(seeking_list != nullptr); + start_height = kMaxHeight; + start_node = seeking_list->HeaderNode(); + } else if (prevs[start_height]->Next(start_height).GetTag() == + SkiplistNode::NodeStatus::Deleted) { + // If prev on this height has been deleted, roll back to higher height + start_height++; + continue; + } else { + start_node = prevs[start_height]; + } + seeking_list->SeekNode(key, start_node, start_height, l, this); + return; + } + } +}; + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/structures.hpp b/volatile/engine/structures.hpp new file mode 100644 index 00000000..8161f492 --- /dev/null +++ b/volatile/engine/structures.hpp @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include + +#include "alias.hpp" +#include "logger.hpp" +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { + +enum class PointerType : uint8_t { + // Value uninitialized considered as Invalid + Invalid = 0, + // Point to a string record + StringRecord = 1, + // Point to a doubly linked record + DLRecord = 2, + // Point to a dram skiplist node object + SkiplistNode = 3, + // Point to a dram Skiplist object + Skiplist = 4, + // Hash + HashList = 5, + // Element in Hash + HashElem = 6, + // List + List = 7, + // Point to a hash entry of hash table + HashEntry = 8, + // Allocated for later insertion + Allocated, + // Empty which point to nothing + Empty = 10, +}; + +// A pointer with additional information on high 16 bits +template +class PointerWithTag { + public: + static constexpr uint64_t kPointerMask = (((uint64_t)1 << 48) - 1); + + // TODO: Maybe explicit + PointerWithTag(PointerType* pointer) : tagged_pointer_((uint64_t)pointer) { + assert(sizeof(TagType) <= 2); + } + + explicit PointerWithTag(PointerType* pointer, TagType tag) + : tagged_pointer_((uint64_t)pointer | ((uint64_t)tag << 48)) { + assert(sizeof(TagType) <= 2); + } + + PointerWithTag() : tagged_pointer_(0) {} + + PointerType* RawPointer() { + return (PointerType*)(tagged_pointer_ & kPointerMask); + } + + const PointerType* RawPointer() const { + return (const PointerType*)(tagged_pointer_ & kPointerMask); + } + + bool Null() const { return RawPointer() == nullptr; } + + TagType GetTag() const { return static_cast(tagged_pointer_ >> 48); } + + void ClearTag() { tagged_pointer_ &= kPointerMask; } + + void SetTag(TagType tag) { tagged_pointer_ |= ((uint64_t)tag << 48); } + + const PointerType& operator*() const { return *RawPointer(); } + + PointerType& operator*() { return *(RawPointer()); } + + const PointerType* operator->() const { return RawPointer(); } + + PointerType* operator->() { return RawPointer(); } + + bool operator==(const PointerType* raw_pointer) { + return RawPointer() == raw_pointer; + } + + bool operator==(const PointerType* raw_pointer) const { + return RawPointer() == raw_pointer; + } + + private: + uint64_t tagged_pointer_; +}; +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/thread_manager.cpp b/volatile/engine/thread_manager.cpp new file mode 100644 index 00000000..a6510fbc --- /dev/null +++ b/volatile/engine/thread_manager.cpp @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "thread_manager.hpp" + +#include + +namespace KVDK_NAMESPACE { + +constexpr size_t kMaxRecycleID = 1024; + +std::shared_ptr ThreadManager::manager_(new ThreadManager); + +Thread::~Thread() { + if (manager != nullptr) { + manager->Release(*this); + } +} + +void ThreadManager::MaybeInitThread(Thread& t) { + if (t.id < 0) { + if (!recycle_id_.empty()) { + std::lock_guard lg(spin_); + if (!recycle_id_.empty()) { + auto it = recycle_id_.begin(); + t.manager = shared_from_this(); + t.id = *it; + recycle_id_.erase(it); + return; + } + } + int id = ids_.fetch_add(1, std::memory_order_relaxed); + t.manager = shared_from_this(); + t.id = id; + } +} + +void ThreadManager::Release(Thread& t) { + if (t.manager.get() == this && t.id >= 0 && + recycle_id_.size() < kMaxRecycleID) { + std::lock_guard lg(spin_); + recycle_id_.insert(t.id); + } + t.id = -1; + t.manager = nullptr; +} + +thread_local Thread this_thread; + +} // namespace KVDK_NAMESPACE diff --git a/engine/thread_manager.hpp b/volatile/engine/thread_manager.hpp similarity index 96% rename from engine/thread_manager.hpp rename to volatile/engine/thread_manager.hpp index 741f4122..cddae75e 100644 --- a/engine/thread_manager.hpp +++ b/volatile/engine/thread_manager.hpp @@ -8,7 +8,7 @@ #include #include "alias.hpp" -#include "kvdk/engine.hpp" +#include "kvdk/volatile/engine.hpp" #include "utils/utils.hpp" namespace KVDK_NAMESPACE { diff --git a/volatile/engine/utils/codec.hpp b/volatile/engine/utils/codec.hpp new file mode 100644 index 00000000..cfa89c35 --- /dev/null +++ b/volatile/engine/utils/codec.hpp @@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include + +#include "../alias.hpp" +#include "../macros.hpp" + +namespace KVDK_NAMESPACE { + +inline std::string EncodeUint64(uint64_t value) { + return std::string((char*)&value, sizeof(uint64_t)); +} + +inline bool DecodeUint64(const StringView& src, uint64_t* value) { + if (src.size() < sizeof(uint64_t)) { + return false; + } + *value = *(uint64_t*)src.data(); + return true; +} + +inline void AppendUint64(std::string* dst, uint64_t value) { + dst->append((char*)&value, sizeof(uint64_t)); +} + +inline bool FetchUint64(StringView* src, uint64_t* value) { + if (src->size() < sizeof(uint64_t)) { + return false; + } + *value = *(uint64_t*)src->data(); + *src = StringView(src->data() + sizeof(uint64_t), + src->size() - sizeof(uint64_t)); + return true; +} + +inline std::string EncodeUint32(uint32_t value) { + return std::string((char*)&value, sizeof(uint32_t)); +} + +inline bool DecodeUint32(const StringView& src, uint32_t* value) { + if (src.size() < sizeof(uint32_t)) { + return false; + } + *value = *(uint32_t*)src.data(); + return true; +} + +inline void AppendUint32(std::string* dst, uint32_t value) { + dst->append((char*)&value, sizeof(uint32_t)); +} + +inline bool FetchUint32(StringView* src, uint32_t* value) { + if (src->size() < sizeof(uint32_t)) { + return false; + } + *value = *(uint32_t*)src->data(); + *src = StringView(src->data() + sizeof(uint32_t), + src->size() - sizeof(uint32_t)); + return true; +} + +// POD stands for Plain-old-data. +// We don't serialize object which may have pointers to other objects. +template +inline void AppendPOD(std::string* dst, T const& value) { + dst->append(reinterpret_cast(&value), sizeof(T)); +} + +template +inline bool FetchPOD(StringView* src, T* ret) { + if (src->size() < sizeof(T)) { + return false; + } + *ret = *reinterpret_cast(src->data()); + *src = StringView{src->data() + sizeof(T), src->size() - sizeof(T)}; + return true; +} + +template +inline T FetchPOD(StringView* src) { + T ret; + bool success = FetchPOD(src, &ret); + kvdk_assert(success, ""); + return ret; +} + +inline void AppendFixedString(std::string* dst, const StringView& str) { + AppendUint32(dst, str.size()); + dst->append(str.data(), str.size()); +} + +inline bool FetchFixedString(StringView* src, std::string* value) { + uint32_t size; + bool ret = FetchUint32(src, &size) && src->size() >= size; + if (ret) { + value->assign(src->data(), size); + *src = StringView(src->data() + size, src->size() - size); + } + return ret; +} + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/utils/sync_impl.hpp b/volatile/engine/utils/sync_impl.hpp new file mode 100644 index 00000000..28a79ede --- /dev/null +++ b/volatile/engine/utils/sync_impl.hpp @@ -0,0 +1,161 @@ +/* Copyright (c) 2011-present, Facebook, Inc. All rights reserved. + * This source code is licensed under both the GPLv2 (found in the + * COPYING file in the root directory) and Apache 2.0 License + * (found in the LICENSE.Apache file in the root directory). + */ + +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../alias.hpp" + +#if KVDK_DEBUG_LEVEL > 0 +namespace KVDK_NAMESPACE { + +struct SyncPointPair { + std::string producer; + std::string consumer; +}; + +struct SyncImpl { + class CrashPoint : public std::runtime_error { + public: + CrashPoint(std::string const& msg) : base{msg} {} + + private: + using base = std::runtime_error; + using base::what; + }; + + SyncImpl() : ready_(false) {} + void LoadDependency(const std::vector& dependencies) { + std::lock_guard lock(mutex_); + consumers_.clear(); + producers_.clear(); + cleared_points_.clear(); + for (const auto& dependency : dependencies) { + consumers_[dependency.producer].push_back(dependency.consumer); + producers_[dependency.consumer].push_back(dependency.producer); + point_table_.insert(dependency.consumer); + point_table_.insert(dependency.producer); + } + cv_.notify_all(); + } + + void EnableProcessing() { ready_ = true; } + + void DisableProcessing() { ready_ = false; } + + void Process(const std::string& point, void* func_arg) { + if (!ready_) { + return; + } + + if (point_table_.find(point) == point_table_.end()) { + return; + } + + std::unique_lock lock(mutex_); + + while (!IsClearedAllproducers(point)) { + cv_.wait(lock); + } + + auto callback_pair = callbacks_.find(point); + if (callback_pair != callbacks_.end()) { + num_callbacks_running_++; + mutex_.unlock(); + callback_pair->second(func_arg); + mutex_.lock(); + num_callbacks_running_--; + } + cleared_points_.insert(point); + + cv_.notify_all(); + } + + void SetCallBack(const std::string& point, + const std::function& callback) { + std::lock_guard lock(mutex_); + callbacks_[point] = callback; + point_table_.insert(point); + } + + void EnableCrashPoint(std::string const& name) { crash_points_.insert(name); } + + void Crash(std::string const& name, std::string const& msg) { + if (!ready_) { + return; + } + + if (crash_points_.find(name) == crash_points_.end()) { + return; + } + + throw CrashPoint{msg}; + } + + void ClearAllCallBacks() { + std::unique_lock lock(mutex_); + while (num_callbacks_running_ > 0) { + cv_.wait(lock); + } + callbacks_.clear(); + } + + bool IsClearedAllproducers(const std::string& point) { + for (const std::string& producer : producers_[point]) { + if (cleared_points_.find(producer) == cleared_points_.end()) return false; + } + return true; + } + + void ClearDependTrace() { + std::lock_guard lock(mutex_); + cleared_points_.clear(); + } + + void Reset() { + std::lock_guard lock(mutex_); + cleared_points_.clear(); + consumers_.clear(); + producers_.clear(); + callbacks_.clear(); + point_table_.clear(); + crash_points_.clear(); + num_callbacks_running_ = 0; + } + + virtual ~SyncImpl() {} + + private: + std::mutex mutex_; + std::condition_variable cv_; + std::atomic ready_; + std::unordered_set point_table_; + std::unordered_set crash_points_; + int num_callbacks_running_ = 0; + + std::unordered_map> consumers_; + std::unordered_map> producers_; + std::unordered_set cleared_points_; + std::unordered_map> callbacks_; +}; + +} // namespace KVDK_NAMESPACE + +#endif diff --git a/volatile/engine/utils/sync_point.cpp b/volatile/engine/utils/sync_point.cpp new file mode 100644 index 00000000..33daf0cf --- /dev/null +++ b/volatile/engine/utils/sync_point.cpp @@ -0,0 +1,57 @@ +/* Copyright (c) 2011-present, Facebook, Inc. All rights reserved. + * This source code is licensed under both the GPLv2 (found in the + * COPYING file in the root directory) and Apache 2.0 License + * (found in the LICENSE.Apache file in the root directory). + */ + +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "sync_point.hpp" + +#if KVDK_DEBUG_LEVEL > 0 + +namespace KVDK_NAMESPACE { +SyncPoint* SyncPoint::GetInstance() { + static SyncPoint sync_point; + return &sync_point; +} +SyncPoint::SyncPoint() : sync_impl_(new SyncImpl){}; + +SyncPoint::~SyncPoint() { delete sync_impl_; } + +void SyncPoint::Process(const std::string& point, void* func_arg) { + sync_impl_->Process(point, func_arg); +} + +void SyncPoint::LoadDependency(const std::vector& dependencies) { + sync_impl_->LoadDependency(dependencies); +} + +void SyncPoint::EnableCrashPoint(std::string const& name) { + sync_impl_->EnableCrashPoint(name); +} + +void SyncPoint::Crash(std::string const& name, std::string const& msg) { + sync_impl_->Crash(name, msg); +} + +void SyncPoint::EnableProcessing() { sync_impl_->EnableProcessing(); } + +void SyncPoint::DisableProcessing() { sync_impl_->DisableProcessing(); } + +void SyncPoint::SetCallBack(const std::string& point, + const std::function& callback) { + sync_impl_->SetCallBack(point, callback); +} + +void SyncPoint::ClearAllCallBacks() { sync_impl_->ClearAllCallBacks(); } + +void SyncPoint::ClearDependTrace() { sync_impl_->ClearDependTrace(); } + +void SyncPoint::Reset() { sync_impl_->Reset(); } + +} // namespace KVDK_NAMESPACE + +#endif diff --git a/volatile/engine/utils/sync_point.hpp b/volatile/engine/utils/sync_point.hpp new file mode 100644 index 00000000..c0968d85 --- /dev/null +++ b/volatile/engine/utils/sync_point.hpp @@ -0,0 +1,84 @@ +/* Copyright (c) 2011-present, Facebook, Inc. All rights reserved. + * This source code is licensed under both the GPLv2 (found in the + * COPYING file in the root directory) and Apache 2.0 License + * (found in the LICENSE.Apache file in the root directory). + */ + +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sync_impl.hpp" + +#if KVDK_DEBUG_LEVEL > 0 + +namespace KVDK_NAMESPACE { + +/* SyncPoint Guide: + * Developer could specify sync points in the codebase by TEST_SYNC_POINT. + * Each sync point represents a position in the execution stream of a thread. + * In the uint test, developer can set the relationship between the sync point + * by LoadDependency to reproduce a desired interleave of threads execution. + * Also can set the execution of the sync point by SetCallBack. Please see the + * example in the unit tests. + */ +class SyncPoint { + public: + using CrashPoint = SyncImpl::CrashPoint; + static SyncPoint* GetInstance(); + SyncPoint(const SyncPoint&) = delete; + SyncPoint& operator=(const SyncPoint&) = delete; + + ~SyncPoint(); + + void LoadDependency(const std::vector& dependencies); + void EnableProcessing(); + void DisableProcessing(); + + void SetCallBack(const std::string& point, + const std::function& callback); + + void EnableCrashPoint(std::string const& name); + + void Crash(std::string const& name, std::string const& msg); + + void ClearAllCallBacks(); + + void ClearDependTrace(); + + void Process(const std::string& point, void* func_arg = nullptr); + + void Reset(); + + private: + SyncPoint(); + SyncImpl* sync_impl_; +}; + +} // namespace KVDK_NAMESPACE + +#define TEST_SYNC_POINT(x) KVDK_NAMESPACE::SyncPoint::GetInstance()->Process(x) +#define TEST_SYNC_POINT_CALLBACK(x, y) \ + KVDK_NAMESPACE::SyncPoint::GetInstance()->Process(x, y) +#define TEST_CRASH_POINT(name, msg) \ + KVDK_NAMESPACE::SyncPoint::GetInstance()->Crash(name, msg) + +#else +#define TEST_SYNC_POINT(x) +#define TEST_SYNC_POINT_CALLBACK(x, y) +#define TEST_CRASH_POINT(name, msg) +#endif diff --git a/volatile/engine/utils/utils.cpp b/volatile/engine/utils/utils.cpp new file mode 100644 index 00000000..2b9a38e2 --- /dev/null +++ b/volatile/engine/utils/utils.cpp @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "utils.hpp" + +#include + +#include "../logger.hpp" + +namespace KVDK_NAMESPACE { + +int get_usable_pu(void) { + hwloc_topology_t topology; + hwloc_bitmap_t set; + int err; + + /* create a topology */ + err = hwloc_topology_init(&topology); + if (err < 0) { + GlobalLogger.Error("Failed to initialize the topology\n"); + return err; + } + err = hwloc_topology_load(topology); + if (err < 0) { + GlobalLogger.Error("Failed to load the topology\n"); + hwloc_topology_destroy(topology); + return err; + } + + /* retrieve the entire set of available PUs */ + [[gnu::unused]] hwloc_const_bitmap_t cset_available = + hwloc_topology_get_topology_cpuset(topology); + + /* retrieve the CPU binding of the current entire process */ + set = hwloc_bitmap_alloc(); + if (!set) { + GlobalLogger.Error("Failed to allocate a bitmap\n"); + hwloc_topology_destroy(topology); + return err; + } + err = hwloc_get_cpubind(topology, set, HWLOC_CPUBIND_PROCESS); + if (err < 0) { + GlobalLogger.Error("Failed to get cpu binding\n"); + hwloc_bitmap_free(set); + hwloc_topology_destroy(topology); + return err; + } + int pu = hwloc_bitmap_weight(set); + hwloc_topology_destroy(topology); + return pu; +} +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/utils/utils.hpp b/volatile/engine/utils/utils.hpp new file mode 100644 index 00000000..eb2c0d53 --- /dev/null +++ b/volatile/engine/utils/utils.hpp @@ -0,0 +1,416 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define XXH_INLINE_ALL +#include "xxhash.h" +#undef XXH_INLINE_ALL + +#include "../alias.hpp" +#include "../macros.hpp" +#include "codec.hpp" + +namespace KVDK_NAMESPACE { + +// A tool struct that call f on destroy +// use the macro defer(code) to do so +template +struct Defer { + F f; + Defer(F f) : f(f) {} + ~Defer() { f(); } +}; +template +Defer defer_func(F f) { + return Defer(f); +} +#define DEFER_1(x, y) x##y +#define DEFER_2(x, y) DEFER_1(x, y) +#define DEFER_3(x) DEFER_2(x, __COUNTER__) +#define defer(code) auto DEFER_3(_defer_) = defer_func([&]() { code; }) + +inline void atomic_memcpy_16(void* dst, void* src) { + ((std::atomic<__uint128_t>*)dst) + ->store(((std::atomic<__uint128_t>*)src)->load()); +} + +inline uint64_t hash_str(const char* str, uint64_t size) { + return XXH3_64bits(str, size); +} + +inline uint64_t xxh_hash(std::string const& val) { + return XXH3_64bits(val.data(), val.size()); +} + +inline uint64_t get_checksum(const void* data, uint64_t size) { + return XXH3_64bits(data, size); +} + +inline unsigned long long get_seed() { + unsigned long long seed = 0; + while (seed == 0 && _rdseed64_step(&seed) != 1) { + } + return seed; +} + +inline uint64_t fast_random_64() { + static std::mt19937_64 generator; + thread_local uint64_t seed = 0; + if (seed == 0) { + seed = generator(); + } + uint64_t x = seed; /* The state must be seeded with a nonzero value. */ + x ^= x >> 12; // a + x ^= x << 25; // b + x ^= x >> 27; // c + seed = x; + return x * 0x2545F4914F6CDD1D; +} + +inline uint64_t rdtsc() { + uint32_t lo, hi; + __asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi)); + return ((uint64_t)lo) | (((uint64_t)hi) << 32); +} + +inline void atomic_load_16(void* dst, const void* src) { + (*(__uint128_t*)dst) = __atomic_load_16(src, std::memory_order_relaxed); +} + +inline void atomic_store_16(void* dst, const void* src) { + __atomic_store_16(dst, (*(__uint128_t*)src), std::memory_order_relaxed); +} + +inline void memcpy_16(void* dst, const void* src) { + __m128i m0 = _mm_loadu_si128(((const __m128i*)src) + 0); + _mm_storeu_si128(((__m128i*)dst) + 0, m0); +} + +inline void memcpy_8(void* dst, const void* src) { + *((uint64_t*)dst) = *((uint64_t*)src); +} + +inline void memcpy_4(void* dst, const void* src) { + *((uint32_t*)dst) = *((uint32_t*)src); +} + +inline void memcpy_2(void* dst, const void* src) { + *((uint16_t*)dst) = *((uint16_t*)src); +} + +inline void memcpy_1(void* dst, const void* src) { + *((uint8_t*)dst) = *((uint8_t*)src); +} + +inline std::string format_dir_path(const std::string& dir) { + return dir.back() == '/' ? dir : dir + "/"; +} + +inline bool file_exist(const std::string& name) { + bool exist = access(name.c_str(), 0) == 0; + return exist; +} + +inline int create_dir_if_missing(const std::string& name) { + int res = mkdir(name.c_str(), 0755) != 0; + if (res != 0) { + if (errno != EEXIST) { + return res; + } else { + struct stat s; + if (stat(name.c_str(), &s) == 0) { + return S_ISDIR(s.st_mode) ? 0 : res; + } + } + } + return res; +} + +inline std::string string_view_2_string(const StringView& src) { + return std::string(src.data(), src.size()); +} + +inline int compare_string_view(const StringView& src, + const StringView& target) { + auto size = std::min(src.size(), target.size()); + for (uint32_t i = 0; i < size; i++) { + if (src[i] != target[i]) { + return (unsigned char)src[i] - (unsigned char)target[i]; + } + } + return src.size() - target.size(); +} + +inline bool equal_string_view(const StringView& src, const StringView& target) { + if (src.size() == target.size()) { + return compare_string_view(src, target) == 0; + } + return false; +} + +class SpinMutex { + private: + std::atomic_flag locked_ = ATOMIC_FLAG_INIT; + + public: + SpinMutex() = default; + + void lock() { + while (!try_lock()) { + for (size_t i = 0; i != 64; ++i) { + _mm_pause(); + } + } + } + + void unlock() { locked_.clear(std::memory_order_release); } + + bool try_lock() { + if (locked_.test_and_set(std::memory_order_acquire)) { + return false; + } + return true; + } + + SpinMutex(const SpinMutex& s) = delete; + SpinMutex(SpinMutex&& s) = delete; + SpinMutex& operator=(const SpinMutex& s) = delete; +}; + +template +class SharedLock { + public: + explicit SharedLock(SharedMutex& mu) : device_{&mu} { + device_->lock_shared(); + } + + ~SharedLock() { + if (device_ != nullptr) { + device_->unlock_shared(); + } + } + + SharedLock(SharedLock const& other) : device_{other.device_} { + device_->lock_shared(); + } + + SharedLock& operator=(SharedLock const& other) { + if (device_ != nullptr) { + device_->unlock_shared(); + } + device_ = other.device_; + device_->lock_shared(); + return *this; + } + + SharedLock(SharedLock&& other) : device_{nullptr} { swap(other); } + + SharedLock& operator=(SharedLock&& other) { + if (device_ != nullptr) { + device_->unlock_shared(); + device_ = nullptr; + } + swap(other); + return *this; + } + + void swap(SharedLock& other) { + using std::swap; + swap(device_, other.device_); + } + + private: + SharedMutex* device_; +}; + +template +SharedLock LockShared(SharedMutex& mu) { + return SharedLock{mu}; +} + +class RWLock { + static constexpr std::int64_t reader_val{1}; + static constexpr std::int64_t writer_val{ + std::numeric_limits::min()}; + + // device.load() > 0 indicates only reader exists + // device.load() == 0 indicates no reader and no writer + // device.load() == writer_val indicates only writer exists, block readers + // Otherwise, writer has registered and is waiting for readers to leave + std::atomic_int64_t device{0}; + + public: + bool try_lock_shared() { + std::int64_t old = device.load(); + if (old < 0 || !device.compare_exchange_strong(old, old + reader_val)) { + // Other writer has acquired lock. + return false; + } + return true; + } + + void lock_shared() { + while (!try_lock_shared()) { + pause(); + } + return; + } + + void unlock_shared() { + device.fetch_sub(reader_val); + return; + } + + bool try_lock() { + std::int64_t old = device.load(); + if (old < 0 || !device.compare_exchange_strong(old, old + writer_val)) { + // Other writer has acquired lock. + return false; + } + while (device.load() != writer_val) { + // Block until all readers have left. + pause(); + } + return true; + } + + void lock() { + while (!try_lock()) { + pause(); + } + return; + } + + void unlock() { + device.fetch_sub(writer_val); + return; + } + + private: + void pause() { + for (size_t i = 0; i < 64; i++) { + _mm_pause(); + } + } +}; + +class Slice { + public: + Slice() : data_(nullptr), size_(0) {} + Slice(const char* data) : data_(data) { size_ = strlen(data_); } + Slice(const char* data, uint64_t size) : data_(data), size_(size) {} + + Slice(const std::string& str) : data_(str.data()), size_(str.size()) {} + Slice(const StringView& sv) : data_(sv.data()), size_(sv.size()) {} + + const char* data() const { return data_; } + + uint64_t& size() { return size_; } + + uint64_t size() const { return size_; } + + bool operator==(const Slice& b) { + if (b.size() == this->size_ && + memcmp(this->data_, b.data(), b.size()) == 0) { + return true; + } else { + return false; + } + } + + static int compare(const Slice& src, const Slice& target) { + auto size = std::min(src.size(), target.size()); + for (uint32_t i = 0; i < size; i++) { + if (src.data()[i] != target.data()[i]) { + return src.data()[i] - target.data()[i]; + } + } + return src.size() - target.size(); + } + + std::string to_string() { return std::string(data_, size_); } + + std::string to_string() const { return std::string(data_, size_); } + + private: + const char* data_; + uint64_t size_; +}; + +template +void compare_excange_if_larger(std::atomic& num, T target) { + while (true) { + T n = num.load(std::memory_order_relaxed); + if (n <= target) { + if (!num.compare_exchange_strong(n, target)) { + continue; + } + } + break; + } +} + +// Return the number of process unit (PU) that are bound to the kvdk instance +int get_usable_pu(void); + +namespace TimeUtils { +/* Return the UNIX time in microseconds */ +inline UnixTimeType unix_time(void) { + struct timeval tv; + UnixTimeType ust; + + gettimeofday(&tv, NULL); + ust = ((UnixTimeType)tv.tv_sec) * 1000000; + ust += tv.tv_usec; + return ust; +} + +inline int64_t millisecond_time() { return unix_time() / 1000; } + +inline bool CheckIsExpired(ExpireTimeType expired_time) { + if (expired_time >= 0 && expired_time <= millisecond_time()) { + return true; + } + return false; +} + +inline bool CheckTTL(TTLType ttl_time, UnixTimeType base_time) { + if (ttl_time != kPersistTime && + /* check overflow*/ ttl_time > INT64_MAX - base_time) { + return false; + } + return true; +} + +inline ExpireTimeType TTLToExpireTime( + TTLType ttl_time, UnixTimeType base_time = millisecond_time()) { + return ttl_time == kPersistTime ? kPersistTime : ttl_time + base_time; +} + +inline TTLType ExpireTimeToTTL(ExpireTimeType expire_time, + UnixTimeType base_time = millisecond_time()) { + return expire_time == kPersistTime ? kPersistTime : expire_time - base_time; +} + +} // namespace TimeUtils +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/version/old_records_cleaner.cpp b/volatile/engine/version/old_records_cleaner.cpp new file mode 100644 index 00000000..b3baa8db --- /dev/null +++ b/volatile/engine/version/old_records_cleaner.cpp @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include "old_records_cleaner.hpp" + +#include "../kv_engine.hpp" +#include "../sorted_collection/skiplist.hpp" + +namespace KVDK_NAMESPACE { + +void OldRecordsCleaner::TryGlobalClean() { + std::vector space_to_free; + // Update recorded oldest snapshot up to state so we can know which records + // can be freed + kv_engine_->version_controller_.UpdateLocalOldestSnapshot(); + TimestampType oldest_snapshot_ts = + kv_engine_->version_controller_.LocalOldestSnapshotTS(); + + std::vector free_entries; + for (size_t i = 0; i < cleaner_thread_cache_.size(); i++) { + auto& cleaner_thread_cache = cleaner_thread_cache_[i]; + if (cleaner_thread_cache.pending_free_space_entries.size() > 0) { + std::lock_guard lg(cleaner_thread_cache.old_records_lock); + for (auto& space_entry : + cleaner_thread_cache.pending_free_space_entries) { + if (space_entry.release_time < oldest_snapshot_ts) { + free_entries.emplace_back(space_entry.entry); + } else { + global_pending_free_space_entries_.emplace_back(space_entry); + } + } + cleaner_thread_cache.pending_free_space_entries.clear(); + } + } + + auto iter = global_pending_free_space_entries_.begin(); + while (iter != global_pending_free_space_entries_.end()) { + if (iter->release_time < oldest_snapshot_ts) { + free_entries.emplace_back(iter->entry); + iter++; + } else { + break; + } + } + global_pending_free_space_entries_.erase( + global_pending_free_space_entries_.begin(), iter); + + if (free_entries.size() > 0) { + kv_engine_->kv_allocator_->BatchFree(space_to_free); + } +} + +void OldRecordsCleaner::maybeUpdateOldestSnapshot() { + // To avoid too many records pending free, we upadte global smallest + // snapshot regularly. We update it every kUpdateSnapshotRound to mitigate + // the overhead + constexpr size_t kUpdateSnapshotRound = 10000; + thread_local size_t round = 0; + if ((++round) % kUpdateSnapshotRound == 0) { + kv_engine_->version_controller_.UpdateLocalOldestSnapshot(); + } +} + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/version/old_records_cleaner.hpp b/volatile/engine/version/old_records_cleaner.hpp new file mode 100644 index 00000000..69db72a9 --- /dev/null +++ b/volatile/engine/version/old_records_cleaner.hpp @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include + +#include "../alias.hpp" +#include "../collection.hpp" +#include "../hash_table.hpp" +#include "../kv_engine_cleaner.hpp" +#include "../thread_manager.hpp" +#include "../utils/utils.hpp" +#include "kvdk/volatile/configs.hpp" +#include "version_controller.hpp" + +namespace KVDK_NAMESPACE { +class KVEngine; + +// OldRecordsCleaner is used to clean old version data records of kvdk +// +// To support multi-version machenism and consistent backup of kvdk, +// the updated/deleted records need to be ramained for a while until they are +// not refered by any snapshot +class OldRecordsCleaner { + public: + OldRecordsCleaner(KVEngine* kv_engine, uint32_t max_access_threads) + : kv_engine_(kv_engine), cleaner_thread_cache_(max_access_threads) { + assert(kv_engine_ != nullptr); + } + + // Try to clean global old records + void TryGlobalClean(); + + private: + struct CleanerThreadCache { + std::deque pending_free_space_entries{}; + SpinMutex old_records_lock; + }; + const uint64_t kLimitCachedDeleteRecords = 1000000; + + void maybeUpdateOldestSnapshot(); + + KVEngine* kv_engine_; + + Array cleaner_thread_cache_; + + std::deque global_pending_free_space_entries_; +}; + +} // namespace KVDK_NAMESPACE diff --git a/volatile/engine/version/version_controller.hpp b/volatile/engine/version/version_controller.hpp new file mode 100644 index 00000000..531e41d7 --- /dev/null +++ b/volatile/engine/version/version_controller.hpp @@ -0,0 +1,342 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include "../alias.hpp" +#include "../allocator.hpp" +#include "../thread_manager.hpp" +#include "kvdk/volatile/configs.hpp" + +namespace KVDK_NAMESPACE { +constexpr TimestampType kMaxTimestamp = UINT64_MAX; + +struct SnapshotImpl : public Snapshot { + explicit SnapshotImpl(const TimestampType& t) : timestamp(t) {} + + SnapshotImpl() = default; + + TimestampType GetTimestamp() const { return timestamp; } + + TimestampType timestamp = kMaxTimestamp; + SnapshotImpl* prev = nullptr; + SnapshotImpl* next = nullptr; +}; + +// A SnapshotList is a linked-list of global snapshots, new older snapshot is +// linked at the tail of list +class SnapshotList { + public: + SnapshotList() : head_() { + head_.prev = &head_; + head_.next = &head_; + } + + SnapshotImpl* New(TimestampType ts) { + SnapshotImpl* impl = new SnapshotImpl(ts); + impl->prev = &head_; + impl->next = head_.next; + head_.next->prev = impl; + head_.next = impl; + return impl; + } + + void Delete(const SnapshotImpl* impl) { + impl->prev->next = impl->next; + impl->next->prev = impl->prev; + delete impl; + } + + TimestampType OldestSnapshotTS() { + return empty() ? kMaxTimestamp : head_.prev->GetTimestamp(); + } + + ~SnapshotList() { + SnapshotImpl* curr = head_.next; + while (curr != &head_) { + SnapshotImpl* tmp = curr->next; + delete curr; + curr = tmp; + } + } + + private: + bool empty() { return head_.prev == &head_; } + + SnapshotImpl head_; +}; + +// VersionController manages snapshots and timestamp of a KVDK instance +// The snapshots include temporal snapshots that cached by each access thread of +// kvdk instance, and a global snapshot list that actively created by user +class VersionController { + public: + // LocalSnapshotHolder is used by kvdk functions such as Get(), SortedGet() + // and HashGet() internally to guarantee lockless reads will always read out + // valid data. LocalSnapshotHolder is thread_local, and one thread can hold + // atmost one at same time. + class LocalSnapshotHolder { + VersionController* owner_{nullptr}; + TimestampType ts_{}; + + public: + LocalSnapshotHolder(VersionController* o) : owner_{o} { + owner_->HoldLocalSnapshot(); + ts_ = owner_ + ->version_thread_cache_[ThreadManager::ThreadID() % + owner_->version_thread_cache_.size()] + .holding_snapshot.timestamp; + }; + LocalSnapshotHolder(LocalSnapshotHolder const&) = delete; + LocalSnapshotHolder& operator=(LocalSnapshotHolder const&) = delete; + LocalSnapshotHolder(LocalSnapshotHolder&& other) { + *this = std::move(other); + } + LocalSnapshotHolder& operator=(LocalSnapshotHolder&& other) { + kvdk_assert(owner_ == nullptr, ""); + kvdk_assert(other.owner_ != nullptr, ""); + std::swap(owner_, other.owner_); + std::swap(ts_, other.ts_); + return *this; + } + ~LocalSnapshotHolder() { + if (owner_ != nullptr) { + owner_->ReleaseLocalSnapshot(); + } + } + TimestampType Timestamp() { return ts_; } + }; + + // GlobalSnapshotHolder is hold internally by iterators. + // Create GlobalSnapshotHolder is more costly then InteralToken. + class GlobalSnapshotHolder { + VersionController* owner_{nullptr}; + SnapshotImpl* snap_{nullptr}; + + public: + GlobalSnapshotHolder(VersionController* o) : owner_{o} { + snap_ = owner_->NewGlobalSnapshot(); + }; + GlobalSnapshotHolder(GlobalSnapshotHolder const&) = delete; + GlobalSnapshotHolder& operator=(GlobalSnapshotHolder const&) = delete; + GlobalSnapshotHolder(GlobalSnapshotHolder&& other) { + *this = std::move(other); + } + GlobalSnapshotHolder& operator=(GlobalSnapshotHolder&& other) { + kvdk_assert(owner_ == nullptr, ""); + kvdk_assert(other.owner_ != nullptr, ""); + kvdk_assert(snap_ == nullptr, ""); + kvdk_assert(other.snap_ != nullptr, ""); + std::swap(owner_, other.owner_); + std::swap(snap_, other.snap_); + return *this; + } + ~GlobalSnapshotHolder() { + if (owner_ != nullptr) { + owner_->ReleaseSnapshot(snap_); + } + } + TimestampType Timestamp() { return snap_->GetTimestamp(); } + }; + + class BatchWriteToken { + VersionController* owner_{nullptr}; + TimestampType ts_{kMaxTimestamp}; + + public: + BatchWriteToken(VersionController* o) + : owner_{o}, ts_{owner_->GetCurrentTimestamp()} { + ts_ = owner_->GetCurrentTimestamp(); + auto& tc = + owner_->version_thread_cache_[ThreadManager::ThreadID() % + owner_->version_thread_cache_.size()]; + kvdk_assert(tc.batch_write_ts == kMaxTimestamp, ""); + tc.batch_write_ts = ts_; + } + BatchWriteToken(BatchWriteToken const&) = delete; + BatchWriteToken& operator=(BatchWriteToken const&) = delete; + BatchWriteToken(BatchWriteToken&& other) { swap(other); } + BatchWriteToken& operator=(BatchWriteToken&& other) { + swap(other); + return *this; + } + ~BatchWriteToken() { + if (owner_ != nullptr) { + auto& tc = + owner_->version_thread_cache_[ThreadManager::ThreadID() % + owner_->version_thread_cache_.size()]; + tc.batch_write_ts = kMaxTimestamp; + } + } + TimestampType Timestamp() { return ts_; } + + private: + void swap(BatchWriteToken& other) { + kvdk_assert(owner_ == nullptr, ""); + kvdk_assert(other.owner_ != nullptr, ""); + kvdk_assert(ts_ == kMaxTimestamp, ""); + kvdk_assert(other.ts_ != kMaxTimestamp, ""); + std::swap(owner_, other.owner_); + std::swap(ts_, other.ts_); + } + }; + + public: + VersionController(uint64_t max_access_threads) + : version_thread_cache_(max_access_threads) {} + + void Init(uint64_t base_timestamp) { + tsc_on_startup_ = rdtsc(); + base_timestamp_ = base_timestamp; + UpdateLocalOldestSnapshot(); + } + + LocalSnapshotHolder GetLocalSnapshotHolder() { + return LocalSnapshotHolder{this}; + } + + BatchWriteToken GetBatchWriteToken() { return BatchWriteToken{this}; } + + std::shared_ptr GetGlobalSnapshotToken() { + return std::make_shared(this); + } + + inline void HoldLocalSnapshot() { + kvdk_assert(ThreadManager::ThreadID() >= 0, + "Uninitialized thread in NewLocalSnapshot"); + kvdk_assert(version_thread_cache_[ThreadManager::ThreadID() % + version_thread_cache_.size()] + .holding_snapshot.timestamp == kMaxTimestamp, + "Previous LocalSnapshot not released yet!"); + version_thread_cache_[ThreadManager::ThreadID() % + version_thread_cache_.size()] + .holding_snapshot.timestamp = GetCurrentTimestamp(); + } + + inline void ReleaseLocalSnapshot() { + kvdk_assert(ThreadManager::ThreadID() >= 0, + "Uninitialized thread in ReleaseLocalSnapshot"); + version_thread_cache_[ThreadManager::ThreadID() % + version_thread_cache_.size()] + .holding_snapshot.timestamp = kMaxTimestamp; + } + + inline const SnapshotImpl& GetLocalSnapshot(size_t thread_num) { + kvdk_assert(thread_num < version_thread_cache_.size(), + "Wrong thread num in GetLocalSnapshot"); + return version_thread_cache_[thread_num].holding_snapshot; + } + + inline const SnapshotImpl& GetLocalSnapshot() { + kvdk_assert(ThreadManager::ThreadID() >= 0, + "Uninitialized thread in GetLocalSnapshot"); + return version_thread_cache_[ThreadManager::ThreadID() % + version_thread_cache_.size()] + .holding_snapshot; + } + + // Create a new global snapshot + SnapshotImpl* NewGlobalSnapshot(bool may_block = true) { + TimestampType ts = GetCurrentTimestamp(); + if (may_block) { + for (size_t i = 0; i < version_thread_cache_.size(); i++) { + while (ts >= version_thread_cache_[i].batch_write_ts) { + _mm_pause(); + } + } + } else { + for (size_t i = 0; i < version_thread_cache_.size(); i++) { + TimestampType batch_ts = version_thread_cache_[i].batch_write_ts; + ts = std::min(ts, batch_ts - 1); + } + } + + std::lock_guard lg(global_snapshots_lock_); + SnapshotImpl* new_global_snapshot = global_snapshots_.New(ts); + global_oldest_snapshot_ts_ = global_snapshots_.OldestSnapshotTS(); + return new_global_snapshot; + } + + // Release a global snapshot, it should be created by this instance + void ReleaseSnapshot(const SnapshotImpl* impl) { + std::lock_guard lg(global_snapshots_lock_); + global_snapshots_.Delete(impl); + global_oldest_snapshot_ts_ = global_snapshots_.OldestSnapshotTS(); + } + + inline TimestampType GetCurrentTimestamp() { + auto res = rdtsc() - tsc_on_startup_ + base_timestamp_; + return res; + } + + TimestampType LocalOldestSnapshotTS() { + return local_oldest_snapshot_.GetTimestamp(); + } + + TimestampType GlobalOldestSnapshotTs() { + return global_oldest_snapshot_ts_.load(); + } + + // Update recorded oldest snapshot up to state by iterating every thread + // holding snapshot + void UpdateLocalOldestSnapshot() { + // update local oldest snapshot + TimestampType ts = GetCurrentTimestamp(); + for (size_t i = 0; i < version_thread_cache_.size(); i++) { + auto& tc = version_thread_cache_[i]; + ts = std::min(tc.holding_snapshot.GetTimestamp(), ts); + } + local_oldest_snapshot_.timestamp = ts; + } + + private: + // Each access thread of the instance hold its own local snapshot in thread + // cache to avoid thread contention + struct alignas(64) VersionThreadCache { + VersionThreadCache() : holding_snapshot(kMaxTimestamp) {} + + SnapshotImpl holding_snapshot; + TimestampType batch_write_ts{kMaxTimestamp}; + char padding[64 - sizeof(holding_snapshot)]; + }; + + Array version_thread_cache_; + SnapshotList global_snapshots_; + SpinMutex global_snapshots_lock_; + // Known oldest snapshot of the instance, there is delay with the actual + // oldest snapshot until call UpdatedOldestSnapshot() + SnapshotImpl local_oldest_snapshot_{kMaxTimestamp}; + std::atomic global_oldest_snapshot_ts_{kMaxTimestamp}; + + // These two used to get current timestamp of the instance + // version_base_: The newest timestamp on instance closing last time + // tsc_on_startup_: The CPU tsc on instance start up + uint64_t base_timestamp_; + uint64_t tsc_on_startup_; +}; + +class CheckPoint { + public: + void MakeCheckpoint(const Snapshot* snapshot) { + checkpoint_ts_ = static_cast(snapshot)->GetTimestamp(); + } + + void MaybeRelease(const Snapshot* releasing_snapshot) { + if (static_cast(releasing_snapshot)->GetTimestamp() == + checkpoint_ts_) { + Release(); + } + } + + void Release() { checkpoint_ts_ = 0; } + + TimestampType CheckpointTS() { return checkpoint_ts_; } + + bool Valid() { return checkpoint_ts_ > 0; } + + private: + TimestampType checkpoint_ts_; +}; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/write_batch_impl.cpp b/volatile/engine/write_batch_impl.cpp new file mode 100644 index 00000000..53adcff2 --- /dev/null +++ b/volatile/engine/write_batch_impl.cpp @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include "write_batch_impl.hpp" + +#include "alias.hpp" + +namespace KVDK_NAMESPACE { + +void BatchWriteLog::EncodeTo(char* dst) { + // TODO + // This function should not be called without PMem support. + // Checking dst == nullptr is a temporary workaround. + if (dst == nullptr) { + return; + } + kvdk_assert(stage == Stage::Initializing, ""); + + size_t total_bytes; + total_bytes = + sizeof(total_bytes) + sizeof(timestamp_) + sizeof(stage) + + sizeof(string_logs_.size()) + + string_logs_.size() * sizeof(StringLogEntry) + + sizeof(sorted_logs_.size()) + + sorted_logs_.size() * sizeof(SortedLogEntry) + sizeof(hash_logs_.size()) + + hash_logs_.size() * sizeof(HashLogEntry) + sizeof(list_logs_.size()) + + list_logs_.size() * sizeof(ListLogEntry); + + std::string buffer; + buffer.reserve(total_bytes); + + AppendPOD(&buffer, total_bytes); + AppendPOD(&buffer, timestamp_); + AppendPOD(&buffer, stage); + + AppendPOD(&buffer, string_logs_.size()); + for (size_t i = 0; i < string_logs_.size(); i++) { + AppendPOD(&buffer, string_logs_[i]); + } + + AppendPOD(&buffer, sorted_logs_.size()); + for (size_t i = 0; i < sorted_logs_.size(); i++) { + AppendPOD(&buffer, sorted_logs_[i]); + } + + AppendPOD(&buffer, hash_logs_.size()); + for (size_t i = 0; i < hash_logs_.size(); i++) { + AppendPOD(&buffer, hash_logs_[i]); + } + + AppendPOD(&buffer, list_logs_.size()); + for (size_t i = 0; i < list_logs_.size(); i++) { + AppendPOD(&buffer, list_logs_[i]); + } + + kvdk_assert(buffer.size() == total_bytes, ""); + + memcpy(dst, buffer.data(), buffer.size()); + for (size_t i = 0; i < buffer.size(); i += 64) { + _mm_clflushopt(&dst[i]); + } + _mm_mfence(); +} + +void BatchWriteLog::DecodeFrom(char const* src) { + if (src == nullptr) { + return; + } + kvdk_assert( + string_logs_.empty() && sorted_logs_.empty() && hash_logs_.empty(), ""); + + size_t total_bytes = *reinterpret_cast(src); + if (total_bytes == 0) { + return; + } + + StringView sw{src, total_bytes}; + + total_bytes = FetchPOD(&sw); + timestamp_ = FetchPOD(&sw); + stage = FetchPOD(&sw); + + if (stage == Stage::Initializing || stage == Stage::Committed) { + // No need to deserialize furthermore. + return; + } + if (stage != Stage::Processing) { + kvdk_assert(false, "Invalid Stage, invalid Log!"); + return; + } + + string_logs_.resize(FetchPOD(&sw)); + for (size_t i = 0; i < string_logs_.size(); i++) { + string_logs_[i] = FetchPOD(&sw); + } + + sorted_logs_.resize(FetchPOD(&sw)); + for (size_t i = 0; i < sorted_logs_.size(); i++) { + sorted_logs_[i] = FetchPOD(&sw); + } + + hash_logs_.resize(FetchPOD(&sw)); + for (size_t i = 0; i < hash_logs_.size(); i++) { + hash_logs_[i] = FetchPOD(&sw); + } + + list_logs_.resize(FetchPOD(&sw)); + for (size_t i = 0; i < list_logs_.size(); i++) { + list_logs_[i] = FetchPOD(&sw); + } + + kvdk_assert(sw.size() == 0, ""); +} + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/engine/write_batch_impl.hpp b/volatile/engine/write_batch_impl.hpp new file mode 100644 index 00000000..848f3586 --- /dev/null +++ b/volatile/engine/write_batch_impl.hpp @@ -0,0 +1,311 @@ +#pragma once + +#include + +#include +#include +#include + +#include "alias.hpp" +#include "hash_table.hpp" +#include "kvdk/volatile/write_batch.hpp" +#include "utils/codec.hpp" +#include "utils/utils.hpp" + +namespace KVDK_NAMESPACE { + +class WriteBatchImpl final : public WriteBatch { + public: + struct StringOp { + StringOp(WriteOp o, const StringView& k, const StringView& v) + : op(o), key(string_view_2_string(k)), value(string_view_2_string(v)) {} + + WriteOp op; + std::string key; + std::string value; + }; + + struct SortedOp { + SortedOp(WriteOp o, const StringView& c, const StringView& k, + const StringView& v) + : op(o), + collection(string_view_2_string(c)), + key(string_view_2_string(k)), + value(string_view_2_string(v)) {} + + WriteOp op; + std::string collection; + std::string key; + std::string value; + }; + + struct HashOp { + HashOp(WriteOp o, const StringView& c, const StringView& k, + const StringView& v) + : op(o), + collection(string_view_2_string(c)), + key(string_view_2_string(k)), + value(string_view_2_string(v)) {} + + WriteOp op; + std::string collection; + std::string key; + std::string value; + }; + + struct HashEq { + size_t operator()(StringOp const& string_op) const { + return xxh_hash(string_op.key); + } + size_t operator()(SortedOp const& sorted_op) const { + return xxh_hash(sorted_op.collection) ^ xxh_hash(sorted_op.key); + } + size_t operator()(HashOp const& hash_op) const { + return xxh_hash(hash_op.collection) ^ xxh_hash(hash_op.key); + } + bool operator()(StringOp const& lhs, StringOp const& rhs) const { + return lhs.key == rhs.key; + } + bool operator()(SortedOp const& lhs, SortedOp const& rhs) const { + return lhs.collection == rhs.collection && lhs.key == rhs.key; + } + bool operator()(HashOp const& lhs, HashOp const& rhs) const { + return lhs.collection == rhs.collection && lhs.key == rhs.key; + } + }; + + void StringPut(const StringView key, const StringView value) final { + StringOp op{WriteOp::Put, key, value}; + string_ops_.erase(op); + string_ops_.insert(op); + } + + void StringDelete(const StringView key) final { + StringOp op{WriteOp::Delete, key, std::string{}}; + string_ops_.erase(op); + string_ops_.insert(op); + } + + void SortedPut(const StringView collection, const StringView key, + const StringView value) final { + SortedOp op{WriteOp::Put, collection, key, value}; + sorted_ops_.erase(op); + sorted_ops_.insert(op); + } + + void SortedDelete(const StringView collection, const StringView key) final { + SortedOp op{WriteOp::Delete, collection, key, std::string{}}; + sorted_ops_.erase(op); + sorted_ops_.insert(op); + } + + void HashPut(const StringView collection, const StringView key, + const StringView value) final { + HashOp op{WriteOp::Put, collection, key, value}; + hash_ops_.erase(op); + hash_ops_.insert(op); + } + + void HashDelete(const StringView collection, const StringView key) final { + HashOp op{WriteOp::Delete, collection, key, std::string{}}; + hash_ops_.erase(op); + hash_ops_.insert(op); + } + + void Clear() final { + string_ops_.clear(); + sorted_ops_.clear(); + hash_ops_.clear(); + } + + size_t Size() const final { + return string_ops_.size() + sorted_ops_.size() + hash_ops_.size(); + } + + using StringOpBatch = std::unordered_set; + using SortedOpBatch = std::unordered_set; + using HashOpBatch = std::unordered_set; + + StringOpBatch const& StringOps() const { return string_ops_; } + SortedOpBatch const& SortedOps() const { return sorted_ops_; } + HashOpBatch const& HashOps() const { return hash_ops_; } + + private: + StringOpBatch string_ops_; + SortedOpBatch sorted_ops_; + HashOpBatch hash_ops_; +}; + +struct StringWriteArgs { + StringView key; + StringView value; + WriteOp op; + SpaceEntry space; + TimestampType ts; + HashTable::LookupResult res; + StringRecord* new_rec; + + void Assign(WriteBatchImpl::StringOp const& string_op) { + key = string_op.key; + value = string_op.value; + op = string_op.op; + } +}; + +class BatchWriteLog { + public: + // The batch is first persisted to PMem with Stage::Initializing. + // After persisting is done, it enters Stage::Processing. + // When all batches are executed, it is marked as Stage::Committed + // and then purged from PMem. + // During recovery, a batch in + // Stage::Initializing is directly discarded and purged. + // Stage::Processing is rolled back. + // Stage::Committed is directly purged. + enum class Stage : size_t { + // Initializing must be 0 so that empty file can be skipped. + Initializing = 0, + Processing, + Committed, + }; + + enum class Op : size_t { Put, Delete }; + + struct StringLogEntry { + Op op; + MemoryOffsetType offset; + }; + + struct SortedLogEntry { + Op op; + MemoryOffsetType offset; + }; + + struct HashLogEntry { + Op op; + MemoryOffsetType offset; + }; + + struct ListLogEntry { + Op op; + MemoryOffsetType offset; + }; + + explicit BatchWriteLog() {} + + void SetTimestamp(TimestampType ts) { timestamp_ = ts; } + + void StringPut(MemoryOffsetType offset) { + string_logs_.emplace_back(StringLogEntry{Op::Put, offset}); + } + + void StringDelete(MemoryOffsetType offset) { + string_logs_.emplace_back(StringLogEntry{Op::Delete, offset}); + } + + void SortedPut(MemoryOffsetType offset) { + sorted_logs_.emplace_back(SortedLogEntry{Op::Put, offset}); + } + + void SortedDelete(MemoryOffsetType offset) { + sorted_logs_.emplace_back(SortedLogEntry{Op::Delete, offset}); + } + + void HashPut(MemoryOffsetType offset) { + hash_logs_.emplace_back(HashLogEntry{Op::Put, offset}); + } + + void HashDelete(MemoryOffsetType offset) { + hash_logs_.emplace_back(HashLogEntry{Op::Delete, offset}); + } + + void ListEmplace(MemoryOffsetType offset) { + list_logs_.emplace_back(ListLogEntry{Op::Put, offset}); + } + + void ListDelete(MemoryOffsetType offset) { + list_logs_.emplace_back(ListLogEntry{Op::Delete, offset}); + } + + void Clear() { + string_logs_.clear(); + sorted_logs_.clear(); + hash_logs_.clear(); + list_logs_.clear(); + } + + size_t Size() const { + return string_logs_.size() + sorted_logs_.size() + hash_logs_.size() + + list_logs_.size(); + } + + static size_t Capacity() { return (1UL << 20); } + + static size_t MaxBytes() { + static_assert(sizeof(HashLogEntry) >= sizeof(StringLogEntry), ""); + static_assert(sizeof(HashLogEntry) >= sizeof(SortedLogEntry), ""); + static_assert(sizeof(HashLogEntry) >= sizeof(ListLogEntry), ""); + return sizeof(size_t) + sizeof(TimestampType) + sizeof(Stage) + + sizeof(size_t) + Capacity() * sizeof(HashLogEntry); + } + + // Format of the BatchWriteLog + // total_bytes | timestamp | stage | + // N | StringLogEntry*N | + // M | SortedLogEntry*M + // K | HashLogEntry*K + // L | ListLogEntry*K + // dst is expected to have capacity of MaxBytes(). + void EncodeTo(char* dst); + + void DecodeFrom(char const* src); + + static void MarkProcessing(char* dst) { + if (dst == nullptr) { + return; + } + dst = &dst[sizeof(size_t) + sizeof(TimestampType)]; + *reinterpret_cast(dst) = Stage::Processing; + _mm_clflush(dst); + _mm_mfence(); + } + + static void MarkCommitted(char* dst) { + if (dst == nullptr) { + return; + } + dst = &dst[sizeof(size_t) + sizeof(TimestampType)]; + *reinterpret_cast(dst) = Stage::Committed; + _mm_clflush(dst); + _mm_mfence(); + } + + // For rollback + static void MarkInitializing(char* dst) { + dst = &dst[sizeof(size_t) + sizeof(TimestampType)]; + *reinterpret_cast(dst) = Stage::Initializing; + _mm_clflush(dst); + _mm_mfence(); + } + + using StringLog = std::vector; + using SortedLog = std::vector; + using HashLog = std::vector; + using ListLog = std::vector; + + StringLog const& StringLogs() const { return string_logs_; } + SortedLog const& SortedLogs() const { return sorted_logs_; } + HashLog const& HashLogs() const { return hash_logs_; } + ListLog const& ListLogs() const { return list_logs_; } + TimestampType Timestamp() const { return timestamp_; } + + private: + Stage stage{Stage::Initializing}; + TimestampType timestamp_; + StringLog string_logs_; + SortedLog sorted_logs_; + HashLog hash_logs_; + ListLog list_logs_; +}; + +} // namespace KVDK_NAMESPACE diff --git a/volatile/examples/tutorial/CMakeLists.txt b/volatile/examples/tutorial/CMakeLists.txt new file mode 100644 index 00000000..9613747a --- /dev/null +++ b/volatile/examples/tutorial/CMakeLists.txt @@ -0,0 +1,18 @@ +if (NOT BUILD_TUTORIAL) + return() +endif() + +enable_testing() + +file(GLOB sources + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.c) + +foreach(src ${sources}) + file(RELATIVE_PATH src_rel_path ${CMAKE_CURRENT_SOURCE_DIR} ${src}) + string(REPLACE ".cpp" "" example_name ${src_rel_path}) + string(REPLACE ".c" "" example_name ${example_name}) + string(REGEX REPLACE "[/\\.]" "_" example_name ${example_name}) + add_executable(${example_name} ${src}) + target_link_libraries(${example_name} PUBLIC engine) +endforeach() diff --git a/volatile/examples/tutorial/c_api_tutorial.c b/volatile/examples/tutorial/c_api_tutorial.c new file mode 100644 index 00000000..af310b96 --- /dev/null +++ b/volatile/examples/tutorial/c_api_tutorial.c @@ -0,0 +1,766 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include +#include +#include +#include + +#include "kvdk/volatile/engine.h" + +// The KVDK instance is mounted as a directory +// /mnt/pmem0/tutorial_kvdk_example. +// Modify this path if necessary. +const char* pmem_path = "/mnt/pmem0/tutorial_kvdk_example"; + +static int StrCmp(const char* a, size_t alen, const char* b, size_t blen) { + int n = (alen < blen) ? alen : blen; + int r = memcmp(a, b, n); + if (r == 0) { + if (alen < blen) + r = -1; + else if (alen > blen) + r = +1; + } + return r; +} + +void AnonymousCollectionExample(KVDKEngine* kvdk_engine) { + const char* key1 = "key1"; + const char* key2 = "key2"; + const char* value1 = "value1"; + const char* value2 = "value2"; + char* read_v1; + char* read_v2; + size_t key1_len = strlen(key1); + size_t key2_len = strlen(key2); + size_t value1_len = strlen(value1); + size_t value2_len = strlen(value2); + size_t read_v1_len, read_v2_len; + int cmp; + KVDKWriteOptions* write_option = KVDKCreateWriteOptions(); + KVDKStatus s = + KVDKPut(kvdk_engine, key1, key1_len, value1, value1_len, write_option); + assert(s == Ok); + s = KVDKPut(kvdk_engine, key2, key1_len, value2, value2_len, write_option); + assert(s == Ok); + s = KVDKGet(kvdk_engine, key1, key1_len, &read_v1_len, &read_v1); + assert(s == Ok); + cmp = StrCmp(read_v1, read_v1_len, value1, value1_len); + assert(cmp == 0); + free(read_v1); + s = KVDKPut(kvdk_engine, key1, key1_len, value2, value2_len, write_option); + assert(s == Ok); + s = KVDKGet(kvdk_engine, key1, key1_len, &read_v1_len, &read_v1); + assert(s == Ok); + cmp = StrCmp(read_v1, read_v1_len, value2, value2_len); + assert(cmp == 0); + s = KVDKGet(kvdk_engine, key2, key2_len, &read_v2_len, &read_v2); + assert(s == Ok); + cmp = StrCmp(read_v2, read_v2_len, value2, value2_len); + assert(cmp == 0); + s = KVDKDelete(kvdk_engine, key1, key1_len); + assert(s == Ok); + s = KVDKDelete(kvdk_engine, key2, key2_len); + assert(s == Ok); + free(read_v1); + free(read_v2); + printf( + "Successfully performed Get, Put, Delete operations on anonymous " + "global collection.\n"); + + KVDKDestroyWriteOptions(write_option); +} + +// Reads and Writes on Named Sorted Collection +void SortedCollectionExample(KVDKEngine* kvdk_engine) { + const char* collection1 = "collection1"; + const char* collection2 = "collection2"; + const char* key1 = "key1"; + const char* key2 = "key2"; + const char* value1 = "value1"; + const char* value2 = "value2"; + char* read_v1; + char* read_v2; + size_t read_v1_len, read_v2_len; + int cmp; + + KVDKSortedCollectionConfigs* s_configs = KVDKCreateSortedCollectionConfigs(); + KVDKStatus s = KVDKSortedCreate(kvdk_engine, collection1, strlen(collection1), + s_configs); + assert(s == Ok); + s = KVDKSortedCreate(kvdk_engine, collection2, strlen(collection2), + s_configs); + assert(s == Ok); + s = KVDKSortedPut(kvdk_engine, collection1, strlen(collection1), key1, + strlen(key1), value1, strlen(value1)); + assert(s == Ok); + s = KVDKSortedPut(kvdk_engine, collection2, strlen(collection2), key2, + strlen(key2), value2, strlen(value2)); + assert(s == Ok); + s = KVDKSortedGet(kvdk_engine, collection1, strlen(collection1), key1, + strlen(key1), &read_v1_len, &read_v1); + assert(s == Ok); + cmp = StrCmp(read_v1, read_v1_len, value1, strlen(value1)); + assert(cmp == 0); + free(read_v1); + s = KVDKSortedPut(kvdk_engine, collection1, strlen(collection1), key1, + strlen(key1), value2, strlen(value2)); + assert(s == Ok); + s = KVDKSortedGet(kvdk_engine, collection1, strlen(collection1), key1, + strlen(key1), &read_v1_len, &read_v1); + assert(s == Ok); + cmp = StrCmp(read_v1, read_v1_len, value2, strlen(value2)); + assert(cmp == 0); + s = KVDKSortedGet(kvdk_engine, collection2, strlen(collection2), key2, + strlen(key2), &read_v2_len, &read_v2); + assert(s == Ok); + cmp = StrCmp(read_v2, read_v2_len, value2, strlen(value2)); + assert(cmp == 0); + s = KVDKSortedDelete(kvdk_engine, collection1, strlen(collection1), key1, + strlen(key1)); + assert(s == Ok); + s = KVDKSortedDelete(kvdk_engine, collection2, strlen(collection2), key2, + strlen(key2)); + assert(s == Ok); + s = KVDKSortedDestroy(kvdk_engine, collection1, strlen(collection1)); + assert(s == Ok); + s = KVDKSortedDestroy(kvdk_engine, collection2, strlen(collection2)); + assert(s == Ok); + s = KVDKSortedPut(kvdk_engine, collection1, strlen(collection1), key1, + strlen(key1), value1, strlen(value1)); + assert(s == NotFound); + free(read_v1); + free(read_v2); + s = KVDKSortedGet(kvdk_engine, collection1, strlen(collection1), key2, + strlen(key2), &read_v2_len, &read_v2); + assert(s == NotFound); + KVDKDestroySortedCollectionConfigs(s_configs); + printf( + "Successfully performed SortedGet, SortedPut, SortedDelete " + "operations on named " + "collections.\n"); +} + +void SortedIteratorExample(KVDKEngine* kvdk_engine) { + const char* nums[10] = {"4", "5", "0", "2", "9", "1", "3", "8", "6", "7"}; + const char* sorted_nums[10] = {"0", "1", "2", "3", "4", + "5", "6", "7", "8", "9"}; + const char* sorted_collection = "sorted_collection"; + KVDKSortedCollectionConfigs* s_configs = KVDKCreateSortedCollectionConfigs(); + KVDKStatus s = KVDKSortedCreate(kvdk_engine, sorted_collection, + strlen(sorted_collection), s_configs); + assert(s == Ok); + for (int i = 0; i < 10; ++i) { + char key[10] = "key", value[10] = "value"; + strcat(key, nums[i]); + strcat(value, nums[i]); + s = KVDKSortedPut(kvdk_engine, sorted_collection, strlen(sorted_collection), + key, strlen(key), value, strlen(value)); + assert(s == Ok); + } + // create sorted iterator + KVDKSortedIterator* kvdk_iter = KVDKSortedIteratorCreate( + kvdk_engine, sorted_collection, strlen(sorted_collection), NULL, NULL); + KVDKSortedIteratorSeekToFirst(kvdk_iter); + // Iterate through range ["key1", "key8"). + const char* beg = "key1"; + const char* end = "key8"; + + int i = 1; + for (KVDKSortedIteratorSeek(kvdk_iter, beg, strlen(beg)); + KVDKSortedIteratorValid(kvdk_iter); KVDKSortedIteratorNext(kvdk_iter)) { + char expected_key[10] = "key", expected_value[10] = "value"; + strcat(expected_key, sorted_nums[i]); + strcat(expected_value, sorted_nums[i]); + size_t key_len, val_len; + char *key_res, *val_res; + KVDKSortedIteratorKey(kvdk_iter, &key_res, &key_len); + KVDKSortedIteratorValue(kvdk_iter, &val_res, &val_len); + if (StrCmp(key_res, key_len, end, strlen(end)) > 0) { + free(key_res); + free(val_res); + break; + } + int cmp = StrCmp(key_res, key_len, expected_key, strlen(expected_key)); + assert(cmp == 0); + cmp = StrCmp(val_res, val_len, expected_value, strlen(expected_value)); + assert(cmp == 0); + free(key_res); + free(val_res); + ++i; + } + assert(i == 9); + + // Iterate through range ["key8", "key1"). + beg = "key8"; + end = "key1"; + + i = 8; + for (KVDKSortedIteratorSeek(kvdk_iter, beg, strlen(beg)); + KVDKSortedIteratorValid(kvdk_iter); KVDKSortedIteratorPrev(kvdk_iter)) { + char expected_key[10] = "key", expected_value[10] = "value"; + strcat(expected_key, sorted_nums[i]); + strcat(expected_value, sorted_nums[i]); + size_t key_len, val_len; + char *key_res, *val_res; + KVDKSortedIteratorKey(kvdk_iter, &key_res, &key_len); + KVDKSortedIteratorValue(kvdk_iter, &val_res, &val_len); + if (StrCmp(key_res, key_len, end, strlen(end)) < 0) { + free(key_res); + free(val_res); + break; + } + int cmp = StrCmp(key_res, key_len, expected_key, strlen(expected_key)); + assert(cmp == 0); + cmp = StrCmp(val_res, val_len, expected_value, strlen(expected_value)); + assert(cmp == 0); + free(key_res); + free(val_res); + --i; + } + assert(i == 0); + printf("Successfully iterated through a sorted named collections.\n"); + KVDKSortedIteratorDestroy(kvdk_engine, kvdk_iter); + KVDKDestroySortedCollectionConfigs(s_configs); +} + +int score_cmp(const char* a, size_t a_len, const char* b, size_t b_len) { + char a_buff[a_len + 1]; + char b_buff[b_len + 1]; + memcpy(a_buff, a, a_len); + memcpy(b_buff, b, b_len); + a_buff[a_len] = '\0'; + b_buff[b_len] = '\0'; + double scorea = atof(a_buff); + double scoreb = atof(b_buff); + if (scorea == scoreb) + return 0; + else if (scorea < scoreb) + return 1; + else + return -1; +} + +void CompFuncForSortedCollectionExample(KVDKEngine* kvdk_engine) { + const char* collection = "collection0"; + struct number_kv { + const char* number_key; + const char* value; + }; + + struct number_kv array[5] = { + {"100", "a"}, {"50", "c"}, {"40", "d"}, {"30", "b"}, {"90", "f"}}; + + struct number_kv expected_array[5] = { + {"100", "a"}, {"90", "f"}, {"50", "c"}, {"40", "d"}, {"30", "b"}}; + + // regitser compare function + const char* comp_name = "double_comp"; + KVDKRegisterCompFunc(kvdk_engine, comp_name, strlen(comp_name), score_cmp); + // create sorted collection + KVDKSortedCollectionConfigs* s_configs = KVDKCreateSortedCollectionConfigs(); + KVDKSetSortedCollectionConfigs(s_configs, comp_name, strlen(comp_name), 1); + KVDKStatus s = + KVDKSortedCreate(kvdk_engine, collection, strlen(collection), s_configs); + assert(s == Ok); + for (int i = 0; i < 5; ++i) { + s = KVDKSortedPut(kvdk_engine, collection, strlen(collection), + array[i].number_key, strlen(array[i].number_key), + array[i].value, strlen(array[i].value)); + assert(s == Ok); + } + KVDKSortedIterator* iter = KVDKSortedIteratorCreate( + kvdk_engine, collection, strlen(collection), NULL, NULL); + assert(iter != NULL); + + int i = 0; + for (KVDKSortedIteratorSeekToFirst(iter); KVDKSortedIteratorValid(iter); + KVDKSortedIteratorNext(iter)) { + size_t key_len, value_len; + char *key, *value; + KVDKSortedIteratorKey(iter, &key, &key_len); + KVDKSortedIteratorValue(iter, &value, &value_len); + if (StrCmp(key, key_len, expected_array[i].number_key, + strlen(expected_array[i].number_key)) != 0) { + printf("sort key error, current key: %s , but expected key: %s\n", key, + expected_array[i].number_key); + } + if (StrCmp(value, value_len, expected_array[i].value, + strlen(expected_array[i].value)) != 0) { + printf("sort value error, current value: %s , but expected value: %s\n", + value, expected_array[i].value); + } + free(key); + free(value); + ++i; + } + KVDKSortedIteratorDestroy(kvdk_engine, iter); + printf("Successfully collections sorted by number.\n"); + KVDKDestroySortedCollectionConfigs(s_configs); +} + +void BatchWriteAnonCollectionExample(KVDKEngine* kvdk_engine) { + const char* key1 = "key1"; + const char* key2 = "key2"; + const char* value1 = "value1"; + const char* value2 = "value2"; + char* read_v1; + char* read_v2; + size_t read_v1_len, read_v2_len; + KVDKWriteBatch* kvdk_wb = KVDKWriteBatchCreate(kvdk_engine); + KVDKWriteBatchStringPut(kvdk_wb, key1, strlen(key1), value1, strlen(value1)); + KVDKWriteBatchStringPut(kvdk_wb, key2, strlen(key2), value2, strlen(value2)); + KVDKWriteBatchStringDelete(kvdk_wb, key1, strlen(key1)); + KVDKWriteBatchStringPut(kvdk_wb, key1, strlen(key1), value2, strlen(value1)); + KVDKStatus s = KVDKBatchWrite(kvdk_engine, kvdk_wb); + assert(s == Ok); + s = KVDKGet(kvdk_engine, key1, strlen(key1), &read_v1_len, &read_v1); + assert(s == Ok); + int cmp = StrCmp(read_v1, read_v1_len, value2, strlen(value2)); + assert(cmp == 0); + s = KVDKGet(kvdk_engine, key2, strlen(key2), &read_v2_len, &read_v2); + assert(s == Ok); + cmp = StrCmp(read_v2, read_v2_len, value2, strlen(value2)); + assert(cmp == 0); + printf("Successfully performed BatchWrite on anonymous global collection.\n"); + KVDKWriteBatchDestory(kvdk_wb); + free(read_v1); + free(read_v2); +} + +void HashesCollectionExample(KVDKEngine* kvdk_engine) { + const char* nums[10] = {"9", "5", "2", "0", "7", "3", "1", "8", "6", "4"}; + const char* hash_collection = "hash_collection"; + KVDKStatus s = + KVDKHashCreate(kvdk_engine, hash_collection, strlen(hash_collection)); + assert(s == Ok); + for (int i = 0; i < 10; ++i) { + char key[10] = "key", value[10] = "value"; + strcat(key, nums[i]); + strcat(value, nums[i]); + s = KVDKHashPut(kvdk_engine, hash_collection, strlen(hash_collection), key, + strlen(key), value, strlen(value)); + assert(s == Ok); + size_t val_len; + char* val; + s = KVDKHashGet(kvdk_engine, hash_collection, strlen(hash_collection), key, + strlen(key), &val, &val_len); + assert(s == Ok); + int cmp = StrCmp(val, val_len, value, strlen(value)); + assert(cmp == 0); + free(val); + } + + s = KVDKHashDelete(kvdk_engine, hash_collection, strlen(hash_collection), + "key8", strlen("key8")); + assert(s == Ok); + // create hash iterator + KVDKHashIterator* kvdk_iter = KVDKHashIteratorCreate( + kvdk_engine, hash_collection, strlen(hash_collection), NULL, NULL); + assert(kvdk_iter != NULL); + int cnt = 0; + for (KVDKHashIteratorSeekToFirst(kvdk_iter); + KVDKHashIteratorIsValid(kvdk_iter); KVDKHashIteratorNext(kvdk_iter)) { + ++cnt; + } + assert(cnt == 9); + + cnt = 0; + for (KVDKHashIteratorSeekToLast(kvdk_iter); + KVDKHashIteratorIsValid(kvdk_iter); KVDKHashIteratorPrev(kvdk_iter)) { + ++cnt; + } + printf("Successfully performed Get Put Delete Iterate on HashList.\n"); + assert(cnt == 9); + KVDKHashIteratorDestroy(kvdk_engine, kvdk_iter); + + s = KVDKHashDestroy(kvdk_engine, hash_collection, strlen(hash_collection)); + assert(s == Ok); +} + +void ListsCollectionExample(KVDKEngine* kvdk_engine) { + const char* nums[10] = {"9", "5", "2", "0", "7", "3", "1", "8", "6", "4"}; + const char* list_collection = "list_collection"; + KVDKStatus s = + KVDKListCreate(kvdk_engine, list_collection, strlen(list_collection)); + assert(s == Ok); + for (int i = 0; i < 10; ++i) { + char key[10] = "key"; + strcat(key, nums[i]); + KVDKStatus s = KVDKListPushFront(kvdk_engine, list_collection, + strlen(list_collection), key, strlen(key)); + assert(s == Ok); + size_t key_len_res; + char* key_res; + s = KVDKListPopFront(kvdk_engine, list_collection, strlen(list_collection), + &key_res, &key_len_res); + assert(s == Ok); + int cmp = StrCmp(key_res, key_len_res, key, strlen(key)); + assert(cmp == 0); + free(key_res); + } + + for (int i = 0; i < 10; ++i) { + char key[10] = "value"; + strcat(key, nums[i]); + KVDKStatus s = KVDKListPushBack(kvdk_engine, list_collection, + strlen(list_collection), key, strlen(key)); + assert(s == Ok); + size_t key_len_res; + char* key_res; + s = KVDKListPopBack(kvdk_engine, list_collection, strlen(list_collection), + &key_res, &key_len_res); + assert(s == Ok); + int cmp = StrCmp(key_res, key_len_res, key, strlen(key)); + assert(cmp == 0); + free(key_res); + } + + s = KVDKListDestroy(kvdk_engine, list_collection, strlen(list_collection)); + assert(s == Ok); + + printf("Successfully performed RPush RPop LPush LPop on Lists.\n"); +} + +void ExpireExample(KVDKEngine* kvdk_engine) { + int64_t ttl_time; + char* got_val; + size_t val_len; + KVDKStatus s; + // For string + { + const char* key = "stringkey"; + const char* val = "stringval"; + // case: set expire time + KVDKWriteOptions* write_option = KVDKCreateWriteOptions(); + KVDKWriteOptionsSetTTLTime(write_option, 100); + s = KVDKPut(kvdk_engine, key, strlen(key), val, strlen(val), write_option); + assert(s == Ok); + s = KVDKGet(kvdk_engine, key, strlen(key), &val_len, &got_val); + assert(s == Ok); + int cmp = StrCmp(got_val, val_len, val, strlen(val)); + assert(cmp == 0); + free(got_val); + s = KVDKGetTTL(kvdk_engine, key, strlen(key), &ttl_time); + assert(s == Ok); + // case: reset expire time + s = KVDKExpire(kvdk_engine, key, strlen(key), INT32_MAX); + assert(s == Ok); + // case: change to persist key + s = KVDKExpire(kvdk_engine, key, strlen(key), INT64_MAX); + assert(s == Ok); + s = KVDKGetTTL(kvdk_engine, key, strlen(key), &ttl_time); + assert(s == Ok); + assert(ttl_time == INT64_MAX); + // case: keep expire time unchanged while updating a existing key + KVDKWriteOptionsSetUpdateTTL(write_option, 0); + KVDKWriteOptionsSetTTLTime(write_option, 0); + s = KVDKPut(kvdk_engine, key, strlen(key), val, strlen(val), write_option); + assert(s == Ok); + s = KVDKGetTTL(kvdk_engine, key, strlen(key), &ttl_time); + assert(s == Ok); + assert(ttl_time == INT64_MAX); + // case: key is expired. + s = KVDKExpire(kvdk_engine, key, strlen(key), 1); + assert(s == Ok); + sleep(1); + s = KVDKGet(kvdk_engine, key, strlen(key), &val_len, &got_val); + assert(s == NotFound); + // case: ttl time is negative or 0 + s = KVDKPut(kvdk_engine, key, strlen(key), val, strlen(val), write_option); + assert(s == Ok); + s = KVDKGet(kvdk_engine, key, strlen(key), &val_len, &got_val); + assert(s == NotFound); + // No need to free(got_val) + printf("Successfully expire string\n"); + + KVDKDestroyWriteOptions(write_option); + } + + { + const char* sorted_collection = "sorted_collection"; + const char* key = "sortedkey"; + const char* val = "sortedval"; + + // case: default persist key. + KVDKSortedCollectionConfigs* s_configs = + KVDKCreateSortedCollectionConfigs(); + s = KVDKSortedCreate(kvdk_engine, sorted_collection, + strlen(sorted_collection), s_configs); + assert(s == Ok || s == Existed); + s = KVDKGetTTL(kvdk_engine, sorted_collection, strlen(sorted_collection), + &ttl_time); + assert(s == Ok); + assert(ttl_time == INT64_MAX); + s = KVDKSortedPut(kvdk_engine, sorted_collection, strlen(sorted_collection), + key, strlen(key), val, strlen(val)); + assert(s == Ok); + // case: set expire_time + s = KVDKExpire(kvdk_engine, sorted_collection, strlen(sorted_collection), + INT32_MAX); + assert(s == Ok); + // case: change to persist key + s = KVDKExpire(kvdk_engine, sorted_collection, strlen(sorted_collection), + INT64_MAX); + s = KVDKGetTTL(kvdk_engine, sorted_collection, strlen(sorted_collection), + &ttl_time); + assert(s == Ok); + assert(ttl_time == INT64_MAX); + // case: key is expired. + s = KVDKExpire(kvdk_engine, sorted_collection, strlen(sorted_collection), + 1); + assert(s == Ok); + sleep(1); + s = KVDKSortedGet(kvdk_engine, sorted_collection, strlen(sorted_collection), + key, strlen(key), &val_len, &got_val); + assert(s == NotFound); + free(got_val); + printf("Successfully expire sorted\n"); + + KVDKDestroySortedCollectionConfigs(s_configs); + } + + { + const char* hash_collection = "hash"; + const char* key = "hashkey"; + const char* val = "hashval"; + + s = KVDKHashCreate(kvdk_engine, hash_collection, strlen(hash_collection)); + assert(s == Ok); + s = KVDKHashPut(kvdk_engine, hash_collection, strlen(hash_collection), key, + strlen(key), val, strlen(val)); + assert(s == Ok); + s = KVDKGetTTL(kvdk_engine, hash_collection, strlen(hash_collection), + &ttl_time); + assert(s == Ok); + assert(ttl_time == INT64_MAX); + + // case: set expire_time + s = KVDKExpire(kvdk_engine, hash_collection, strlen(hash_collection), 1); + assert(s == Ok); + // case: change to persist key + s = KVDKExpire(kvdk_engine, hash_collection, strlen(hash_collection), + INT64_MAX); + s = KVDKGetTTL(kvdk_engine, hash_collection, strlen(hash_collection), + &ttl_time); + assert(s == Ok); + assert(ttl_time == INT64_MAX); + // case: key is expired. + s = KVDKExpire(kvdk_engine, hash_collection, strlen(hash_collection), 1); + assert(s == Ok); + sleep(1); + s = KVDKHashGet(kvdk_engine, hash_collection, strlen(hash_collection), key, + strlen(key), &got_val, &val_len); + assert(s == NotFound); + + s = KVDKHashDestroy(kvdk_engine, hash_collection, strlen(hash_collection)); + assert(s == NotFound); + + printf("Successfully expire hash\n"); + } + return; +} + +typedef struct { + int incr_by; + int result; +} IncNArgs; + +int IncN(const char* old_val, size_t old_val_len, char** new_val, + size_t* new_val_len, void* args_pointer) { + assert(args_pointer); + + int old_num; + if (old_val == NULL) { + old_num = 0; + } else { + if (old_val_len != sizeof(int)) { + return KVDK_MODIFY_ABORT; + } + assert(old_val_len == sizeof(int)); + memcpy(&old_num, old_val, sizeof(int)); + } + + IncNArgs* args = (IncNArgs*)args_pointer; + *new_val = (char*)malloc(sizeof(int)); + if (*new_val == NULL) { + return KVDK_MODIFY_ABORT; + } + + *new_val_len = sizeof(int); + + args->result = old_num + args->incr_by; + memcpy(*new_val, &args->result, sizeof(int)); + return KVDK_MODIFY_WRITE; +} + +void ModifyExample(KVDKEngine* kvdk_engine) { + KVDKWriteOptions* write_option = KVDKCreateWriteOptions(); + char* incr_key = "incr"; + char* wrong_value_key = "wrong"; + IncNArgs args; + args.incr_by = 5; + + KVDKStatus s = KVDKPut(kvdk_engine, wrong_value_key, strlen(wrong_value_key), + "a", 1, write_option); + assert(s == Ok); + s = KVDKModify(kvdk_engine, wrong_value_key, strlen(wrong_value_key), IncN, + &args, free, write_option); + assert(s == Abort); + + int recycle = 100; + for (int i = 1; i <= recycle; i++) { + KVDKStatus s = KVDKModify(kvdk_engine, incr_key, strlen(incr_key), IncN, + &args, free, write_option); + assert(s == Ok); + assert(args.result == args.incr_by * (int)i); + + char* val; + size_t val_len; + s = KVDKGet(kvdk_engine, incr_key, strlen(incr_key), &val_len, &val); + assert(s == Ok); + assert(val_len == sizeof(int)); + int current_num; + memcpy(¤t_num, val, sizeof(int)); + assert(current_num == args.incr_by * i); + free(val); + } + KVDKDestroyWriteOptions(write_option); + printf("Successfully increase num by %d\n", args.incr_by); +} + +typedef struct { + char const* data; + size_t len; + size_t ret; +} HPutArgs; +// new_data is not touched as long as caller does not pass a free_func to +// KVDKHashModify This avoids a malloc() in HPutNXFunc and a free() in +// KVDKHashModify() +int HPutNXFunc(char const* old_data, size_t old_len, char** new_data, + size_t* new_len, void* args) { + HPutArgs* my_args = (HPutArgs*)args; + if (old_data == NULL) { + assert(old_len == 0); + *new_data = (char*)my_args->data; + *new_len = my_args->len; + my_args->ret = 1; + return KVDK_MODIFY_WRITE; + } else { + my_args->ret = 0; + return KVDK_MODIFY_NOOP; + } +} +int HPutFunc(char const* old_data, size_t old_len, char** new_data, + size_t* new_len, void* args) { + HPutArgs* my_args = (HPutArgs*)args; + *new_data = (char*)my_args->data; + *new_len = my_args->len; + if (old_data == NULL) { + assert(old_len == 0); + my_args->ret = 1; + } else { + my_args->ret = 0; + } + return KVDK_MODIFY_WRITE; +} + +void HashModifyExample(KVDKEngine* engine) { + char const* key = "my_hash"; + char const* field = "my_field"; + char const* value1 = "hello"; + char const* value2 = "my_field"; + char* resp_data; + size_t resp_len; + + HPutArgs args; + args.data = value1; + args.len = strlen(value1); + KVDKStatus s = KVDKHashCreate(engine, key, strlen(key)); + s = KVDKHashModify(engine, key, strlen(key), field, strlen(field), HPutNXFunc, + &args, NULL); + assert(s == Ok); + assert(args.ret == 1); + s = KVDKHashGet(engine, key, strlen(key), field, strlen(field), &resp_data, + &resp_len); + assert(s == Ok); + assert(resp_len == strlen(value1)); + assert(StrCmp(resp_data, resp_len, value1, strlen(value1)) == 0); + free(resp_data); + + args.data = value2; + args.len = strlen(value2); + s = KVDKHashModify(engine, key, strlen(key), field, strlen(field), HPutNXFunc, + &args, NULL); + assert(s == Ok); + assert(args.ret == 0); // Fail to set since the field already exists + s = KVDKHashGet(engine, key, strlen(key), field, strlen(field), &resp_data, + &resp_len); + assert(s == Ok); + assert(resp_len == strlen(value1)); + assert(StrCmp(resp_data, resp_len, value1, strlen(value1)) == + 0); // field is untouched + free(resp_data); + + s = KVDKHashModify(engine, key, strlen(key), field, strlen(field), HPutFunc, + &args, NULL); + assert(s == Ok); + assert(args.ret == 0); // Update, no field added + s = KVDKHashGet(engine, key, strlen(key), field, strlen(field), &resp_data, + &resp_len); + assert(s == Ok); + assert(resp_len == strlen(value2)); + assert(StrCmp(resp_data, resp_len, value2, strlen(value2)) == + 0); // field is updated + free(resp_data); + s = KVDKHashDestroy(engine, key, strlen(key)); + assert(s == Ok); + + printf("Successfully excecuted HSET and HSETNX.\n"); +} + +int main() { + // Initialize a KVDK instance. + KVDKConfigs* kvdk_configs = KVDKCreateConfigs(); + KVDKSetConfigs(kvdk_configs, 48, 1ull << 10, 1 << 4); + + const char* engine_path = "/mnt/pmem0/tutorial_kvdk_example"; + // open engine + KVDKEngine* kvdk_engine; + KVDKStatus s = KVDKOpen(engine_path, kvdk_configs, stdout, &kvdk_engine); + assert(s == Ok); + + // Modify Example + ModifyExample(kvdk_engine); + + // Anonymous Global Collection Example + AnonymousCollectionExample(kvdk_engine); + + // Named Sorted Collection Example + SortedCollectionExample(kvdk_engine); + + // Sorted Named Collection Example + SortedIteratorExample(kvdk_engine); + + CompFuncForSortedCollectionExample(kvdk_engine); + + // BatchWrite on Anonymous Global Collection Example + BatchWriteAnonCollectionExample(kvdk_engine); + + // Hashes Collection Example + HashesCollectionExample(kvdk_engine); + + // Listes Collection Example + ListsCollectionExample(kvdk_engine); + + // Expire Example + ExpireExample(kvdk_engine); + + HashModifyExample(kvdk_engine); + + KVDKDestroyConfigs(kvdk_configs); + KVDKCloseEngine(kvdk_engine); + return 0; +} diff --git a/volatile/examples/tutorial/cpp_api_tutorial.cpp b/volatile/examples/tutorial/cpp_api_tutorial.cpp new file mode 100644 index 00000000..4a8e2d96 --- /dev/null +++ b/volatile/examples/tutorial/cpp_api_tutorial.cpp @@ -0,0 +1,457 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include "kvdk/volatile/engine.hpp" + +#define DEBUG // For assert + +using StringView = pmem::obj::string_view; +using kvdk::Snapshot; + +// The KVDK instance is mounted as a directory +// /mnt/pmem0/tutorial_kvdk_example. +// Modify this path if necessary. +const char* pmem_path = "/mnt/pmem0/tutorial_kvdk_example"; + +kvdk::Status status; +kvdk::Engine* engine = nullptr; + +static void test_anon_coll() { + std::string key1{"key1"}; + std::string key2{"key2"}; + std::string value1{"value1"}; + std::string value2{"value2"}; + std::string v; + kvdk::WriteOptions write_options; + + // Insert key1-value1 + status = engine->Put(key1, value1, write_options); + assert(status == kvdk::Status::Ok); + + // Get value1 by key1 + status = engine->Get(key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value1); + + // Update key1-value1 to key1-value2 + status = engine->Put(key1, value2, write_options); + assert(status == kvdk::Status::Ok); + + // Get value2 by key1 + status = engine->Get(key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value2); + + // Insert key2-value2 + status = engine->Put(key2, value2, write_options); + assert(status == kvdk::Status::Ok); + + // Delete key1-value2 + status = engine->Delete(key1); + assert(status == kvdk::Status::Ok); + + // Delete key2-value2 + status = engine->Delete(key2); + assert(status == kvdk::Status::Ok); + + printf( + "Successfully performed Get, Put, Delete operations on anonymous " + "global collection.\n"); + return; +} + +static void test_named_coll() { + std::string collection1{"my_collection_1"}; + std::string collection2{"my_collection_2"}; + std::string key1{"key1"}; + std::string key2{"key2"}; + std::string value1{"value1"}; + std::string value2{"value2"}; + std::string v; + status = engine->SortedCreate(collection1); + assert(status == kvdk::Status::Ok); + + status = engine->SortedCreate(collection2); + assert(status == kvdk::Status::Ok); + // Insert key1-value1 into "my_collection_1". + // Implicitly create a collection named "my_collection_1" in which + // key1-value1 is stored. + status = engine->SortedPut(collection1, key1, value1); + assert(status == kvdk::Status::Ok); + + // Get value1 by key1 in collection "my_collection_1" + status = engine->SortedGet(collection1, key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value1); + + // Insert key1-value2 into "my_collection_2". + // Implicitly create a collection named "my_collection_2" in which + // key1-value2 is stored. + status = engine->SortedPut(collection2, key1, value2); + assert(status == kvdk::Status::Ok); + + // Get value2 by key1 in collection "my_collection_2" + status = engine->SortedGet(collection2, key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value2); + + // Get value1 by key1 in collection "my_collection_1" + // key1-value2 is stored in "my_collection_2" + // Thus key1-value1 stored in "my_collection_1" is unaffected by operation + // engine->SortedPut(collection2, key1, value2). + status = engine->SortedGet(collection1, key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value1); + + // Insert key2-value2 into collection "my_collection_2" + // Collection "my_collection_2" already exists and no implicit collection + // creation occurs. + status = engine->SortedPut(collection2, key2, value2); + assert(status == kvdk::Status::Ok); + + // Delete key1-value1 in collection "my_collection_1" + // Although "my_collection_1" has no elements now, the collection itself is + // not deleted though. + status = engine->SortedDelete(collection1, key1); + assert(status == kvdk::Status::Ok); + + printf( + "Successfully performed SortedGet, SortedPut, SortedDelete operations on " + "named " + "collections.\n"); + return; +} + +static void test_iterator() { + std::string sorted_collection{"my_sorted_collection"}; + // Create Sorted Collection + status = engine->SortedCreate(sorted_collection); + assert(status == kvdk::Status::Ok); + // Create toy keys and values. + std::vector> kv_pairs; + for (int i = 0; i < 10; ++i) { + kv_pairs.emplace_back( + std::make_pair("key" + std::to_string(i), "value" + std::to_string(i))); + } + std::shuffle(kv_pairs.begin(), kv_pairs.end(), std::mt19937{42}); + // Print out kv_pairs to check if they are really shuffled. + printf("The shuffled kv-pairs are:\n"); + for (const auto& kv : kv_pairs) + printf("%s\t%s\n", kv.first.c_str(), kv.second.c_str()); + + // Populate collection "my_sorted_collection" with keys and values. + // kv_pairs are not necessarily sorted, but kv-pairs in collection + // "my_sorted_collection" are sorted. + for (int i = 0; i < 10; ++i) { + // Collection "my_sorted_collection" is implicitly created in first + // iteration + status = engine->SortedPut(sorted_collection, kv_pairs[i].first, + kv_pairs[i].second); + assert(status == kvdk::Status::Ok); + } + // Sort kv_pairs for checking the order of "my_sorted_collection". + std::sort(kv_pairs.begin(), kv_pairs.end()); + + // Iterate through collection "my_sorted_collection" + auto iter = engine->SortedIteratorCreate(sorted_collection); + if (!iter) { + fprintf(stderr, "Seek error\n"); + return; + } + iter->SeekToFirst(); + { + int i = 0; + while (iter->Valid()) { + assert(iter->Key() == kv_pairs[i].first); + assert(iter->Value() == kv_pairs[i].second); + iter->Next(); + ++i; + } + } + + // Iterate through range ["key1", "key8"). + std::string beg{"key1"}; + std::string end{"key8"}; + { + int i = 1; + iter->Seek(beg); + for (iter->Seek(beg); iter->Valid() && iter->Key() < end; iter->Next()) { + assert(iter->Key() == kv_pairs[i].first); + assert(iter->Value() == kv_pairs[i].second); + ++i; + } + } + + // Reversely iterate through range ["key8", "key1"). + beg = "key8"; + end = "key1"; + { + int i = 8; + for (iter->Seek(beg); iter->Valid() && iter->Key() > end; iter->Prev()) { + assert(iter->Key() == kv_pairs[i].first); + assert(iter->Value() == kv_pairs[i].second); + --i; + } + } + + printf("Successfully iterated through a sorted named collections.\n"); + engine->SortedIteratorRelease(iter); + return; +} + +static void test_batch_write() { + std::string key1{"key1"}; + std::string key2{"key2"}; + std::string value1{"value1"}; + std::string value2{"value2"}; + std::string v; + + auto batch = engine->WriteBatchCreate(); + batch->StringPut(key1, value1); + batch->StringPut(key1, value2); + batch->StringPut(key2, value2); + batch->StringDelete(key2); + + // If the batch is successfully written, there should be only key1-value2 in + // anonymous global collection. + status = engine->BatchWrite(batch); + assert(status == kvdk::Status::Ok); + + // Get value2 by key1 + status = engine->Get(key1, &v); + assert(status == kvdk::Status::Ok); + assert(v == value2); + + // Get value2 by key1 + status = engine->Get(key2, &v); + assert(status == kvdk::Status::NotFound); + // v is unchanged, but it is invalid. Always Check kvdk::Status before + // perform further operations! + assert(v == value2); + + printf("Successfully performed BatchWrite on anonymous global collection.\n"); + return; +} + +static void test_customer_sorted_func() { + std::string collection = "collection0"; + struct number_kv { + std::string number_key; + std::string value; + }; + + std::vector array = { + {"100", "a"}, {"50", "c"}, {"40", "d"}, {"30", "b"}, {"90", "f"}}; + + std::vector expected_array = { + {"100", "a"}, {"90", "f"}, {"50", "c"}, {"40", "d"}, {"30", "b"}}; + + // regitser compare function + std::string comp_name = "double_comp"; + auto score_cmp = [](const StringView& a, const StringView& b) -> int { + std::string str_a(a.data(), a.size()); + std::string str_b(b.data(), b.size()); + double scorea = std::stod(str_a); + double scoreb = std::stod(str_b); + if (scorea == scoreb) + return 0; + else if (scorea < scoreb) + return 1; + else + return -1; + }; + engine->registerComparator(comp_name, score_cmp); + // create sorted collection + kvdk::SortedCollectionConfigs s_configs; + s_configs.comparator_name = comp_name; + kvdk::Status s = engine->SortedCreate(collection, s_configs); + assert(s == Ok); + for (int i = 0; i < 5; ++i) { + s = engine->SortedPut(collection, array[i].number_key, array[i].value); + assert(s == Ok); + } + auto iter = engine->SortedIteratorCreate(collection); + + assert(iter != nullptr); + + int i = 0; + for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { + std::string key = iter->Key(); + std::string value = iter->Value(); + if (key != expected_array[i].number_key) { + printf("sort key error, current key: %s , but expected key: %s\n", + key.c_str(), expected_array[i].number_key.c_str()); + } + if (value != expected_array[i].value) { + printf("sort value error, current value: %s , but expected value: %s\n", + value.c_str(), expected_array[i].value.c_str()); + } + ++i; + } + printf("Successfully collections sorted by number.\n"); + engine->SortedIteratorRelease(iter); +} + +static void test_expire() { + int64_t ttl_time; + std::string got_val; + kvdk::Status s; + // For string + { + std::string key = "stringkey"; + std::string val = "stringval"; + // case: set expire time + s = engine->Put(key, val, kvdk::WriteOptions{100}); + assert(s == kvdk::Status::Ok); + s = engine->Get(key, &got_val); + assert(s == kvdk::Status::Ok); + assert(got_val == val); + s = engine->GetTTL(key, &ttl_time); + assert(s == kvdk::Status::Ok); + // case: reset expire time + s = engine->Expire(key, INT32_MAX); + assert(s == kvdk::Status::Ok); + // case: change to persist key + s = engine->Expire(key, kvdk::kPersistTime); + assert(s == kvdk::Status::Ok); + s = engine->GetTTL(key, &ttl_time); + assert(s == kvdk::Status::Ok); + assert(ttl_time == kvdk::kPersistTime); + // case: key is expired. + s = engine->Expire(key, 1); + assert(s == kvdk::Status::Ok); + sleep(1); + s = engine->Get(key, &got_val); + assert(s == kvdk::Status::NotFound); + // case: ttl time is negative. + s = engine->Put(key, "Updatedval"); + assert(s == kvdk::Status::Ok); + s = engine->Expire(key, -1); + assert(s == kvdk::Status::Ok); + s = engine->GetTTL(key, &ttl_time); + assert(s == kvdk::Status::NotFound); + printf("Successfully expire string\n"); + } + + { + std::string sorted_collection = "sorted_collection"; + std::string key = "sortedkey"; + std::string val = "sortedval"; + + s = engine->SortedCreate(sorted_collection); + // case: default persist key. + s = engine->GetTTL(sorted_collection, &ttl_time); + assert(s == kvdk::Status::Ok); + assert(ttl_time == kvdk::kPersistTime); + s = engine->SortedPut(sorted_collection, key, val); + assert(s == kvdk::Status::Ok); + // case: set expire_time + s = engine->Expire(sorted_collection, INT32_MAX); + assert(s == kvdk::Status::Ok); + // case: change to persist key + s = engine->Expire(sorted_collection, kvdk::kPersistTime); + s = engine->GetTTL(sorted_collection, &ttl_time); + assert(s == kvdk::Status::Ok); + assert(ttl_time == kvdk::kPersistTime); + // case: key is expired. + s = engine->Expire(sorted_collection, 1); + assert(s == kvdk::Status::Ok); + sleep(1); + s = engine->SortedGet(sorted_collection, key, &got_val); + assert(s == kvdk::Status::NotFound); + printf("Successfully expire sorted\n"); + } + + { + std::string hash_collection = "hash_collection"; + std::string key = "hashkey"; + std::string val = "hashval"; + + // case: default persist key + s = engine->HashCreate(hash_collection); + assert(s == kvdk::Status::Ok); + s = engine->HashPut(hash_collection, key, val); + assert(s == kvdk::Status::Ok); + s = engine->GetTTL(hash_collection, &ttl_time); + assert(s == kvdk::Status::Ok); + assert(ttl_time == kvdk::kPersistTime); + + // case: set expire_time + s = engine->Expire(hash_collection, 1); + assert(s == kvdk::Status::Ok); + // case: change to persist key + s = engine->Expire(hash_collection, kvdk::kPersistTime); + s = engine->GetTTL(hash_collection, &ttl_time); + assert(s == kvdk::Status::Ok); + assert(ttl_time == kvdk::kPersistTime); + // case: key is expired. + s = engine->Expire(hash_collection, 1); + assert(s == kvdk::Status::Ok); + sleep(1); + s = engine->HashGet(hash_collection, key, &got_val); + assert(s == kvdk::Status::NotFound); + + s = engine->HashDestroy(hash_collection); + assert(s == kvdk::Status::NotFound); + printf("Successfully expire hash\n"); + } + + { + // TODO: add expire list, but now list api has changed. + } + return; +} + +int main() { + // Initialize a KVDK instance. + kvdk::Configs engine_configs; + { + // Configure for a tiny KVDK instance. + // Please refer to "Configuration" section in user documentation for + // details. + engine_configs.hash_bucket_num = (1ull << 10); + } + std::string engine_path{pmem_path}; + + // Purge old KVDK instance + [[gnu::unused]] int sink = + system(std::string{"rm -rf " + engine_path + "\n"}.c_str()); + + status = kvdk::Engine::Open(engine_path, &engine, engine_configs, stdout); + assert(status == kvdk::Status::Ok); + printf("Successfully opened a KVDK instance.\n"); + + // Reads and Writes on Anonymous Global Collection + test_anon_coll(); + + // Reads and Writes on Named Collection + test_named_coll(); + + // Iterating a Sorted Named Collection + test_iterator(); + + // Sorted Collection with Customer Sorted Function + test_customer_sorted_func(); + + // BatchWrite on Anonymous Global Collection + test_batch_write(); + + // Expire + test_expire(); + + // Close KVDK instance. + delete engine; + + // Remove persisted contents on PMem + return system(std::string{"rm -rf " + engine_path + "\n"}.c_str()); +} diff --git a/volatile/examples/tutorial/zset.c b/volatile/examples/tutorial/zset.c new file mode 100644 index 00000000..4e6af779 --- /dev/null +++ b/volatile/examples/tutorial/zset.c @@ -0,0 +1,275 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +// This is a example of single-thread redis zset implementation with kvdk c api + +#include "assert.h" +#include "kvdk/volatile/engine.h" +#include "malloc.h" +#include "string.h" + +#define NUM_MEMBERS 7 + +int StrCompare(const char* a, size_t a_len, const char* b, size_t b_len) { + int n = (a_len < b_len) ? a_len : b_len; + int cmp = memcmp(a, b, n); + if (cmp == 0) { + if (a_len < b_len) + cmp = -1; + else if (a_len > b_len) + cmp = 1; + } + return cmp; +} + +const char* comp_name = "score_comp"; +int ScoreCmp(const char* score_key_a, size_t a_len, const char* score_key_b, + size_t b_len) { + assert(a_len >= sizeof(int64_t)); + assert(b_len >= sizeof(int64_t)); + int cmp = (*(int64_t*)score_key_a) - (*(int64_t*)score_key_b); + return cmp == 0 ? StrCompare( + score_key_a + sizeof(int64_t), a_len - sizeof(int64_t), + score_key_b + sizeof(int64_t), b_len - sizeof(int64_t)) + : cmp; +} + +// Store score key in sorted collection to index score->member +void EncodeScoreKey(int64_t score, const char* member, size_t member_len, + char** score_key, size_t* score_key_len) { + *score_key_len = sizeof(int64_t) + member_len; + *score_key = (char*)malloc(*score_key_len); + memcpy(*score_key, &score, sizeof(int64_t)); + memcpy(*score_key + sizeof(int64_t), member, member_len); +} + +// Store member with string type to index member->score +void EncodeStringKey(const char* collection, size_t collection_len, + const char* member, size_t member_len, char** string_key, + size_t* string_key_len) { + *string_key_len = collection_len + member_len; + *string_key = (char*)malloc(*string_key_len); + memcpy(*string_key, collection, collection_len); + memcpy(*string_key + collection_len, member, member_len); +} + +// notice: we set member as a view of score key (which means no ownership) +void DecodeScoreKey(char* score_key, size_t score_key_len, char** member, + size_t* member_len, int64_t* score) { + assert(score_key_len > sizeof(int64_t)); + memcpy(score, score_key, sizeof(int64_t)); + *member = score_key + sizeof(int64_t); + *member_len = score_key_len - sizeof(int64_t); +} + +void PrintMemberScore(size_t index, char* member, size_t member_len, + int64_t score) { + char member_c_str[member_len + 1]; + memcpy(member_c_str, member, member_len); + member_c_str[member_len] = '\0'; + printf("(%lu)\"%s\"\n", 2 * index + 1, member_c_str); + printf("(%lu)\"%ld\"\n", 2 * index + 2, score); +} + +KVDKStatus KVDKZAdd(KVDKEngine* engine, const char* collection, + size_t collection_len, int64_t score, const char* member, + size_t member_len, KVDKWriteOptions* write_option) { + printf("ZADD %s %ld %s\n", collection, score, member); + + char* score_key; + char* string_key; + size_t score_key_len; + size_t string_key_len; + EncodeScoreKey(score, member, member_len, &score_key, &score_key_len); + EncodeStringKey(collection, collection_len, member, member_len, &string_key, + &string_key_len); + + KVDKStatus s = KVDKSortedPut(engine, collection, collection_len, score_key, + score_key_len, "", 0); + if (s == NotFound) { + KVDKSortedCollectionConfigs* s_config = KVDKCreateSortedCollectionConfigs(); + KVDKSetSortedCollectionConfigs( + s_config, comp_name, strlen(comp_name), + 0 /*we do not need hash index for score part*/); + s = KVDKSortedCreate(engine, collection, collection_len, s_config); + if (s == Ok) { + s = KVDKSortedPut(engine, collection, collection_len, score_key, + score_key_len, "", 0); + } + KVDKDestroySortedCollectionConfigs(s_config); + } + + if (s == Ok) { + s = KVDKPut(engine, string_key, string_key_len, (char*)&score, + sizeof(int64_t), write_option); + } + + free(score_key); + free(string_key); + + return s; +} + +KVDKStatus KVDKZPopMin(KVDKEngine* engine, const char* collection, + size_t collection_len, size_t n) { + printf("ZPOPMIN %s %lu\n", collection, n); + + KVDKStatus s; + KVDKSortedIterator* iter = + KVDKSortedIteratorCreate(engine, collection, collection_len, NULL, NULL); + if (iter == NULL) { + return Ok; + } + size_t cnt = 0; + + for (KVDKSortedIteratorSeekToFirst(iter); + KVDKSortedIteratorValid(iter) && n > 0; KVDKSortedIteratorNext(iter)) { + char* score_key; + char* string_key; + char* member; + size_t score_key_len; + size_t string_key_len; + size_t member_len; + int64_t score; + KVDKSortedIteratorKey(iter, &score_key, &score_key_len); + DecodeScoreKey(score_key, score_key_len, &member, &member_len, &score); + EncodeStringKey(collection, collection_len, member, member_len, &string_key, + &string_key_len); + s = KVDKSortedDelete(engine, collection, collection_len, score_key, + score_key_len); + if (s == Ok) { + s = KVDKDelete(engine, string_key, string_key_len); + } + + if (s == Ok) { + // do anything with poped key, like print + PrintMemberScore(cnt++, member, member_len, score); + (void)member; + } + + free(score_key); + free(string_key); + if (s != Ok || cnt == n) { + break; + } + } + KVDKSortedIteratorDestroy(engine, iter); + return s; +} + +KVDKStatus KVDKZPopMax(KVDKEngine* engine, const char* collection, + size_t collection_len, size_t n) { + printf("ZPOPMAX %s %lu\n", collection, n); + + KVDKStatus s; + KVDKSortedIterator* iter = + KVDKSortedIteratorCreate(engine, collection, collection_len, NULL, NULL); + if (iter == NULL) { + return Ok; + } + size_t cnt = 0; + + for (KVDKSortedIteratorSeekToLast(iter); + KVDKSortedIteratorValid(iter) && n > 0; KVDKSortedIteratorPrev(iter)) { + char* score_key; + char* string_key; + char* member; + size_t score_key_len; + size_t string_key_len; + size_t member_len; + int64_t score; + KVDKSortedIteratorKey(iter, &score_key, &score_key_len); + DecodeScoreKey(score_key, score_key_len, &member, &member_len, &score); + EncodeStringKey(collection, collection_len, member, member_len, &string_key, + &string_key_len); + s = KVDKSortedDelete(engine, collection, collection_len, score_key, + score_key_len); + if (s == Ok) { + s = KVDKDelete(engine, string_key, string_key_len); + } + + if (s == Ok) { + // do anything with poped key, like print + PrintMemberScore(cnt++, member, member_len, score); + (void)member; + } + + free(score_key); + free(string_key); + if (s != Ok || cnt == n) { + break; + } + } + KVDKSortedIteratorDestroy(engine, iter); + return s; +} + +KVDKStatus KVDKZRange(KVDKEngine* engine, const char* collection, + size_t collection_len, int64_t min_score, + int64_t max_score) { + printf("ZRANGE %s %ld %ld\n", collection, min_score, max_score); + + KVDKSortedIterator* iter = + KVDKSortedIteratorCreate(engine, collection, collection_len, NULL, NULL); + if (iter == NULL) { + return Ok; + } + size_t cnt = 0; + + for (KVDKSortedIteratorSeek(iter, (char*)&min_score, sizeof(int64_t)); + KVDKSortedIteratorValid(iter); KVDKSortedIteratorNext(iter)) { + char* score_key; + size_t score_key_len; + KVDKSortedIteratorKey(iter, &score_key, &score_key_len); + char* member; + size_t member_len; + int64_t score; + DecodeScoreKey(score_key, score_key_len, &member, &member_len, &score); + if (score <= max_score) { + // do any thing with in-range key, like print; + PrintMemberScore(cnt++, member, member_len, score); + free(score_key); + } else { + free(score_key); + break; + } + } + KVDKSortedIteratorDestroy(engine, iter); + return Ok; +} + +void ZSetTest(KVDKEngine* engine) { + const char* zset_name = "zset"; + const size_t zset_name_len = strlen(zset_name); + char* members[NUM_MEMBERS] = {"one", "two", "three", "four", + "five", "another_five", "six"}; + int64_t scores[NUM_MEMBERS] = {1, 2, 3, 4, 5, 5, 6}; + KVDKWriteOptions* write_option = KVDKCreateWriteOptions(); + for (size_t i = 0; i < NUM_MEMBERS; i++) { + KVDKStatus s = KVDKZAdd(engine, zset_name, zset_name_len, scores[i], + members[i], strlen(members[i]), write_option); + assert(s == Ok); + } + + assert(KVDKZRange(engine, zset_name, zset_name_len, 1, 6) == Ok); + assert(KVDKZPopMax(engine, zset_name, zset_name_len, 2) == Ok); + assert(KVDKZPopMin(engine, zset_name, zset_name_len, 2) == Ok); + assert(KVDKZRange(engine, zset_name, zset_name_len, 1, 6) == Ok); + KVDKDestroyWriteOptions(write_option); +} + +int main() { + KVDKConfigs* kvdk_configs = KVDKCreateConfigs(); + KVDKSetConfigs(kvdk_configs, 48, 1ull << 10, 1 << 4); + const char* engine_path = "/mnt/pmem0/kvdk_zset_example"; + KVDKEngine* kvdk_engine; + KVDKStatus s = KVDKOpen(engine_path, kvdk_configs, stdout, &kvdk_engine); + assert(s == Ok); + KVDKRegisterCompFunc(kvdk_engine, comp_name, strlen(comp_name), ScoreCmp); + ZSetTest(kvdk_engine); + + KVDKDestroyConfigs(kvdk_configs); + KVDKCloseEngine(kvdk_engine); + return 0; +} \ No newline at end of file diff --git a/volatile/extern/gtest b/volatile/extern/gtest new file mode 160000 index 00000000..e2239ee6 --- /dev/null +++ b/volatile/extern/gtest @@ -0,0 +1 @@ +Subproject commit e2239ee6043f73722e7aa812a459f54a28552929 diff --git a/volatile/extern/jemalloc b/volatile/extern/jemalloc new file mode 160000 index 00000000..36366f3c --- /dev/null +++ b/volatile/extern/jemalloc @@ -0,0 +1 @@ +Subproject commit 36366f3c4c741723369853c923e56999716398fc diff --git a/volatile/extern/jemalloc-cmake/CMakeLists.txt b/volatile/extern/jemalloc-cmake/CMakeLists.txt new file mode 100644 index 00000000..1310538b --- /dev/null +++ b/volatile/extern/jemalloc-cmake/CMakeLists.txt @@ -0,0 +1,108 @@ +if (NOT CMAKE_SYSTEM_NAME MATCHES "Linux") + message (FATAL_ERROR "Not supported for non-linux environment") +endif() + +message (STATUS "Configure to build jemalloc") + +set (JEMALLOC_CONFIG_MALLOC_CONF "percpu_arena:percpu,oversize_threshold:0,muzzy_decay_ms:5000,dirty_decay_ms:5000") + +set (JEMALLOC_CONFIG_MALLOC_CONF_OVERRIDE "" CACHE STRING "Change default configuration string of jemalloc" ) +if (JEMALLOC_CONFIG_MALLOC_CONF_OVERRIDE) + set (JEMALLOC_CONFIG_MALLOC_CONF "${JEMALLOC_CONFIG_MALLOC_CONF_OVERRIDE}") +endif() +message (STATUS "jemalloc malloc_conf: ${JEMALLOC_CONFIG_MALLOC_CONF}") + +set (JEMALLOC_SOURCE_DIR "${PROJECT_SOURCE_DIR}/extern/jemalloc") + +set (SRCS + "${JEMALLOC_SOURCE_DIR}/src/arena.c" + "${JEMALLOC_SOURCE_DIR}/src/background_thread.c" + "${JEMALLOC_SOURCE_DIR}/src/base.c" + "${JEMALLOC_SOURCE_DIR}/src/bin.c" + "${JEMALLOC_SOURCE_DIR}/src/bin_info.c" + "${JEMALLOC_SOURCE_DIR}/src/bitmap.c" + "${JEMALLOC_SOURCE_DIR}/src/buf_writer.c" + "${JEMALLOC_SOURCE_DIR}/src/cache_bin.c" + "${JEMALLOC_SOURCE_DIR}/src/ckh.c" + "${JEMALLOC_SOURCE_DIR}/src/counter.c" + "${JEMALLOC_SOURCE_DIR}/src/ctl.c" + "${JEMALLOC_SOURCE_DIR}/src/decay.c" + "${JEMALLOC_SOURCE_DIR}/src/div.c" + "${JEMALLOC_SOURCE_DIR}/src/ecache.c" + "${JEMALLOC_SOURCE_DIR}/src/edata.c" + "${JEMALLOC_SOURCE_DIR}/src/edata_cache.c" + "${JEMALLOC_SOURCE_DIR}/src/ehooks.c" + "${JEMALLOC_SOURCE_DIR}/src/emap.c" + "${JEMALLOC_SOURCE_DIR}/src/eset.c" + "${JEMALLOC_SOURCE_DIR}/src/exp_grow.c" + "${JEMALLOC_SOURCE_DIR}/src/extent.c" + "${JEMALLOC_SOURCE_DIR}/src/extent_dss.c" + "${JEMALLOC_SOURCE_DIR}/src/extent_mmap.c" + "${JEMALLOC_SOURCE_DIR}/src/fxp.c" + "${JEMALLOC_SOURCE_DIR}/src/hook.c" + "${JEMALLOC_SOURCE_DIR}/src/hpa.c" + "${JEMALLOC_SOURCE_DIR}/src/hpa_hooks.c" + "${JEMALLOC_SOURCE_DIR}/src/hpdata.c" + "${JEMALLOC_SOURCE_DIR}/src/inspect.c" + "${JEMALLOC_SOURCE_DIR}/src/jemalloc.c" + "${JEMALLOC_SOURCE_DIR}/src/large.c" + "${JEMALLOC_SOURCE_DIR}/src/log.c" + "${JEMALLOC_SOURCE_DIR}/src/malloc_io.c" + "${JEMALLOC_SOURCE_DIR}/src/mutex.c" + "${JEMALLOC_SOURCE_DIR}/src/nstime.c" + "${JEMALLOC_SOURCE_DIR}/src/pa.c" + "${JEMALLOC_SOURCE_DIR}/src/pac.c" + "${JEMALLOC_SOURCE_DIR}/src/pa_extra.c" + "${JEMALLOC_SOURCE_DIR}/src/pages.c" + "${JEMALLOC_SOURCE_DIR}/src/pai.c" + "${JEMALLOC_SOURCE_DIR}/src/peak_event.c" + "${JEMALLOC_SOURCE_DIR}/src/prof.c" + "${JEMALLOC_SOURCE_DIR}/src/prof_data.c" + "${JEMALLOC_SOURCE_DIR}/src/prof_log.c" + "${JEMALLOC_SOURCE_DIR}/src/prof_recent.c" + "${JEMALLOC_SOURCE_DIR}/src/prof_stats.c" + "${JEMALLOC_SOURCE_DIR}/src/prof_sys.c" + "${JEMALLOC_SOURCE_DIR}/src/psset.c" + "${JEMALLOC_SOURCE_DIR}/src/rtree.c" + "${JEMALLOC_SOURCE_DIR}/src/safety_check.c" + "${JEMALLOC_SOURCE_DIR}/src/san_bump.c" + "${JEMALLOC_SOURCE_DIR}/src/san.c" + "${JEMALLOC_SOURCE_DIR}/src/sc.c" + "${JEMALLOC_SOURCE_DIR}/src/sec.c" + "${JEMALLOC_SOURCE_DIR}/src/stats.c" + "${JEMALLOC_SOURCE_DIR}/src/sz.c" + "${JEMALLOC_SOURCE_DIR}/src/tcache.c" + "${JEMALLOC_SOURCE_DIR}/src/test_hooks.c" + "${JEMALLOC_SOURCE_DIR}/src/thread_event.c" + "${JEMALLOC_SOURCE_DIR}/src/ticker.c" + "${JEMALLOC_SOURCE_DIR}/src/tsd.c" + "${JEMALLOC_SOURCE_DIR}/src/witness.c" +) + +add_library(_jemalloc ${SRCS}) + +target_include_directories(_jemalloc SYSTEM PUBLIC include) +target_include_directories(_jemalloc SYSTEM PRIVATE "jemalloc/internal") +target_include_directories(_jemalloc SYSTEM PRIVATE "${JEMALLOC_SOURCE_DIR}/include") + +configure_file(include/jemalloc/internal/jemalloc_internal_defs.h.in + include/jemalloc/internal/jemalloc_internal_defs.h) +target_include_directories(_jemalloc SYSTEM PRIVATE + "${CMAKE_CURRENT_BINARY_DIR}/include/jemalloc/internal") + +target_compile_definitions(_jemalloc PRIVATE -DJEMALLOC_NO_PRIVATE_NAMESPACE) + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_definitions(_jemalloc PRIVATE + # -DJEMALLOC_DEBUG=1 # Disable debug due to possible assertion failures + -DJEMALLOC_LOG=1) +endif () + +target_compile_definitions(_jemalloc PRIVATE -DJEMALLOC_PROF=1) + +# for RTLD_NEXT +target_compile_options(_jemalloc PRIVATE -D_GNU_SOURCE) + +set_target_properties(_jemalloc PROPERTIES COMPILE_FLAGS "-w -fPIC") + +add_library(kvdk_extern_lib::jemalloc ALIAS _jemalloc) diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_internal_defs.h.in b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_internal_defs.h.in new file mode 100644 index 00000000..0781740f --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_internal_defs.h.in @@ -0,0 +1,435 @@ +/* include/jemalloc/internal/jemalloc_internal_defs.h. Generated from jemalloc_internal_defs.h.in by configure. */ +#ifndef JEMALLOC_INTERNAL_DEFS_H_ +#define JEMALLOC_INTERNAL_DEFS_H_ +/* + * If JEMALLOC_PREFIX is defined via --with-jemalloc-prefix, it will cause all + * public APIs to be prefixed. This makes it possible, with some care, to use + * multiple allocators simultaneously. + */ +#define JEMALLOC_PREFIX "je_kvdk_" +#define JEMALLOC_CPREFIX "JE_KVDK_" + +/* + * Define overrides for non-standard allocator-related functions if they are + * present on the system. + */ +/* #undef JEMALLOC_OVERRIDE___LIBC_CALLOC */ +/* #undef JEMALLOC_OVERRIDE___LIBC_FREE */ +/* #undef JEMALLOC_OVERRIDE___LIBC_MALLOC */ +/* #undef JEMALLOC_OVERRIDE___LIBC_MEMALIGN */ +/* #undef JEMALLOC_OVERRIDE___LIBC_REALLOC */ +/* #undef JEMALLOC_OVERRIDE___LIBC_VALLOC */ +/* #undef JEMALLOC_OVERRIDE___LIBC_PVALLOC */ +/* #undef JEMALLOC_OVERRIDE___POSIX_MEMALIGN */ + +/* + * JEMALLOC_PRIVATE_NAMESPACE is used as a prefix for all library-private APIs. + * For shared libraries, symbol visibility mechanisms prevent these symbols + * from being exported, but for static libraries, naming collisions are a real + * possibility. + */ +#define JEMALLOC_PRIVATE_NAMESPACE je_ + +/* + * Hyper-threaded CPUs may need a special instruction inside spin loops in + * order to yield to another virtual CPU. + */ +#define CPU_SPINWAIT __asm__ volatile("pause") +/* 1 if CPU_SPINWAIT is defined, 0 otherwise. */ +#define HAVE_CPU_SPINWAIT 1 + +/* + * Number of significant bits in virtual addresses. This may be less than the + * total number of bits in a pointer, e.g. on x64, for which the uppermost 16 + * bits are the same as bit 47. + */ +#define LG_VADDR 48 + +/* Defined if C11 atomics are available. */ +/* #undef JEMALLOC_C11_ATOMICS */ + +/* Defined if GCC __atomic atomics are available. */ +#define JEMALLOC_GCC_ATOMIC_ATOMICS +/* and the 8-bit variant support. */ +#define JEMALLOC_GCC_U8_ATOMIC_ATOMICS + +/* Defined if GCC __sync atomics are available. */ +#define JEMALLOC_GCC_SYNC_ATOMICS +/* and the 8-bit variant support. */ +#define JEMALLOC_GCC_U8_SYNC_ATOMICS + +/* + * Defined if __builtin_clz() and __builtin_clzl() are available. + */ +#define JEMALLOC_HAVE_BUILTIN_CLZ + +/* + * Defined if os_unfair_lock_*() functions are available, as provided by Darwin. + */ +/* #undef JEMALLOC_OS_UNFAIR_LOCK */ + +/* Defined if syscall(2) is usable. */ +#define JEMALLOC_USE_SYSCALL + +/* + * Defined if secure_getenv(3) is available. + */ +/* #undef JEMALLOC_HAVE_SECURE_GETENV */ + +/* + * Defined if issetugid(2) is available. + */ +/* #undef JEMALLOC_HAVE_ISSETUGID */ + +/* Defined if pthread_atfork(3) is available. */ +/* #undef JEMALLOC_HAVE_PTHREAD_ATFORK */ + +/* Defined if pthread_setname_np(3) is available. */ +#define JEMALLOC_HAVE_PTHREAD_SETNAME_NP + +/* Defined if pthread_getname_np(3) is available. */ +#define JEMALLOC_HAVE_PTHREAD_GETNAME_NP + +/* Defined if pthread_get_name_np(3) is available. */ +/* #undef JEMALLOC_HAVE_PTHREAD_GET_NAME_NP */ + +/* + * Defined if clock_gettime(CLOCK_MONOTONIC_COARSE, ...) is available. + */ +#define JEMALLOC_HAVE_CLOCK_MONOTONIC_COARSE + +/* + * Defined if clock_gettime(CLOCK_MONOTONIC, ...) is available. + */ +#define JEMALLOC_HAVE_CLOCK_MONOTONIC + +/* + * Defined if mach_absolute_time() is available. + */ +/* #undef JEMALLOC_HAVE_MACH_ABSOLUTE_TIME */ + +/* + * Defined if clock_gettime(CLOCK_REALTIME, ...) is available. + */ +#define JEMALLOC_HAVE_CLOCK_REALTIME + +/* + * Defined if _malloc_thread_cleanup() exists. At least in the case of + * FreeBSD, pthread_key_create() allocates, which if used during malloc + * bootstrapping will cause recursion into the pthreads library. Therefore, if + * _malloc_thread_cleanup() exists, use it as the basis for thread cleanup in + * malloc_tsd. + */ +/* #undef JEMALLOC_MALLOC_THREAD_CLEANUP */ + +/* + * Defined if threaded initialization is known to be safe on this platform. + * Among other things, it must be possible to initialize a mutex without + * triggering allocation in order for threaded allocation to be safe. + */ +#define JEMALLOC_THREADED_INIT + +/* + * Defined if the pthreads implementation defines + * _pthread_mutex_init_calloc_cb(), in which case the function is used in order + * to avoid recursive allocation during mutex initialization. + */ +/* #undef JEMALLOC_MUTEX_INIT_CB */ + +/* Non-empty if the tls_model attribute is supported. */ +#define JEMALLOC_TLS_MODEL + +/* + * JEMALLOC_DEBUG enables assertions and other sanity checks, and disables + * inline functions. + */ +/* #undef JEMALLOC_DEBUG */ + +/* JEMALLOC_STATS enables statistics calculation. */ +#define JEMALLOC_STATS + +/* JEMALLOC_EXPERIMENTAL_SMALLOCX_API enables experimental smallocx API. */ +/* #undef JEMALLOC_EXPERIMENTAL_SMALLOCX_API */ + +/* JEMALLOC_PROF enables allocation profiling. */ +/* #undef JEMALLOC_PROF */ + +/* Use libunwind for profile backtracing if defined. */ +/* #undef JEMALLOC_PROF_LIBUNWIND */ + +/* Use libgcc for profile backtracing if defined. */ +/* #undef JEMALLOC_PROF_LIBGCC */ + +/* Use gcc intrinsics for profile backtracing if defined. */ +/* #undef JEMALLOC_PROF_GCC */ + +/* JEMALLOC_PAGEID enabled page id */ +/* #undef JEMALLOC_PAGEID */ + +/* JEMALLOC_HAVE_PRCTL checks prctl */ +#define JEMALLOC_HAVE_PRCTL + +/* + * JEMALLOC_DSS enables use of sbrk(2) to allocate extents from the data storage + * segment (DSS). + */ +#define JEMALLOC_DSS + +/* Support memory filling (junk/zero). */ +#define JEMALLOC_FILL + +/* Support utrace(2)-based tracing. */ +/* #undef JEMALLOC_UTRACE */ + +/* Support utrace(2)-based tracing (label based signature). */ +/* #undef JEMALLOC_UTRACE_LABEL */ + +/* Support optional abort() on OOM. */ +/* #undef JEMALLOC_XMALLOC */ + +/* Support lazy locking (avoid locking unless a second thread is launched). */ +/* #undef JEMALLOC_LAZY_LOCK */ + +/* + * Minimum allocation alignment is 2^LG_QUANTUM bytes (ignoring tiny size + * classes). + */ +/* #undef LG_QUANTUM */ + +/* One page is 2^LG_PAGE bytes. */ +#define LG_PAGE 12 + +/* Maximum number of regions in a slab. */ +/* #undef CONFIG_LG_SLAB_MAXREGS */ + +/* + * One huge page is 2^LG_HUGEPAGE bytes. Note that this is defined even if the + * system does not explicitly support huge pages; system calls that require + * explicit huge page support are separately configured. + */ +#define LG_HUGEPAGE 21 + +/* + * If defined, adjacent virtual memory mappings with identical attributes + * automatically coalesce, and they fragment when changes are made to subranges. + * This is the normal order of things for mmap()/munmap(), but on Windows + * VirtualAlloc()/VirtualFree() operations must be precisely matched, i.e. + * mappings do *not* coalesce/fragment. + */ +#define JEMALLOC_MAPS_COALESCE + +/* + * If defined, retain memory for later reuse by default rather than using e.g. + * munmap() to unmap freed extents. This is enabled on 64-bit Linux because + * common sequences of mmap()/munmap() calls will cause virtual memory map + * holes. + */ +#define JEMALLOC_RETAIN + +/* TLS is used to map arenas and magazine caches to threads. */ +#define JEMALLOC_TLS + +/* + * Used to mark unreachable code to quiet "end of non-void" compiler warnings. + * Don't use this directly; instead use unreachable() from util.h + */ +#define JEMALLOC_INTERNAL_UNREACHABLE __builtin_unreachable + +/* + * ffs*() functions to use for bitmapping. Don't use these directly; instead, + * use ffs_*() from util.h. + */ +#define JEMALLOC_INTERNAL_FFSLL __builtin_ffsll +#define JEMALLOC_INTERNAL_FFSL __builtin_ffsl +#define JEMALLOC_INTERNAL_FFS __builtin_ffs + +/* + * popcount*() functions to use for bitmapping. + */ +#define JEMALLOC_INTERNAL_POPCOUNTL __builtin_popcountl +#define JEMALLOC_INTERNAL_POPCOUNT __builtin_popcount + +/* + * If defined, explicitly attempt to more uniformly distribute large allocation + * pointer alignments across all cache indices. + */ +#define JEMALLOC_CACHE_OBLIVIOUS + +/* + * If defined, enable logging facilities. We make this a configure option to + * avoid taking extra branches everywhere. + */ +/* #undef JEMALLOC_LOG */ + +/* + * If defined, use readlinkat() (instead of readlink()) to follow + * /etc/malloc_conf. + */ +/* #undef JEMALLOC_READLINKAT */ + +/* + * Darwin (OS X) uses zones to work around Mach-O symbol override shortcomings. + */ +/* #undef JEMALLOC_ZONE */ + +/* + * Methods for determining whether the OS overcommits. + * JEMALLOC_PROC_SYS_VM_OVERCOMMIT_MEMORY: Linux's + * /proc/sys/vm.overcommit_memory file. + * JEMALLOC_SYSCTL_VM_OVERCOMMIT: FreeBSD's vm.overcommit sysctl. + */ +/* #undef JEMALLOC_SYSCTL_VM_OVERCOMMIT */ +#define JEMALLOC_PROC_SYS_VM_OVERCOMMIT_MEMORY + +/* Defined if madvise(2) is available. */ +#define JEMALLOC_HAVE_MADVISE + +/* + * Defined if transparent huge pages are supported via the MADV_[NO]HUGEPAGE + * arguments to madvise(2). + */ +#define JEMALLOC_HAVE_MADVISE_HUGE + +/* + * Methods for purging unused pages differ between operating systems. + * + * madvise(..., MADV_FREE) : This marks pages as being unused, such that they + * will be discarded rather than swapped out. + * madvise(..., MADV_DONTNEED) : If JEMALLOC_PURGE_MADVISE_DONTNEED_ZEROS is + * defined, this immediately discards pages, + * such that new pages will be demand-zeroed if + * the address region is later touched; + * otherwise this behaves similarly to + * MADV_FREE, though typically with higher + * system overhead. + */ +#define JEMALLOC_PURGE_MADVISE_FREE +#define JEMALLOC_PURGE_MADVISE_DONTNEED +#define JEMALLOC_PURGE_MADVISE_DONTNEED_ZEROS + +/* Defined if madvise(2) is available but MADV_FREE is not (x86 Linux only). */ +#define JEMALLOC_DEFINE_MADVISE_FREE + +/* + * Defined if MADV_DO[NT]DUMP is supported as an argument to madvise. + */ +#define JEMALLOC_MADVISE_DONTDUMP + +/* + * Defined if MADV_[NO]CORE is supported as an argument to madvise. + */ +/* #undef JEMALLOC_MADVISE_NOCORE */ + +/* Defined if mprotect(2) is available. */ +#define JEMALLOC_HAVE_MPROTECT + +/* + * Defined if transparent huge pages (THPs) are supported via the + * MADV_[NO]HUGEPAGE arguments to madvise(2), and THP support is enabled. + */ +/* #undef JEMALLOC_THP */ + +/* Defined if posix_madvise is available. */ +/* #undef JEMALLOC_HAVE_POSIX_MADVISE */ + +/* + * Method for purging unused pages using posix_madvise. + * + * posix_madvise(..., POSIX_MADV_DONTNEED) + */ +/* #undef JEMALLOC_PURGE_POSIX_MADVISE_DONTNEED */ +/* #undef JEMALLOC_PURGE_POSIX_MADVISE_DONTNEED_ZEROS */ + +/* + * Defined if memcntl page admin call is supported + */ +/* #undef JEMALLOC_HAVE_MEMCNTL */ + +/* + * Defined if malloc_size is supported + */ +/* #undef JEMALLOC_HAVE_MALLOC_SIZE */ + +/* Define if operating system has alloca.h header. */ +#define JEMALLOC_HAS_ALLOCA_H + +/* C99 restrict keyword supported. */ +#define JEMALLOC_HAS_RESTRICT + +/* For use by hash code. */ +/* #undef JEMALLOC_BIG_ENDIAN */ + +/* sizeof(int) == 2^LG_SIZEOF_INT. */ +#define LG_SIZEOF_INT 2 + +/* sizeof(long) == 2^LG_SIZEOF_LONG. */ +#define LG_SIZEOF_LONG 3 + +/* sizeof(long long) == 2^LG_SIZEOF_LONG_LONG. */ +#define LG_SIZEOF_LONG_LONG 3 + +/* sizeof(intmax_t) == 2^LG_SIZEOF_INTMAX_T. */ +#define LG_SIZEOF_INTMAX_T 3 + +/* glibc malloc hooks (__malloc_hook, __realloc_hook, __free_hook). */ +/* #undef JEMALLOC_GLIBC_MALLOC_HOOK */ + +/* glibc memalign hook. */ +/* #undef JEMALLOC_GLIBC_MEMALIGN_HOOK */ + +/* pthread support */ +#define JEMALLOC_HAVE_PTHREAD + +/* dlsym() support */ +#define JEMALLOC_HAVE_DLSYM + +/* Adaptive mutex support in pthreads. */ +#define JEMALLOC_HAVE_PTHREAD_MUTEX_ADAPTIVE_NP + +/* GNU specific sched_getcpu support */ +#define JEMALLOC_HAVE_SCHED_GETCPU + +/* GNU specific sched_setaffinity support */ +#define JEMALLOC_HAVE_SCHED_SETAFFINITY + +/* + * If defined, all the features necessary for background threads are present. + */ +#define JEMALLOC_BACKGROUND_THREAD + +/* + * If defined, jemalloc symbols are not exported (doesn't work when + * JEMALLOC_PREFIX is not defined). + */ +/* #undef JEMALLOC_EXPORT */ + +/* config.malloc_conf options string. */ +#define JEMALLOC_CONFIG_MALLOC_CONF "@JEMALLOC_CONFIG_MALLOC_CONF@" + +/* If defined, jemalloc takes the malloc/free/etc. symbol names. */ +/* #undef JEMALLOC_IS_MALLOC */ + +/* + * Defined if strerror_r returns char * if _GNU_SOURCE is defined. + */ +#define JEMALLOC_STRERROR_R_RETURNS_CHAR_WITH_GNU_SOURCE + +/* Performs additional safety checks when defined. */ +/* #undef JEMALLOC_OPT_SAFETY_CHECKS */ + +/* Is C++ support being built? */ +/* #undef JEMALLOC_ENABLE_CXX */ + +/* Performs additional size checks when defined. */ +/* #undef JEMALLOC_OPT_SIZE_CHECKS */ + +/* Allows sampled junk and stash for checking use-after-free when defined. */ +/* #undef JEMALLOC_UAF_DETECTION */ + +/* Darwin VM_MAKE_TAG support */ +/* #undef JEMALLOC_HAVE_VM_MAKE_TAG */ + +/* If defined, realloc(ptr, 0) defaults to "free" instead of "alloc". */ +#define JEMALLOC_ZERO_REALLOC_DEFAULT_FREE + +#endif /* JEMALLOC_INTERNAL_DEFS_H_ */ diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_preamble.h b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_preamble.h new file mode 100644 index 00000000..fafc7889 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/jemalloc_preamble.h @@ -0,0 +1,263 @@ +#ifndef JEMALLOC_PREAMBLE_H +#define JEMALLOC_PREAMBLE_H + +#include "jemalloc_internal_defs.h" +#include "jemalloc/internal/jemalloc_internal_decls.h" + +#if defined(JEMALLOC_UTRACE) || defined(JEMALLOC_UTRACE_LABEL) +#include +# if defined(JEMALLOC_UTRACE) +# define UTRACE_CALL(p, l) utrace(p, l) +# else +# define UTRACE_CALL(p, l) utrace("jemalloc_process", p, l) +# define JEMALLOC_UTRACE +# endif +#endif + +#define JEMALLOC_NO_DEMANGLE +#ifdef JEMALLOC_JET +# undef JEMALLOC_IS_MALLOC +# define JEMALLOC_N(n) jet_##n +# include "jemalloc/internal/public_namespace.h" +# define JEMALLOC_NO_RENAME +# include "../jemalloc.h" +# undef JEMALLOC_NO_RENAME +#else +# define JEMALLOC_N(n) je_##n +# include "../jemalloc.h" +#endif + +#if defined(JEMALLOC_OSATOMIC) +#include +#endif + +#ifdef JEMALLOC_ZONE +#include +#include +#include +#endif + +#include "jemalloc/internal/jemalloc_internal_macros.h" + +/* + * Note that the ordering matters here; the hook itself is name-mangled. We + * want the inclusion of hooks to happen early, so that we hook as much as + * possible. + */ +#ifndef JEMALLOC_NO_PRIVATE_NAMESPACE +# ifndef JEMALLOC_JET +# include "jemalloc/internal/private_namespace.h" +# else +# include "jemalloc/internal/private_namespace_jet.h" +# endif +#endif +#include "jemalloc/internal/test_hooks.h" + +#ifdef JEMALLOC_DEFINE_MADVISE_FREE +# define JEMALLOC_MADV_FREE 8 +#endif + +static const bool config_debug = +#ifdef JEMALLOC_DEBUG + true +#else + false +#endif + ; +static const bool have_dss = +#ifdef JEMALLOC_DSS + true +#else + false +#endif + ; +static const bool have_madvise_huge = +#ifdef JEMALLOC_HAVE_MADVISE_HUGE + true +#else + false +#endif + ; +static const bool config_fill = +#ifdef JEMALLOC_FILL + true +#else + false +#endif + ; +static const bool config_lazy_lock = +#ifdef JEMALLOC_LAZY_LOCK + true +#else + false +#endif + ; +static const char * const config_malloc_conf = JEMALLOC_CONFIG_MALLOC_CONF; +static const bool config_prof = +#ifdef JEMALLOC_PROF + true +#else + false +#endif + ; +static const bool config_prof_libgcc = +#ifdef JEMALLOC_PROF_LIBGCC + true +#else + false +#endif + ; +static const bool config_prof_libunwind = +#ifdef JEMALLOC_PROF_LIBUNWIND + true +#else + false +#endif + ; +static const bool maps_coalesce = +#ifdef JEMALLOC_MAPS_COALESCE + true +#else + false +#endif + ; +static const bool config_stats = +#ifdef JEMALLOC_STATS + true +#else + false +#endif + ; +static const bool config_tls = +#ifdef JEMALLOC_TLS + true +#else + false +#endif + ; +static const bool config_utrace = +#ifdef JEMALLOC_UTRACE + true +#else + false +#endif + ; +static const bool config_xmalloc = +#ifdef JEMALLOC_XMALLOC + true +#else + false +#endif + ; +static const bool config_cache_oblivious = +#ifdef JEMALLOC_CACHE_OBLIVIOUS + true +#else + false +#endif + ; +/* + * Undocumented, for jemalloc development use only at the moment. See the note + * in jemalloc/internal/log.h. + */ +static const bool config_log = +#ifdef JEMALLOC_LOG + true +#else + false +#endif + ; +/* + * Are extra safety checks enabled; things like checking the size of sized + * deallocations, double-frees, etc. + */ +static const bool config_opt_safety_checks = +#ifdef JEMALLOC_OPT_SAFETY_CHECKS + true +#elif defined(JEMALLOC_DEBUG) + /* + * This lets us only guard safety checks by one flag instead of two; fast + * checks can guard solely by config_opt_safety_checks and run in debug mode + * too. + */ + true +#else + false +#endif + ; + +/* + * Extra debugging of sized deallocations too onerous to be included in the + * general safety checks. + */ +static const bool config_opt_size_checks = +#if defined(JEMALLOC_OPT_SIZE_CHECKS) || defined(JEMALLOC_DEBUG) + true +#else + false +#endif + ; + +static const bool config_uaf_detection = +#if defined(JEMALLOC_UAF_DETECTION) || defined(JEMALLOC_DEBUG) + true +#else + false +#endif + ; + +/* Whether or not the C++ extensions are enabled. */ +static const bool config_enable_cxx = +#ifdef JEMALLOC_ENABLE_CXX + true +#else + false +#endif +; + +#if defined(_WIN32) || defined(__APPLE__) || defined(JEMALLOC_HAVE_SCHED_GETCPU) +/* Currently percpu_arena depends on sched_getcpu. */ +#define JEMALLOC_PERCPU_ARENA +#endif +static const bool have_percpu_arena = +#ifdef JEMALLOC_PERCPU_ARENA + true +#else + false +#endif + ; +/* + * Undocumented, and not recommended; the application should take full + * responsibility for tracking provenance. + */ +static const bool force_ivsalloc = +#ifdef JEMALLOC_FORCE_IVSALLOC + true +#else + false +#endif + ; +static const bool have_background_thread = +#ifdef JEMALLOC_BACKGROUND_THREAD + true +#else + false +#endif + ; +static const bool config_high_res_timer = +#ifdef JEMALLOC_HAVE_CLOCK_REALTIME + true +#else + false +#endif + ; + +static const bool have_memcntl = +#ifdef JEMALLOC_HAVE_MEMCNTL + true +#else + false +#endif + ; + +#endif /* JEMALLOC_PREAMBLE_H */ diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_namespace.h b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_namespace.h new file mode 100644 index 00000000..7e8b354a --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_namespace.h @@ -0,0 +1,25 @@ +#define je_aligned_alloc JEMALLOC_N(aligned_alloc) +#define je_calloc JEMALLOC_N(calloc) +#define je_dallocx JEMALLOC_N(dallocx) +#define je_free JEMALLOC_N(free) +#define je_mallctl JEMALLOC_N(mallctl) +#define je_mallctlbymib JEMALLOC_N(mallctlbymib) +#define je_mallctlnametomib JEMALLOC_N(mallctlnametomib) +#define je_malloc JEMALLOC_N(malloc) +#define je_malloc_conf JEMALLOC_N(malloc_conf) +#define je_malloc_conf_2_conf_harder JEMALLOC_N(malloc_conf_2_conf_harder) +#define je_malloc_message JEMALLOC_N(malloc_message) +#define je_malloc_stats_print JEMALLOC_N(malloc_stats_print) +#define je_malloc_usable_size JEMALLOC_N(malloc_usable_size) +#define je_mallocx JEMALLOC_N(mallocx) +#define je_smallocx_36366f3c4c741723369853c923e56999716398fc JEMALLOC_N(smallocx_36366f3c4c741723369853c923e56999716398fc) +#define je_nallocx JEMALLOC_N(nallocx) +#define je_posix_memalign JEMALLOC_N(posix_memalign) +#define je_rallocx JEMALLOC_N(rallocx) +#define je_realloc JEMALLOC_N(realloc) +#define je_sallocx JEMALLOC_N(sallocx) +#define je_sdallocx JEMALLOC_N(sdallocx) +#define je_xallocx JEMALLOC_N(xallocx) +#define je_memalign JEMALLOC_N(memalign) +#define je_valloc JEMALLOC_N(valloc) +#define je_pvalloc JEMALLOC_N(pvalloc) diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_unnamespace.h b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_unnamespace.h new file mode 100644 index 00000000..f809b3f2 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/internal/public_unnamespace.h @@ -0,0 +1,25 @@ +#undef je_aligned_alloc +#undef je_calloc +#undef je_dallocx +#undef je_free +#undef je_mallctl +#undef je_mallctlbymib +#undef je_mallctlnametomib +#undef je_malloc +#undef je_malloc_conf +#undef je_malloc_conf_2_conf_harder +#undef je_malloc_message +#undef je_malloc_stats_print +#undef je_malloc_usable_size +#undef je_mallocx +#undef je_smallocx_36366f3c4c741723369853c923e56999716398fc +#undef je_nallocx +#undef je_posix_memalign +#undef je_rallocx +#undef je_realloc +#undef je_sallocx +#undef je_sdallocx +#undef je_xallocx +#undef je_memalign +#undef je_valloc +#undef je_pvalloc diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc.h new file mode 100644 index 00000000..f70f2f5c --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc.h @@ -0,0 +1,478 @@ +#ifndef JEMALLOC_H_ +#define JEMALLOC_H_ +#ifdef __cplusplus +extern "C" { +#endif + +/* Defined if __attribute__((...)) syntax is supported. */ +#define JEMALLOC_HAVE_ATTR + +/* Defined if alloc_size attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_ALLOC_SIZE + +/* Defined if format_arg(...) attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_FORMAT_ARG + +/* Defined if format(gnu_printf, ...) attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_FORMAT_GNU_PRINTF + +/* Defined if format(printf, ...) attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_FORMAT_PRINTF + +/* Defined if fallthrough attribute is supported. */ +/* #undef JEMALLOC_HAVE_ATTR_FALLTHROUGH */ + +/* Defined if cold attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_COLD + +/* + * Define overrides for non-standard allocator-related functions if they are + * present on the system. + */ +#define JEMALLOC_OVERRIDE_MEMALIGN +#define JEMALLOC_OVERRIDE_VALLOC +#define JEMALLOC_OVERRIDE_PVALLOC + +/* + * At least Linux omits the "const" in: + * + * size_t malloc_usable_size(const void *ptr); + * + * Match the operating system's prototype. + */ +#define JEMALLOC_USABLE_SIZE_CONST + +/* + * If defined, specify throw() for the public function prototypes when compiling + * with C++. The only justification for this is to match the prototypes that + * glibc defines. + */ +#define JEMALLOC_USE_CXX_THROW + +#ifdef _MSC_VER +# ifdef _WIN64 +# define LG_SIZEOF_PTR_WIN 3 +# else +# define LG_SIZEOF_PTR_WIN 2 +# endif +#endif + +/* sizeof(void *) == 2^LG_SIZEOF_PTR. */ +#define LG_SIZEOF_PTR 3 + +/* + * Name mangling for public symbols is controlled by --with-mangling and + * --with-jemalloc-prefix. With default settings the je_ prefix is stripped by + * these macro definitions. + */ +#ifndef JEMALLOC_NO_RENAME +# define je_aligned_alloc je_kvdk_aligned_alloc +# define je_calloc je_kvdk_calloc +# define je_dallocx je_kvdk_dallocx +# define je_free je_kvdk_free +# define je_mallctl je_kvdk_mallctl +# define je_mallctlbymib je_kvdk_mallctlbymib +# define je_mallctlnametomib je_kvdk_mallctlnametomib +# define je_malloc je_kvdk_malloc +# define je_malloc_conf je_kvdk_malloc_conf +# define je_malloc_conf_2_conf_harder je_kvdk_malloc_conf_2_conf_harder +# define je_malloc_message je_kvdk_malloc_message +# define je_malloc_stats_print je_kvdk_malloc_stats_print +# define je_malloc_usable_size je_kvdk_malloc_usable_size +# define je_mallocx je_kvdk_mallocx +# define je_smallocx_36366f3c4c741723369853c923e56999716398fc je_kvdk_smallocx_36366f3c4c741723369853c923e56999716398fc +# define je_nallocx je_kvdk_nallocx +# define je_posix_memalign je_kvdk_posix_memalign +# define je_rallocx je_kvdk_rallocx +# define je_realloc je_kvdk_realloc +# define je_sallocx je_kvdk_sallocx +# define je_sdallocx je_kvdk_sdallocx +# define je_xallocx je_kvdk_xallocx +# define je_memalign je_kvdk_memalign +# define je_valloc je_kvdk_valloc +# define je_pvalloc je_kvdk_pvalloc +#endif + +#include +#include +#include +#include +#include + +#define JEMALLOC_VERSION "5.3.0-17-g36366f3c4c741723369853c923e56999716398fc" +#define JEMALLOC_VERSION_MAJOR 5 +#define JEMALLOC_VERSION_MINOR 3 +#define JEMALLOC_VERSION_BUGFIX 0 +#define JEMALLOC_VERSION_NREV 17 +#define JEMALLOC_VERSION_GID "36366f3c4c741723369853c923e56999716398fc" +#define JEMALLOC_VERSION_GID_IDENT 36366f3c4c741723369853c923e56999716398fc + +#define MALLOCX_LG_ALIGN(la) ((int)(la)) +#if LG_SIZEOF_PTR == 2 +# define MALLOCX_ALIGN(a) ((int)(ffs((int)(a))-1)) +#else +# define MALLOCX_ALIGN(a) \ + ((int)(((size_t)(a) < (size_t)INT_MAX) ? ffs((int)(a))-1 : \ + ffs((int)(((size_t)(a))>>32))+31)) +#endif +#define MALLOCX_ZERO ((int)0x40) +/* + * Bias tcache index bits so that 0 encodes "automatic tcache management", and 1 + * encodes MALLOCX_TCACHE_NONE. + */ +#define MALLOCX_TCACHE(tc) ((int)(((tc)+2) << 8)) +#define MALLOCX_TCACHE_NONE MALLOCX_TCACHE(-1) +/* + * Bias arena index bits so that 0 encodes "use an automatically chosen arena". + */ +#define MALLOCX_ARENA(a) ((((int)(a))+1) << 20) + +/* + * Use as arena index in "arena..{purge,decay,dss}" and + * "stats.arenas..*" mallctl interfaces to select all arenas. This + * definition is intentionally specified in raw decimal format to support + * cpp-based string concatenation, e.g. + * + * #define STRINGIFY_HELPER(x) #x + * #define STRINGIFY(x) STRINGIFY_HELPER(x) + * + * mallctl("arena." STRINGIFY(MALLCTL_ARENAS_ALL) ".purge", NULL, NULL, NULL, + * 0); + */ +#define MALLCTL_ARENAS_ALL 4096 +/* + * Use as arena index in "stats.arenas..*" mallctl interfaces to select + * destroyed arenas. + */ +#define MALLCTL_ARENAS_DESTROYED 4097 + +#if defined(__cplusplus) && defined(JEMALLOC_USE_CXX_THROW) +# define JEMALLOC_CXX_THROW throw() +#else +# define JEMALLOC_CXX_THROW +#endif + +#if defined(_MSC_VER) +# define JEMALLOC_ATTR(s) +# define JEMALLOC_ALIGNED(s) __declspec(align(s)) +# define JEMALLOC_ALLOC_SIZE(s) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) +# ifndef JEMALLOC_EXPORT +# ifdef DLLEXPORT +# define JEMALLOC_EXPORT __declspec(dllexport) +# else +# define JEMALLOC_EXPORT __declspec(dllimport) +# endif +# endif +# define JEMALLOC_FORMAT_ARG(i) +# define JEMALLOC_FORMAT_PRINTF(s, i) +# define JEMALLOC_FALLTHROUGH +# define JEMALLOC_NOINLINE __declspec(noinline) +# ifdef __cplusplus +# define JEMALLOC_NOTHROW __declspec(nothrow) +# else +# define JEMALLOC_NOTHROW +# endif +# define JEMALLOC_SECTION(s) __declspec(allocate(s)) +# define JEMALLOC_RESTRICT_RETURN __declspec(restrict) +# if _MSC_VER >= 1900 && !defined(__EDG__) +# define JEMALLOC_ALLOCATOR __declspec(allocator) +# else +# define JEMALLOC_ALLOCATOR +# endif +# define JEMALLOC_COLD +#elif defined(JEMALLOC_HAVE_ATTR) +# define JEMALLOC_ATTR(s) __attribute__((s)) +# define JEMALLOC_ALIGNED(s) JEMALLOC_ATTR(aligned(s)) +# ifdef JEMALLOC_HAVE_ATTR_ALLOC_SIZE +# define JEMALLOC_ALLOC_SIZE(s) JEMALLOC_ATTR(alloc_size(s)) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) JEMALLOC_ATTR(alloc_size(s1, s2)) +# else +# define JEMALLOC_ALLOC_SIZE(s) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) +# endif +# ifndef JEMALLOC_EXPORT +# define JEMALLOC_EXPORT JEMALLOC_ATTR(visibility("default")) +# endif +# ifdef JEMALLOC_HAVE_ATTR_FORMAT_ARG +# define JEMALLOC_FORMAT_ARG(i) JEMALLOC_ATTR(__format_arg__(3)) +# else +# define JEMALLOC_FORMAT_ARG(i) +# endif +# ifdef JEMALLOC_HAVE_ATTR_FORMAT_GNU_PRINTF +# define JEMALLOC_FORMAT_PRINTF(s, i) JEMALLOC_ATTR(format(gnu_printf, s, i)) +# elif defined(JEMALLOC_HAVE_ATTR_FORMAT_PRINTF) +# define JEMALLOC_FORMAT_PRINTF(s, i) JEMALLOC_ATTR(format(printf, s, i)) +# else +# define JEMALLOC_FORMAT_PRINTF(s, i) +# endif +# ifdef JEMALLOC_HAVE_ATTR_FALLTHROUGH +# define JEMALLOC_FALLTHROUGH JEMALLOC_ATTR(fallthrough) +# else +# define JEMALLOC_FALLTHROUGH +# endif +# define JEMALLOC_NOINLINE JEMALLOC_ATTR(noinline) +# define JEMALLOC_NOTHROW JEMALLOC_ATTR(nothrow) +# define JEMALLOC_SECTION(s) JEMALLOC_ATTR(section(s)) +# define JEMALLOC_RESTRICT_RETURN +# define JEMALLOC_ALLOCATOR +# ifdef JEMALLOC_HAVE_ATTR_COLD +# define JEMALLOC_COLD JEMALLOC_ATTR(__cold__) +# else +# define JEMALLOC_COLD +# endif +#else +# define JEMALLOC_ATTR(s) +# define JEMALLOC_ALIGNED(s) +# define JEMALLOC_ALLOC_SIZE(s) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) +# define JEMALLOC_EXPORT +# define JEMALLOC_FORMAT_PRINTF(s, i) +# define JEMALLOC_FALLTHROUGH +# define JEMALLOC_NOINLINE +# define JEMALLOC_NOTHROW +# define JEMALLOC_SECTION(s) +# define JEMALLOC_RESTRICT_RETURN +# define JEMALLOC_ALLOCATOR +# define JEMALLOC_COLD +#endif + +#if (defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)) && !defined(JEMALLOC_NO_RENAME) +# define JEMALLOC_SYS_NOTHROW +#else +# define JEMALLOC_SYS_NOTHROW JEMALLOC_NOTHROW +#endif + +/* + * The je_ prefix on the following public symbol declarations is an artifact + * of namespace management, and should be omitted in application code unless + * JEMALLOC_NO_DEMANGLE is defined (see jemalloc_mangle.h). + */ +extern JEMALLOC_EXPORT const char *je_malloc_conf; +extern JEMALLOC_EXPORT void (*je_malloc_message)(void *cbopaque, + const char *s); + +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_malloc(size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_calloc(size_t num, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE2(1, 2); +JEMALLOC_EXPORT int JEMALLOC_SYS_NOTHROW je_posix_memalign( + void **memptr, size_t alignment, size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(nonnull(1)); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_aligned_alloc(size_t alignment, + size_t size) JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) + JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_realloc(void *ptr, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT void JEMALLOC_SYS_NOTHROW je_free(void *ptr) + JEMALLOC_CXX_THROW; + +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_NOTHROW *je_mallocx(size_t size, int flags) + JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_NOTHROW *je_rallocx(void *ptr, size_t size, + int flags) JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_xallocx(void *ptr, size_t size, + size_t extra, int flags); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_sallocx(const void *ptr, + int flags) JEMALLOC_ATTR(pure); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_dallocx(void *ptr, int flags); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_sdallocx(void *ptr, size_t size, + int flags); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_nallocx(size_t size, int flags) + JEMALLOC_ATTR(pure); + +JEMALLOC_EXPORT int JEMALLOC_NOTHROW je_mallctl(const char *name, + void *oldp, size_t *oldlenp, void *newp, size_t newlen); +JEMALLOC_EXPORT int JEMALLOC_NOTHROW je_mallctlnametomib(const char *name, + size_t *mibp, size_t *miblenp); +JEMALLOC_EXPORT int JEMALLOC_NOTHROW je_mallctlbymib(const size_t *mib, + size_t miblen, void *oldp, size_t *oldlenp, void *newp, size_t newlen); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_malloc_stats_print( + void (*write_cb)(void *, const char *), void *je_cbopaque, + const char *opts); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_malloc_usable_size( + JEMALLOC_USABLE_SIZE_CONST void *ptr) JEMALLOC_CXX_THROW; +#ifdef JEMALLOC_HAVE_MALLOC_SIZE +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_malloc_size( + const void *ptr); +#endif + +#ifdef JEMALLOC_OVERRIDE_MEMALIGN +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_memalign(size_t alignment, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc); +#endif + +#ifdef JEMALLOC_OVERRIDE_VALLOC +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_valloc(size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(malloc); +#endif + +#ifdef JEMALLOC_OVERRIDE_PVALLOC +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_pvalloc(size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(malloc); +#endif + +typedef struct extent_hooks_s extent_hooks_t; + +/* + * void * + * extent_alloc(extent_hooks_t *extent_hooks, void *new_addr, size_t size, + * size_t alignment, bool *zero, bool *commit, unsigned arena_ind); + */ +typedef void *(extent_alloc_t)(extent_hooks_t *, void *, size_t, size_t, bool *, + bool *, unsigned); + +/* + * bool + * extent_dalloc(extent_hooks_t *extent_hooks, void *addr, size_t size, + * bool committed, unsigned arena_ind); + */ +typedef bool (extent_dalloc_t)(extent_hooks_t *, void *, size_t, bool, + unsigned); + +/* + * void + * extent_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size, + * bool committed, unsigned arena_ind); + */ +typedef void (extent_destroy_t)(extent_hooks_t *, void *, size_t, bool, + unsigned); + +/* + * bool + * extent_commit(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t offset, size_t length, unsigned arena_ind); + */ +typedef bool (extent_commit_t)(extent_hooks_t *, void *, size_t, size_t, size_t, + unsigned); + +/* + * bool + * extent_decommit(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t offset, size_t length, unsigned arena_ind); + */ +typedef bool (extent_decommit_t)(extent_hooks_t *, void *, size_t, size_t, + size_t, unsigned); + +/* + * bool + * extent_purge(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t offset, size_t length, unsigned arena_ind); + */ +typedef bool (extent_purge_t)(extent_hooks_t *, void *, size_t, size_t, size_t, + unsigned); + +/* + * bool + * extent_split(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t size_a, size_t size_b, bool committed, unsigned arena_ind); + */ +typedef bool (extent_split_t)(extent_hooks_t *, void *, size_t, size_t, size_t, + bool, unsigned); + +/* + * bool + * extent_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a, + * void *addr_b, size_t size_b, bool committed, unsigned arena_ind); + */ +typedef bool (extent_merge_t)(extent_hooks_t *, void *, size_t, void *, size_t, + bool, unsigned); + +struct extent_hooks_s { + extent_alloc_t *alloc; + extent_dalloc_t *dalloc; + extent_destroy_t *destroy; + extent_commit_t *commit; + extent_decommit_t *decommit; + extent_purge_t *purge_lazy; + extent_purge_t *purge_forced; + extent_split_t *split; + extent_merge_t *merge; +}; + +/* + * By default application code must explicitly refer to mangled symbol names, + * so that it is possible to use jemalloc in conjunction with another allocator + * in the same application. Define JEMALLOC_MANGLE in order to cause automatic + * name mangling that matches the API prefixing that happened as a result of + * --with-mangling and/or --with-jemalloc-prefix configuration settings. + */ +#ifdef JEMALLOC_MANGLE +# ifndef JEMALLOC_NO_DEMANGLE +# define JEMALLOC_NO_DEMANGLE +# endif +# define aligned_alloc je_aligned_alloc +# define calloc je_calloc +# define dallocx je_dallocx +# define free je_free +# define mallctl je_mallctl +# define mallctlbymib je_mallctlbymib +# define mallctlnametomib je_mallctlnametomib +# define malloc je_malloc +# define malloc_conf je_malloc_conf +# define malloc_conf_2_conf_harder je_malloc_conf_2_conf_harder +# define malloc_message je_malloc_message +# define malloc_stats_print je_malloc_stats_print +# define malloc_usable_size je_malloc_usable_size +# define mallocx je_mallocx +# define smallocx_36366f3c4c741723369853c923e56999716398fc je_smallocx_36366f3c4c741723369853c923e56999716398fc +# define nallocx je_nallocx +# define posix_memalign je_posix_memalign +# define rallocx je_rallocx +# define realloc je_realloc +# define sallocx je_sallocx +# define sdallocx je_sdallocx +# define xallocx je_xallocx +# define memalign je_memalign +# define valloc je_valloc +# define pvalloc je_pvalloc +#endif + +/* + * The je_* macros can be used as stable alternative names for the + * public jemalloc API if JEMALLOC_NO_DEMANGLE is defined. This is primarily + * meant for use in jemalloc itself, but it can be used by application code to + * provide isolation from the name mangling specified via --with-mangling + * and/or --with-jemalloc-prefix. + */ +#ifndef JEMALLOC_NO_DEMANGLE +# undef je_aligned_alloc +# undef je_calloc +# undef je_dallocx +# undef je_free +# undef je_mallctl +# undef je_mallctlbymib +# undef je_mallctlnametomib +# undef je_malloc +# undef je_malloc_conf +# undef je_malloc_conf_2_conf_harder +# undef je_malloc_message +# undef je_malloc_stats_print +# undef je_malloc_usable_size +# undef je_mallocx +# undef je_smallocx_36366f3c4c741723369853c923e56999716398fc +# undef je_nallocx +# undef je_posix_memalign +# undef je_rallocx +# undef je_realloc +# undef je_sallocx +# undef je_sdallocx +# undef je_xallocx +# undef je_memalign +# undef je_valloc +# undef je_pvalloc +#endif + +#ifdef __cplusplus +} +#endif +#endif /* JEMALLOC_H_ */ diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_defs.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_defs.h new file mode 100644 index 00000000..341c6e98 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_defs.h @@ -0,0 +1,56 @@ +/* include/jemalloc/jemalloc_defs.h. Generated from jemalloc_defs.h.in by configure. */ +/* Defined if __attribute__((...)) syntax is supported. */ +#define JEMALLOC_HAVE_ATTR + +/* Defined if alloc_size attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_ALLOC_SIZE + +/* Defined if format_arg(...) attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_FORMAT_ARG + +/* Defined if format(gnu_printf, ...) attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_FORMAT_GNU_PRINTF + +/* Defined if format(printf, ...) attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_FORMAT_PRINTF + +/* Defined if fallthrough attribute is supported. */ +/* #undef JEMALLOC_HAVE_ATTR_FALLTHROUGH */ + +/* Defined if cold attribute is supported. */ +#define JEMALLOC_HAVE_ATTR_COLD + +/* + * Define overrides for non-standard allocator-related functions if they are + * present on the system. + */ +#define JEMALLOC_OVERRIDE_MEMALIGN +#define JEMALLOC_OVERRIDE_VALLOC +#define JEMALLOC_OVERRIDE_PVALLOC + +/* + * At least Linux omits the "const" in: + * + * size_t malloc_usable_size(const void *ptr); + * + * Match the operating system's prototype. + */ +#define JEMALLOC_USABLE_SIZE_CONST + +/* + * If defined, specify throw() for the public function prototypes when compiling + * with C++. The only justification for this is to match the prototypes that + * glibc defines. + */ +#define JEMALLOC_USE_CXX_THROW + +#ifdef _MSC_VER +# ifdef _WIN64 +# define LG_SIZEOF_PTR_WIN 3 +# else +# define LG_SIZEOF_PTR_WIN 2 +# endif +#endif + +/* sizeof(void *) == 2^LG_SIZEOF_PTR. */ +#define LG_SIZEOF_PTR 3 diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_macros.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_macros.h new file mode 100644 index 00000000..c02e5608 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_macros.h @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include + +#define JEMALLOC_VERSION "5.3.0-17-g36366f3c4c741723369853c923e56999716398fc" +#define JEMALLOC_VERSION_MAJOR 5 +#define JEMALLOC_VERSION_MINOR 3 +#define JEMALLOC_VERSION_BUGFIX 0 +#define JEMALLOC_VERSION_NREV 17 +#define JEMALLOC_VERSION_GID "36366f3c4c741723369853c923e56999716398fc" +#define JEMALLOC_VERSION_GID_IDENT 36366f3c4c741723369853c923e56999716398fc + +#define MALLOCX_LG_ALIGN(la) ((int)(la)) +#if LG_SIZEOF_PTR == 2 +# define MALLOCX_ALIGN(a) ((int)(ffs((int)(a))-1)) +#else +# define MALLOCX_ALIGN(a) \ + ((int)(((size_t)(a) < (size_t)INT_MAX) ? ffs((int)(a))-1 : \ + ffs((int)(((size_t)(a))>>32))+31)) +#endif +#define MALLOCX_ZERO ((int)0x40) +/* + * Bias tcache index bits so that 0 encodes "automatic tcache management", and 1 + * encodes MALLOCX_TCACHE_NONE. + */ +#define MALLOCX_TCACHE(tc) ((int)(((tc)+2) << 8)) +#define MALLOCX_TCACHE_NONE MALLOCX_TCACHE(-1) +/* + * Bias arena index bits so that 0 encodes "use an automatically chosen arena". + */ +#define MALLOCX_ARENA(a) ((((int)(a))+1) << 20) + +/* + * Use as arena index in "arena..{purge,decay,dss}" and + * "stats.arenas..*" mallctl interfaces to select all arenas. This + * definition is intentionally specified in raw decimal format to support + * cpp-based string concatenation, e.g. + * + * #define STRINGIFY_HELPER(x) #x + * #define STRINGIFY(x) STRINGIFY_HELPER(x) + * + * mallctl("arena." STRINGIFY(MALLCTL_ARENAS_ALL) ".purge", NULL, NULL, NULL, + * 0); + */ +#define MALLCTL_ARENAS_ALL 4096 +/* + * Use as arena index in "stats.arenas..*" mallctl interfaces to select + * destroyed arenas. + */ +#define MALLCTL_ARENAS_DESTROYED 4097 + +#if defined(__cplusplus) && defined(JEMALLOC_USE_CXX_THROW) +# define JEMALLOC_CXX_THROW throw() +#else +# define JEMALLOC_CXX_THROW +#endif + +#if defined(_MSC_VER) +# define JEMALLOC_ATTR(s) +# define JEMALLOC_ALIGNED(s) __declspec(align(s)) +# define JEMALLOC_ALLOC_SIZE(s) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) +# ifndef JEMALLOC_EXPORT +# ifdef DLLEXPORT +# define JEMALLOC_EXPORT __declspec(dllexport) +# else +# define JEMALLOC_EXPORT __declspec(dllimport) +# endif +# endif +# define JEMALLOC_FORMAT_ARG(i) +# define JEMALLOC_FORMAT_PRINTF(s, i) +# define JEMALLOC_FALLTHROUGH +# define JEMALLOC_NOINLINE __declspec(noinline) +# ifdef __cplusplus +# define JEMALLOC_NOTHROW __declspec(nothrow) +# else +# define JEMALLOC_NOTHROW +# endif +# define JEMALLOC_SECTION(s) __declspec(allocate(s)) +# define JEMALLOC_RESTRICT_RETURN __declspec(restrict) +# if _MSC_VER >= 1900 && !defined(__EDG__) +# define JEMALLOC_ALLOCATOR __declspec(allocator) +# else +# define JEMALLOC_ALLOCATOR +# endif +# define JEMALLOC_COLD +#elif defined(JEMALLOC_HAVE_ATTR) +# define JEMALLOC_ATTR(s) __attribute__((s)) +# define JEMALLOC_ALIGNED(s) JEMALLOC_ATTR(aligned(s)) +# ifdef JEMALLOC_HAVE_ATTR_ALLOC_SIZE +# define JEMALLOC_ALLOC_SIZE(s) JEMALLOC_ATTR(alloc_size(s)) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) JEMALLOC_ATTR(alloc_size(s1, s2)) +# else +# define JEMALLOC_ALLOC_SIZE(s) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) +# endif +# ifndef JEMALLOC_EXPORT +# define JEMALLOC_EXPORT JEMALLOC_ATTR(visibility("default")) +# endif +# ifdef JEMALLOC_HAVE_ATTR_FORMAT_ARG +# define JEMALLOC_FORMAT_ARG(i) JEMALLOC_ATTR(__format_arg__(3)) +# else +# define JEMALLOC_FORMAT_ARG(i) +# endif +# ifdef JEMALLOC_HAVE_ATTR_FORMAT_GNU_PRINTF +# define JEMALLOC_FORMAT_PRINTF(s, i) JEMALLOC_ATTR(format(gnu_printf, s, i)) +# elif defined(JEMALLOC_HAVE_ATTR_FORMAT_PRINTF) +# define JEMALLOC_FORMAT_PRINTF(s, i) JEMALLOC_ATTR(format(printf, s, i)) +# else +# define JEMALLOC_FORMAT_PRINTF(s, i) +# endif +# ifdef JEMALLOC_HAVE_ATTR_FALLTHROUGH +# define JEMALLOC_FALLTHROUGH JEMALLOC_ATTR(fallthrough) +# else +# define JEMALLOC_FALLTHROUGH +# endif +# define JEMALLOC_NOINLINE JEMALLOC_ATTR(noinline) +# define JEMALLOC_NOTHROW JEMALLOC_ATTR(nothrow) +# define JEMALLOC_SECTION(s) JEMALLOC_ATTR(section(s)) +# define JEMALLOC_RESTRICT_RETURN +# define JEMALLOC_ALLOCATOR +# ifdef JEMALLOC_HAVE_ATTR_COLD +# define JEMALLOC_COLD JEMALLOC_ATTR(__cold__) +# else +# define JEMALLOC_COLD +# endif +#else +# define JEMALLOC_ATTR(s) +# define JEMALLOC_ALIGNED(s) +# define JEMALLOC_ALLOC_SIZE(s) +# define JEMALLOC_ALLOC_SIZE2(s1, s2) +# define JEMALLOC_EXPORT +# define JEMALLOC_FORMAT_PRINTF(s, i) +# define JEMALLOC_FALLTHROUGH +# define JEMALLOC_NOINLINE +# define JEMALLOC_NOTHROW +# define JEMALLOC_SECTION(s) +# define JEMALLOC_RESTRICT_RETURN +# define JEMALLOC_ALLOCATOR +# define JEMALLOC_COLD +#endif + +#if (defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)) && !defined(JEMALLOC_NO_RENAME) +# define JEMALLOC_SYS_NOTHROW +#else +# define JEMALLOC_SYS_NOTHROW JEMALLOC_NOTHROW +#endif diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle.h new file mode 100644 index 00000000..b87a1f33 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle.h @@ -0,0 +1,72 @@ +/* + * By default application code must explicitly refer to mangled symbol names, + * so that it is possible to use jemalloc in conjunction with another allocator + * in the same application. Define JEMALLOC_MANGLE in order to cause automatic + * name mangling that matches the API prefixing that happened as a result of + * --with-mangling and/or --with-jemalloc-prefix configuration settings. + */ +#ifdef JEMALLOC_MANGLE +# ifndef JEMALLOC_NO_DEMANGLE +# define JEMALLOC_NO_DEMANGLE +# endif +# define aligned_alloc je_aligned_alloc +# define calloc je_calloc +# define dallocx je_dallocx +# define free je_free +# define mallctl je_mallctl +# define mallctlbymib je_mallctlbymib +# define mallctlnametomib je_mallctlnametomib +# define malloc je_malloc +# define malloc_conf je_malloc_conf +# define malloc_conf_2_conf_harder je_malloc_conf_2_conf_harder +# define malloc_message je_malloc_message +# define malloc_stats_print je_malloc_stats_print +# define malloc_usable_size je_malloc_usable_size +# define mallocx je_mallocx +# define smallocx_36366f3c4c741723369853c923e56999716398fc je_smallocx_36366f3c4c741723369853c923e56999716398fc +# define nallocx je_nallocx +# define posix_memalign je_posix_memalign +# define rallocx je_rallocx +# define realloc je_realloc +# define sallocx je_sallocx +# define sdallocx je_sdallocx +# define xallocx je_xallocx +# define memalign je_memalign +# define valloc je_valloc +# define pvalloc je_pvalloc +#endif + +/* + * The je_* macros can be used as stable alternative names for the + * public jemalloc API if JEMALLOC_NO_DEMANGLE is defined. This is primarily + * meant for use in jemalloc itself, but it can be used by application code to + * provide isolation from the name mangling specified via --with-mangling + * and/or --with-jemalloc-prefix. + */ +#ifndef JEMALLOC_NO_DEMANGLE +# undef je_aligned_alloc +# undef je_calloc +# undef je_dallocx +# undef je_free +# undef je_mallctl +# undef je_mallctlbymib +# undef je_mallctlnametomib +# undef je_malloc +# undef je_malloc_conf +# undef je_malloc_conf_2_conf_harder +# undef je_malloc_message +# undef je_malloc_stats_print +# undef je_malloc_usable_size +# undef je_mallocx +# undef je_smallocx_36366f3c4c741723369853c923e56999716398fc +# undef je_nallocx +# undef je_posix_memalign +# undef je_rallocx +# undef je_realloc +# undef je_sallocx +# undef je_sdallocx +# undef je_xallocx +# undef je_memalign +# undef je_valloc +# undef je_pvalloc +#endif diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle_jet.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle_jet.h new file mode 100644 index 00000000..d883fe69 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_mangle_jet.h @@ -0,0 +1,72 @@ +/* + * By default application code must explicitly refer to mangled symbol names, + * so that it is possible to use jemalloc in conjunction with another allocator + * in the same application. Define JEMALLOC_MANGLE in order to cause automatic + * name mangling that matches the API prefixing that happened as a result of + * --with-mangling and/or --with-jemalloc-prefix configuration settings. + */ +#ifdef JEMALLOC_MANGLE +# ifndef JEMALLOC_NO_DEMANGLE +# define JEMALLOC_NO_DEMANGLE +# endif +# define aligned_alloc jet_aligned_alloc +# define calloc jet_calloc +# define dallocx jet_dallocx +# define free jet_free +# define mallctl jet_mallctl +# define mallctlbymib jet_mallctlbymib +# define mallctlnametomib jet_mallctlnametomib +# define malloc jet_malloc +# define malloc_conf jet_malloc_conf +# define malloc_conf_2_conf_harder jet_malloc_conf_2_conf_harder +# define malloc_message jet_malloc_message +# define malloc_stats_print jet_malloc_stats_print +# define malloc_usable_size jet_malloc_usable_size +# define mallocx jet_mallocx +# define smallocx_36366f3c4c741723369853c923e56999716398fc jet_smallocx_36366f3c4c741723369853c923e56999716398fc +# define nallocx jet_nallocx +# define posix_memalign jet_posix_memalign +# define rallocx jet_rallocx +# define realloc jet_realloc +# define sallocx jet_sallocx +# define sdallocx jet_sdallocx +# define xallocx jet_xallocx +# define memalign jet_memalign +# define valloc jet_valloc +# define pvalloc jet_pvalloc +#endif + +/* + * The jet_* macros can be used as stable alternative names for the + * public jemalloc API if JEMALLOC_NO_DEMANGLE is defined. This is primarily + * meant for use in jemalloc itself, but it can be used by application code to + * provide isolation from the name mangling specified via --with-mangling + * and/or --with-jemalloc-prefix. + */ +#ifndef JEMALLOC_NO_DEMANGLE +# undef jet_aligned_alloc +# undef jet_calloc +# undef jet_dallocx +# undef jet_free +# undef jet_mallctl +# undef jet_mallctlbymib +# undef jet_mallctlnametomib +# undef jet_malloc +# undef jet_malloc_conf +# undef jet_malloc_conf_2_conf_harder +# undef jet_malloc_message +# undef jet_malloc_stats_print +# undef jet_malloc_usable_size +# undef jet_mallocx +# undef jet_smallocx_36366f3c4c741723369853c923e56999716398fc +# undef jet_nallocx +# undef jet_posix_memalign +# undef jet_rallocx +# undef jet_realloc +# undef jet_sallocx +# undef jet_sdallocx +# undef jet_xallocx +# undef jet_memalign +# undef jet_valloc +# undef jet_pvalloc +#endif diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos.h new file mode 100644 index 00000000..082083d9 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos.h @@ -0,0 +1,77 @@ +/* + * The je_ prefix on the following public symbol declarations is an artifact + * of namespace management, and should be omitted in application code unless + * JEMALLOC_NO_DEMANGLE is defined (see jemalloc_mangle.h). + */ +extern JEMALLOC_EXPORT const char *je_malloc_conf; +extern JEMALLOC_EXPORT void (*je_malloc_message)(void *cbopaque, + const char *s); + +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_malloc(size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_calloc(size_t num, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE2(1, 2); +JEMALLOC_EXPORT int JEMALLOC_SYS_NOTHROW je_posix_memalign( + void **memptr, size_t alignment, size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(nonnull(1)); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_aligned_alloc(size_t alignment, + size_t size) JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) + JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_realloc(void *ptr, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT void JEMALLOC_SYS_NOTHROW je_free(void *ptr) + JEMALLOC_CXX_THROW; + +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_NOTHROW *je_mallocx(size_t size, int flags) + JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_NOTHROW *je_rallocx(void *ptr, size_t size, + int flags) JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_xallocx(void *ptr, size_t size, + size_t extra, int flags); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_sallocx(const void *ptr, + int flags) JEMALLOC_ATTR(pure); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_dallocx(void *ptr, int flags); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_sdallocx(void *ptr, size_t size, + int flags); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_nallocx(size_t size, int flags) + JEMALLOC_ATTR(pure); + +JEMALLOC_EXPORT int JEMALLOC_NOTHROW je_mallctl(const char *name, + void *oldp, size_t *oldlenp, void *newp, size_t newlen); +JEMALLOC_EXPORT int JEMALLOC_NOTHROW je_mallctlnametomib(const char *name, + size_t *mibp, size_t *miblenp); +JEMALLOC_EXPORT int JEMALLOC_NOTHROW je_mallctlbymib(const size_t *mib, + size_t miblen, void *oldp, size_t *oldlenp, void *newp, size_t newlen); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_malloc_stats_print( + void (*write_cb)(void *, const char *), void *je_cbopaque, + const char *opts); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_malloc_usable_size( + JEMALLOC_USABLE_SIZE_CONST void *ptr) JEMALLOC_CXX_THROW; +#ifdef JEMALLOC_HAVE_MALLOC_SIZE +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW je_malloc_size( + const void *ptr); +#endif + +#ifdef JEMALLOC_OVERRIDE_MEMALIGN +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_memalign(size_t alignment, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc); +#endif + +#ifdef JEMALLOC_OVERRIDE_VALLOC +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_valloc(size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(malloc); +#endif + +#ifdef JEMALLOC_OVERRIDE_PVALLOC +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *je_pvalloc(size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(malloc); +#endif diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos_jet.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos_jet.h new file mode 100644 index 00000000..80af5ea0 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_protos_jet.h @@ -0,0 +1,77 @@ +/* + * The jet_ prefix on the following public symbol declarations is an artifact + * of namespace management, and should be omitted in application code unless + * JEMALLOC_NO_DEMANGLE is defined (see jemalloc_mangle@install_suffix@.h). + */ +extern JEMALLOC_EXPORT const char *jet_malloc_conf; +extern JEMALLOC_EXPORT void (*jet_malloc_message)(void *cbopaque, + const char *s); + +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *jet_malloc(size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *jet_calloc(size_t num, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE2(1, 2); +JEMALLOC_EXPORT int JEMALLOC_SYS_NOTHROW jet_posix_memalign( + void **memptr, size_t alignment, size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(nonnull(1)); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *jet_aligned_alloc(size_t alignment, + size_t size) JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc) + JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *jet_realloc(void *ptr, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT void JEMALLOC_SYS_NOTHROW jet_free(void *ptr) + JEMALLOC_CXX_THROW; + +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_NOTHROW *jet_mallocx(size_t size, int flags) + JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1); +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_NOTHROW *jet_rallocx(void *ptr, size_t size, + int flags) JEMALLOC_ALLOC_SIZE(2); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW jet_xallocx(void *ptr, size_t size, + size_t extra, int flags); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW jet_sallocx(const void *ptr, + int flags) JEMALLOC_ATTR(pure); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW jet_dallocx(void *ptr, int flags); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW jet_sdallocx(void *ptr, size_t size, + int flags); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW jet_nallocx(size_t size, int flags) + JEMALLOC_ATTR(pure); + +JEMALLOC_EXPORT int JEMALLOC_NOTHROW jet_mallctl(const char *name, + void *oldp, size_t *oldlenp, void *newp, size_t newlen); +JEMALLOC_EXPORT int JEMALLOC_NOTHROW jet_mallctlnametomib(const char *name, + size_t *mibp, size_t *miblenp); +JEMALLOC_EXPORT int JEMALLOC_NOTHROW jet_mallctlbymib(const size_t *mib, + size_t miblen, void *oldp, size_t *oldlenp, void *newp, size_t newlen); +JEMALLOC_EXPORT void JEMALLOC_NOTHROW jet_malloc_stats_print( + void (*write_cb)(void *, const char *), void *jet_cbopaque, + const char *opts); +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW jet_malloc_usable_size( + JEMALLOC_USABLE_SIZE_CONST void *ptr) JEMALLOC_CXX_THROW; +#ifdef JEMALLOC_HAVE_MALLOC_SIZE +JEMALLOC_EXPORT size_t JEMALLOC_NOTHROW jet_malloc_size( + const void *ptr); +#endif + +#ifdef JEMALLOC_OVERRIDE_MEMALIGN +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *jet_memalign(size_t alignment, size_t size) + JEMALLOC_CXX_THROW JEMALLOC_ATTR(malloc); +#endif + +#ifdef JEMALLOC_OVERRIDE_VALLOC +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *jet_valloc(size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(malloc); +#endif + +#ifdef JEMALLOC_OVERRIDE_PVALLOC +JEMALLOC_EXPORT JEMALLOC_ALLOCATOR JEMALLOC_RESTRICT_RETURN + void JEMALLOC_SYS_NOTHROW *jet_pvalloc(size_t size) JEMALLOC_CXX_THROW + JEMALLOC_ATTR(malloc); +#endif diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_rename.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_rename.h new file mode 100644 index 00000000..dd61f3d7 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_rename.h @@ -0,0 +1,32 @@ +/* + * Name mangling for public symbols is controlled by --with-mangling and + * --with-jemalloc-prefix. With default settings the je_ prefix is stripped by + * these macro definitions. + */ +#ifndef JEMALLOC_NO_RENAME +# define je_aligned_alloc je_kvdk_aligned_alloc +# define je_calloc je_kvdk_calloc +# define je_dallocx je_kvdk_dallocx +# define je_free je_kvdk_free +# define je_mallctl je_kvdk_mallctl +# define je_mallctlbymib je_kvdk_mallctlbymib +# define je_mallctlnametomib je_kvdk_mallctlnametomib +# define je_malloc je_kvdk_malloc +# define je_malloc_conf je_kvdk_malloc_conf +# define je_malloc_conf_2_conf_harder je_kvdk_malloc_conf_2_conf_harder +# define je_malloc_message je_kvdk_malloc_message +# define je_malloc_stats_print je_kvdk_malloc_stats_print +# define je_malloc_usable_size je_kvdk_malloc_usable_size +# define je_mallocx je_kvdk_mallocx +# define je_smallocx_36366f3c4c741723369853c923e56999716398fc je_kvdk_smallocx_36366f3c4c741723369853c923e56999716398fc +# define je_nallocx je_kvdk_nallocx +# define je_posix_memalign je_kvdk_posix_memalign +# define je_rallocx je_kvdk_rallocx +# define je_realloc je_kvdk_realloc +# define je_sallocx je_kvdk_sallocx +# define je_sdallocx je_kvdk_sdallocx +# define je_xallocx je_kvdk_xallocx +# define je_memalign je_kvdk_memalign +# define je_valloc je_kvdk_valloc +# define je_pvalloc je_kvdk_pvalloc +#endif diff --git a/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_typedefs.h b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_typedefs.h new file mode 100644 index 00000000..1a588743 --- /dev/null +++ b/volatile/extern/jemalloc-cmake/include/jemalloc/jemalloc_typedefs.h @@ -0,0 +1,77 @@ +typedef struct extent_hooks_s extent_hooks_t; + +/* + * void * + * extent_alloc(extent_hooks_t *extent_hooks, void *new_addr, size_t size, + * size_t alignment, bool *zero, bool *commit, unsigned arena_ind); + */ +typedef void *(extent_alloc_t)(extent_hooks_t *, void *, size_t, size_t, bool *, + bool *, unsigned); + +/* + * bool + * extent_dalloc(extent_hooks_t *extent_hooks, void *addr, size_t size, + * bool committed, unsigned arena_ind); + */ +typedef bool (extent_dalloc_t)(extent_hooks_t *, void *, size_t, bool, + unsigned); + +/* + * void + * extent_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size, + * bool committed, unsigned arena_ind); + */ +typedef void (extent_destroy_t)(extent_hooks_t *, void *, size_t, bool, + unsigned); + +/* + * bool + * extent_commit(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t offset, size_t length, unsigned arena_ind); + */ +typedef bool (extent_commit_t)(extent_hooks_t *, void *, size_t, size_t, size_t, + unsigned); + +/* + * bool + * extent_decommit(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t offset, size_t length, unsigned arena_ind); + */ +typedef bool (extent_decommit_t)(extent_hooks_t *, void *, size_t, size_t, + size_t, unsigned); + +/* + * bool + * extent_purge(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t offset, size_t length, unsigned arena_ind); + */ +typedef bool (extent_purge_t)(extent_hooks_t *, void *, size_t, size_t, size_t, + unsigned); + +/* + * bool + * extent_split(extent_hooks_t *extent_hooks, void *addr, size_t size, + * size_t size_a, size_t size_b, bool committed, unsigned arena_ind); + */ +typedef bool (extent_split_t)(extent_hooks_t *, void *, size_t, size_t, size_t, + bool, unsigned); + +/* + * bool + * extent_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a, + * void *addr_b, size_t size_b, bool committed, unsigned arena_ind); + */ +typedef bool (extent_merge_t)(extent_hooks_t *, void *, size_t, void *, size_t, + bool, unsigned); + +struct extent_hooks_s { + extent_alloc_t *alloc; + extent_dalloc_t *dalloc; + extent_destroy_t *destroy; + extent_commit_t *commit; + extent_decommit_t *decommit; + extent_purge_t *purge_lazy; + extent_purge_t *purge_forced; + extent_split_t *split; + extent_merge_t *merge; +}; diff --git a/volatile/extern/libpmemobj++/string_view.hpp b/volatile/extern/libpmemobj++/string_view.hpp new file mode 100644 index 00000000..e3461d3a --- /dev/null +++ b/volatile/extern/libpmemobj++/string_view.hpp @@ -0,0 +1,1416 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* Copyright 2020-2021, Intel Corporation */ + +/** + * @file + * Our partial std::string_view implementation. + */ + +#ifndef LIBPMEMOBJ_CPP_STRING_VIEW +#define LIBPMEMOBJ_CPP_STRING_VIEW + +#include +#include +#include +#include +#include + +#if __cpp_lib_string_view +#include +#endif + +namespace pmem +{ + +namespace obj +{ + +#if __cpp_lib_string_view + +template > +using basic_string_view = std::basic_string_view; +using string_view = std::string_view; +using wstring_view = std::basic_string_view; +using u16string_view = std::basic_string_view; +using u32string_view = std::basic_string_view; + +#else + +/** + * Our partial std::string_view implementation. + * + * If C++17's std::string_view implementation is not available, this one + * is used to avoid unnecessary string copying. It's compatible with + * the std API, but it does not cover all functionalities. + * @ingroup data_view + */ +template > +class basic_string_view { +public: + /* Member types */ + using traits_type = Traits; + using value_type = CharT; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + using const_iterator = const_pointer; + using iterator = const_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + static constexpr const size_type npos = + (std::numeric_limits::max)(); + + constexpr basic_string_view() noexcept; + constexpr basic_string_view(const CharT *data, size_type size); + constexpr basic_string_view(const std::basic_string &s); + constexpr basic_string_view(const CharT *data); + + /** Constructor initialized with the basic_string_view *rhs*. + * + * @param[in] rhs basic_string_view to initialize with + */ + constexpr basic_string_view(const basic_string_view &rhs) noexcept = + default; + + /** + * Replaces the view with that of *rhs*. + * + * @param[in] rhs basic_string_view to replace with + */ + basic_string_view & + operator=(const basic_string_view &rhs) noexcept = default; + + constexpr const_iterator begin() const noexcept; + constexpr const_iterator cbegin() const noexcept; + constexpr const_iterator end() const noexcept; + constexpr const_iterator cend() const noexcept; + constexpr const_reverse_iterator rbegin() const noexcept; + constexpr const_reverse_iterator crbegin() const noexcept; + constexpr const_reverse_iterator rend() const noexcept; + constexpr const_reverse_iterator crend() const noexcept; + + constexpr const CharT *data() const noexcept; + constexpr size_type size() const noexcept; + constexpr size_type length() const noexcept; + constexpr bool empty() const noexcept; + constexpr size_type max_size() const noexcept; + + const CharT &at(size_type pos) const; + constexpr const CharT &operator[](size_type pos) const noexcept; + constexpr const_reference front() const noexcept; + constexpr const_reference back() const noexcept; + + void remove_prefix(size_type n); + void remove_suffix(size_type n); + void swap(basic_string_view &v) noexcept; + + constexpr basic_string_view substr(size_type pos = 0, + size_type count = npos) const; + size_type copy(CharT *dest, size_type count, size_type pos = 0) const; + inline int compare(size_type pos1, size_type n1, + basic_string_view sv) const; + inline int compare(size_type pos1, size_type n1, basic_string_view sv, + size_type pos2, size_type n2) const; + inline int compare(const CharT *s) const noexcept; + inline int compare(size_type pos1, size_type n1, const CharT *s) const; + inline int compare(size_type pos1, size_type n1, const CharT *s, + size_type n2) const; + int compare(const basic_string_view &other) const noexcept; + + size_type find(basic_string_view str, size_type pos = 0) const noexcept; + size_type find(CharT ch, size_type pos = 0) const noexcept; + size_type find(const CharT *s, size_type pos = 0) const; + size_type find(const CharT *s, size_type pos, size_type count) const; + + size_type rfind(basic_string_view str, size_type pos = npos) const + noexcept; + size_type rfind(const CharT *s, size_type pos, size_type count) const; + size_type rfind(const CharT *s, size_type pos = npos) const; + size_type rfind(CharT ch, size_type pos = npos) const noexcept; + size_type find_first_of(basic_string_view str, size_type pos = 0) const + noexcept; + size_type find_first_of(const CharT *s, size_type pos, + size_type count) const; + size_type find_first_of(const CharT *s, size_type pos = 0) const; + size_type find_first_of(CharT ch, size_type pos = 0) const noexcept; + size_type find_first_not_of(basic_string_view str, + size_type pos = 0) const noexcept; + size_type find_first_not_of(const CharT *s, size_type pos, + size_type count) const; + size_type find_first_not_of(const CharT *s, size_type pos = 0) const; + size_type find_first_not_of(CharT ch, size_type pos = 0) const noexcept; + size_type find_last_of(basic_string_view str, + size_type pos = npos) const noexcept; + size_type find_last_of(const CharT *s, size_type pos, + size_type count) const; + size_type find_last_of(const CharT *s, size_type pos = npos) const; + size_type find_last_of(CharT ch, size_type pos = npos) const noexcept; + size_type find_last_not_of(basic_string_view str, + size_type pos = npos) const noexcept; + size_type find_last_not_of(const CharT *s, size_type pos, + size_type count) const; + size_type find_last_not_of(const CharT *s, size_type pos = npos) const; + size_type find_last_not_of(CharT ch, size_type pos = npos) const + noexcept; + +private: + const value_type *data_; + size_type size_; +}; + +/** + * The most typical string_view usage - the char specialization. + * @ingroup data_view + */ +using string_view = basic_string_view; + +/** + * The wide char specialization. + * @ingroup data_view + */ +using wstring_view = basic_string_view; + +/** + * The char16 specialization. + * @ingroup data_view + */ +using u16string_view = basic_string_view; + +/** + * The char32 specialization. + * @ingroup data_view + */ +using u32string_view = basic_string_view; + +/** + * Default constructor with empty data. + */ +template +constexpr inline basic_string_view::basic_string_view() noexcept + : data_(nullptr), size_(0) +{ +} + +/** + * Constructor initialized with *data* and its *size*. + * + * @param[in] data pointer to the C-like string to initialize with, + * it can contain null characters. + * @param[in] size length of the given data. + */ +template +constexpr inline basic_string_view::basic_string_view( + const CharT *data, size_type size) + : data_(data), size_(size) +{ +} + +/** + * Constructor initialized by the basic string *s*. + * + * @param[in] s reference to the string to initialize with. + */ +template +constexpr inline basic_string_view::basic_string_view( + const std::basic_string &s) + : data_(s.c_str()), size_(s.size()) +{ +} + +/** + * Constructor initialized by *data*. Size of the data will be set + * using Traits::length(). + * + * @param[in] data pointer to C-like string (char *) to initialize with, + * it has to end with the terminating null character. + */ +template +constexpr inline basic_string_view::basic_string_view( + const CharT *data) + : data_(data), size_(Traits::length(data)) +{ +} + +/** + * Returns an iterator to the first character of the view. + * + * @return const_iterator to the first character + */ +template +constexpr typename basic_string_view::const_iterator +basic_string_view::begin() const noexcept +{ + return cbegin(); +} + +/** + * Returns an iterator to the first character of the view. + * + * @return const_iterator to the first character + */ +template +constexpr typename basic_string_view::const_iterator +basic_string_view::cbegin() const noexcept +{ + return data_; +} + +/** + * Returns an iterator to the character following the last character of the + * view. This character acts as a placeholder, attempting to access it results + * in undefined behavior. + * + * @return const_iterator to the character following the last character. + */ +template +constexpr typename basic_string_view::const_iterator +basic_string_view::end() const noexcept +{ + return cend(); +} + +/** + * Returns an iterator to the character following the last character of the + * view. This character acts as a placeholder, attempting to access it results + * in undefined behavior. + * + * @return const_iterator to the character following the last character. + */ +template +constexpr typename basic_string_view::const_iterator +basic_string_view::cend() const noexcept +{ + return data_ + size_; +} + +/** + * Returns a reverse_iterator to the character following the last character of + * the view (reverse beginning). Reverse iterators iterate backwards: increasing + * them moves them towards the beginning of the string. + * + * @return const_reverse_iterator to the character following the last character. + */ +template +constexpr typename basic_string_view::const_reverse_iterator +basic_string_view::rbegin() const noexcept +{ + return reverse_iterator(cend()); +} + +/** + * Returns a reverse_iterator to the character following the last character of + * the view (reverse beginning). Reverse iterators iterate backwards: increasing + * them moves them towards the beginning of the string. + * + * @return const_reverse_iterator to the character following the last character. + */ +template +constexpr typename basic_string_view::const_reverse_iterator +basic_string_view::crbegin() const noexcept +{ + return reverse_iterator(cend()); +} + +/** + * Returns a reverse_iterator to the first character of the view. + * Reverse iterators iterate backwards: increasing + * them moves them towards the beginning of the string. + * This character acts as a placeholder, attempting to access it results + * in undefined behavior. + * + * @return const_reverse_iterator to the first character + */ +template +constexpr typename basic_string_view::const_reverse_iterator +basic_string_view::rend() const noexcept +{ + return reverse_iterator(cbegin()); +} + +/** + * Returns a reverse_iterator to the first character of the view. + * Reverse iterators iterate backwards: increasing + * them moves them towards the beginning of the string. + * This character acts as a placeholder, attempting to access it results + * in undefined behavior. + * + * @return const_reverse_iterator to the first character + */ +template +constexpr typename basic_string_view::const_reverse_iterator +basic_string_view::crend() const noexcept +{ + return reverse_iterator(cbegin()); +} + +/** + * Returns pointer to data stored in this pmem::obj::string_view. It may + * not contain the terminating null character. + * + * @return pointer to C-like string (char *), it may not end with null + * character. + */ +template +constexpr inline const CharT * +basic_string_view::data() const noexcept +{ + return data_; +} + +/** + * Returns that view is empty or not. + * + * @return true when size() == 0. + */ +template +constexpr inline bool +basic_string_view::empty() const noexcept +{ + return size() == 0; +} + +/** + * Returns the largest possible number of char-like objects that can be + * referred to by a basic_string_view. + * + * @return maximum number of characters. + */ +template +constexpr inline typename basic_string_view::size_type +basic_string_view::max_size() const noexcept +{ + return (std::numeric_limits::max)(); +} + +/** + * Returns count of characters stored in this pmem::obj::string_view + * data. + * + * @return the number of CharT elements in the view. + */ +template +constexpr inline typename basic_string_view::size_type +basic_string_view::size() const noexcept +{ + return size_; +} + +/** + * Returns count of characters stored in this pmem::obj::string_view data. + * + * @return the number of CharT elements in the view. + */ +template +constexpr inline typename basic_string_view::size_type +basic_string_view::length() const noexcept +{ + return size_; +} + +/** + * Returns reference to a character at position @param[in] pos . + * + * @return reference to the char. + */ +template +constexpr inline const CharT & + basic_string_view::operator[](size_t pos) const noexcept +{ + return data()[pos]; +} + +/** + * Returns reference to the character at position @param[in] pos and + * performs bound checking. + * + * @return reference to the char. + * + * @throw std::out_of_range when out of bounds occurs. + */ +template +inline const CharT & +basic_string_view::at(size_t pos) const +{ + if (pos >= size()) + throw std::out_of_range("Accessing a position out of bounds!"); + return data()[pos]; +} + +/** + * Returns reference to the last character in the view. + * The behavior is undefined if empty() == true. + * + * @return reference to the last character. + */ +template +constexpr inline const CharT & +basic_string_view::back() const noexcept +{ + return operator[](size() - 1); +} + +/** + * Returns reference to the first character in the view. + * The behavior is undefined if empty() == true. + * + * @return reference to the first character. + */ +template +constexpr inline const CharT & +basic_string_view::front() const noexcept +{ + return operator[](0); +} + +/** + * Moves the start of the view forward by n characters. + * The behavior is undefined if n > size(). + * + * @param[in] n number of characters to remove from the start of the view + */ +template +void +basic_string_view::remove_prefix(size_type n) +{ + data_ += n; + size_ -= n; +} + +/** + * Moves the end of the view back by n characters. + * The behavior is undefined if n > size(). + * + * @param[in] n number of characters to remove from the end of the view + */ +template +void +basic_string_view::remove_suffix(size_type n) +{ + size_ -= n; +} + +/** + * Exchanges the view with that of v. + * + * @param[in] v view to swap with + */ +template +void +basic_string_view::swap( + basic_string_view &v) noexcept +{ + std::swap(data_, v.data_); + std::swap(size_, v.size_); +} + +/** + * Finds the first substring equal to str. + * + * @param[in] str string to search for + * @param[in] pos position at which to start the search + * + * @return Position of the first character of the found substring or + * npos if no such substring is found. + */ +template +typename basic_string_view::size_type +basic_string_view::find(basic_string_view str, + size_type pos) const noexcept +{ + return find(str.data(), pos, str.size()); +} + +/** + * Finds the first character ch + * + * @param[in] ch character to search for + * @param[in] pos position at which to start the search + * + * @return Position of the first character equal to ch, or npos if no such + * character is found. + */ +template +typename basic_string_view::size_type +basic_string_view::find(CharT ch, size_type pos) const noexcept +{ + return find(&ch, pos, 1); +} + +/** + * Finds the first substring equal to the range [s, s+count). + * This range may contain null characters. + * + * @param[in] s pointer to the C-style string to search for + * @param[in] pos position at which to start the search + * @param[in] count length of the substring to search for + * + * @return Position of the first character of the found substring or + * npos if no such substring is found. + */ +template +typename basic_string_view::size_type +basic_string_view::find(const CharT *s, size_type pos, + size_type count) const +{ + auto sz = size(); + + if (pos > sz) + return npos; + + if (count == 0) + return pos; + + while (pos + count <= sz) { + auto found = traits_type::find(data() + pos, sz - pos, s[0]); + if (!found) + return npos; + pos = static_cast(std::distance(data(), found)); + if (traits_type::compare(found, s, count) == 0) { + return pos; + } + ++pos; + } + return npos; +} + +/** + * Finds the first substring equal to the C-style string pointed to by s. + * The length of the string is determined by the first null character. + * + * @param[in] s pointer to the C-style string to search for + * @param[in] pos position at which to start the search + * + * @return Position of the first character of the found substring or + * npos if no such substring is found. + */ +template +typename basic_string_view::size_type +basic_string_view::find(const CharT *s, size_type pos) const +{ + return find(s, pos, traits_type::length(s)); +} + +/** + * Finds the last substring equal to str. + * If npos or any value not smaller than size()-1 is passed as pos, whole string + * will be searched. + * @param[in] str string to search for + * @param[in] pos position at which to start the search + * + * @return Position (as an offset from the start of the string) of the first + * character of the found substring or npos if no such substring is found + */ +template +typename basic_string_view::size_type +basic_string_view::rfind(basic_string_view str, + size_type pos) const noexcept +{ + return rfind(str.data(), pos, str.size()); +} + +/** + * Finds the last substring equal to the range [s, s+count). + * This range can include null characters. When pos is specified, the search + * only includes sequences of characters that begin at or before position pos, + * ignoring any possible match beginning after pos. If npos or any value not + * smaller than size()-1 is passed as pos, whole string will be searched. + * + * @param[in] s pointer to the C-style string to search for + * @param[in] pos position at which to start the search + * @param[in] count length of the substring to search for + * + * @return Position (as an offset from the start of the string) of the first + * character of the found substring or npos if no such substring is found. If + * searching for an empty string returns pos unless pos > size(), in which + * case returns size(). + */ +template +typename basic_string_view::size_type +basic_string_view::rfind(const CharT *s, size_type pos, + size_type count) const +{ + if (count <= size()) { + pos = (std::min)(size() - count, pos); + do { + if (traits_type::compare(data() + pos, s, count) == 0) + return pos; + } while (pos-- > 0); + } + return npos; +} + +/** + * Finds the last substring equal to the C-style string pointed to by s. + * The length of the string is determined by the first null character. + * If npos or any value not smaller than size()-1 is passed as pos, whole string + * will be searched. + * + * @param[in] s pointer to the C-style string to search for + * @param[in] pos position at which to start the search + * + * @return Position (as an offset from the start of the string) of the first + * character of the found substring or npos if no such substring is found + */ +template +typename basic_string_view::size_type +basic_string_view::rfind(const CharT *s, size_type pos) const +{ + return rfind(s, pos, traits_type::length(s)); +} + +/** + * Finds the last character equal to ch. + * If npos or any value not smaller than size()-1 is passed as pos, whole string + * will be searched. + * + * @param[in] ch character to search for + * @param[in] pos position at which to start the search + * + * @return Position (as an offset from the start of the string) of the first + * character equal to ch or npos if no such character is found + */ +template +typename basic_string_view::size_type +basic_string_view::rfind(CharT ch, size_type pos) const noexcept +{ + return rfind(&ch, pos, 1); +} + +/** + * Finds the first character equal to any of the characters in str. + * + * @param[in] str string identifying characters to search for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_of(basic_string_view str, + size_type pos) const noexcept +{ + return find_first_of(str.data(), pos, str.size()); +} + +/** + * Finds the first character equal to any of the characters + * in the range [s, s+count). This range can include null characters. + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * @param[in] count length of the C-style string identifying characters to + * search for + * + * @return The position of the first character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_of(const CharT *s, size_type pos, + size_type count) const +{ + size_type first_of = npos; + for (const CharT *c = s; c != s + count; ++c) { + size_type found = find(*c, pos); + if (found != npos && found < first_of) + first_of = found; + } + return first_of; +} + +/** + * Finds the first character equal to any of the characters in the C-style + * string pointed to by s. The length of the string is determined by the first + * null character + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_of(const CharT *s, + size_type pos) const +{ + return find_first_of(s, pos, traits_type::length(s)); +} + +/** + * Finds the first character equal to ch + * + * @param[in] ch character to search for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_of(CharT ch, size_type pos) const + noexcept +{ + return find(ch, pos); +} + +/** + * Finds the first character equal to none of the characters in str. + * + * @param[in] str string identifying characters to search for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that does not match. + * If no such characters are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_not_of(basic_string_view str, + size_type pos) const + noexcept +{ + return find_first_not_of(str.data(), pos, str.size()); +} + +/** + * Finds the first character equal to none of the characters + * in the range [s, s+count). This range can include null characters. + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * @param[in] count length of the C-style string identifying characters to + * search for + * + * @return The position of the first character that does not match. + * If no such characters are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_not_of(const CharT *s, + size_type pos, + size_type count) const +{ + if (pos >= size()) + return npos; + + for (auto it = cbegin() + pos; it != cend(); ++it) + if (!traits_type::find(s, count, *it)) + return static_cast( + std::distance(cbegin(), it)); + return npos; +} + +/** + * Finds the first character equal to none of the characters in the C-style + * string pointed to by s. The length of the string is determined by the first + * null character + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that does not match. + * If no such characters are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_not_of(const CharT *s, + size_type pos) const +{ + return find_first_not_of(s, pos, traits_type::length(s)); +} + +/** + * Finds the first character not equal to ch + * + * @param[in] ch character to search for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that does not match. + * If no such characters are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_first_not_of(CharT ch, + size_type pos) const + noexcept +{ + return find_first_not_of(&ch, pos, 1); +} + +/** + * Finds the last character equal to any of the characters in str. + * + * @param[in] str string identifying characters to search for + * @param[in] pos position at which to start the search + * + * @return The position of the last character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_of(basic_string_view str, + size_type pos) const noexcept +{ + return find_last_of(str.data(), pos, str.size()); +} + +/** + * Finds the last character equal to any of the characters + * in the range [s, s+count). This range can include null characters. + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * @param[in] count length of the C-style string identifying characters to + * search for + * + * @return The position of the last character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_of(const CharT *s, size_type pos, + size_type count) const +{ + if (size() == 0 || count == 0) + return npos; + + bool found = false; + size_type last_of = 0; + for (const CharT *c = s; c != s + count; ++c) { + size_type position = rfind(*c, pos); + if (position != npos) { + found = true; + if (position > last_of) + last_of = position; + } + } + if (!found) + return npos; + return last_of; +} + +/** + * Finds the last character equal to any of the characters in the C-style string + * pointed to by s. The length of the string is determined by the + * first null character + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * + * @return The position of the last character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_of(const CharT *s, + size_type pos) const +{ + return find_last_of(s, pos, traits_type::length(s)); +} + +/** + * Finds the last character equal to ch + * + * @param[in] ch character to search for + * @param[in] pos position at which to start the search + * + * @return The position of the last character that matches. + * If no matches are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_of(CharT ch, size_type pos) const + noexcept +{ + return rfind(ch, pos); +} + +/** + * Finds the last character equal to none of the characters in str. + * + * @param[in] str string identifying characters to search for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that does not match. + * If no such characters are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_not_of(basic_string_view str, + size_type pos) const noexcept +{ + return find_last_not_of(str.data(), pos, str.size()); +} + +/** + * Finds the last character equal to none of the characters + * in the range [s, s+count). This range can include null characters. + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * @param[in] count length of the C-style string identifying characters to + * search for + * + * @return The position of the first character that does not match. + * If no such characters are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_not_of(const CharT *s, + size_type pos, + size_type count) const +{ + if (size() > 0) { + pos = (std::min)(pos, size() - 1); + do { + if (!traits_type::find(s, count, *(data() + pos))) + return pos; + + } while (pos-- > 0); + } + return npos; +} + +/** + * Finds the last character equal to none of the characters in the C-style + * string pointed to by s. The length of the string is determined by the first + * null character + * + * @param[in] s pointer to the C-style string identifying characters to search + * for + * @param[in] pos position at which to start the search + * + * @return Position of the first character not equal to any of the characters + * in the given string, or npos if no such character is found. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_not_of(const CharT *s, + size_type pos) const +{ + return find_last_not_of(s, pos, traits_type::length(s)); +} + +/** + * Finds the last character not equal to ch + * + * @param[in] ch character to search for + * @param[in] pos position at which to start the search + * + * @return The position of the first character that does not match. + * If no such characters are found, the function returns npos. + */ +template +typename basic_string_view::size_type +basic_string_view::find_last_not_of(CharT ch, + size_type pos) const noexcept +{ + return find_last_not_of(&ch, pos, 1); +} + +/** + * Returns a view of the substring [pos, pos + rcount), where rcount is the + * smaller of count and size() - pos. + * + * @param[in] pos position of the first character + * @param[in] count requested length + * + * @return view of the substring [pos, pos + rcount). + * + * @throw std::out_of_range if pos > size() + */ +template +constexpr basic_string_view +basic_string_view::substr(size_type pos, size_type count) const +{ + return pos > size() + ? throw std::out_of_range("string_view::substr") + : basic_string_view(data() + pos, + (std::min)(count, size() - pos)); +} + +/** + * Copies the substring [pos, pos + rcount) to the character array pointed to by + * dest, where rcount is the smaller of count and size() - pos. + * + * @param[in] dest pointer to the destination character string + * @param[in] pos position of the first character + * @param[in] count requested substring length + * + * @return number of characters copied. + * + * @throw std::out_of_range if pos > size() + */ +template +typename basic_string_view::size_type +basic_string_view::copy(CharT *dest, size_type count, + size_type pos) const +{ + if (pos > size()) + throw std::out_of_range("string_view::copy"); + size_type rlen = (std::min)(count, size() - pos); + Traits::copy(dest, data() + pos, rlen); + return rlen; +} + +/** + * Compares two character sequences. + * + * @param[in] pos1 position of the first character in this view to compare + * @param[in] n1 number of characters of this view to compare + * @param[in] sv view to compare + * + * @return negative value if this view is less than the other character + * sequence, zero if the both character sequences are equal, positive value if + * this view is greater than the other character sequence. + */ +template +inline int +basic_string_view::compare(size_type pos1, size_type n1, + basic_string_view sv) const +{ + return substr(pos1, n1).compare(sv); +} + +/** + * Compares two character sequences. + * + * @param[in] pos1 position of the first character in this view to compare + * @param[in] n1 number of characters of this view to compare + * @param[in] pos2 position of the first character of the given view to compare + * @param[in] n2 number of characters of the given view to compare + * @param[in] sv view to compare + * + * @return negative value if this view is less than the other character + * sequence, zero if the both character sequences are equal, positive value if + * this view is greater than the other character sequence. + */ +template +inline int +basic_string_view::compare(size_type pos1, size_type n1, + basic_string_view sv, size_type pos2, + size_type n2) const +{ + return substr(pos1, n1).compare(sv.substr(pos2, n2)); +} + +/** + * Compares two character sequences. + * + * @param[in] s pointer to the character string to compare to + * + * @return negative value if this view is less than the other character + * sequence, zero if the both character sequences are equal, positive value if + * this view is greater than the other character sequence. + */ +template +inline int +basic_string_view::compare(const CharT *s) const noexcept +{ + return compare(basic_string_view(s)); +} + +/** + * Compares two character sequences. + * + * @param[in] pos1 position of the first character in this view to compare + * @param[in] n1 number of characters of this view to compare + * @param[in] s pointer to the character string to compare to + * + * @return negative value if this view is less than the other character + * sequence, zero if the both character sequences are equal, positive value if + * this view is greater than the other character sequence. + */ +template +inline int +basic_string_view::compare(size_type pos1, size_type n1, + const CharT *s) const +{ + return substr(pos1, n1).compare(basic_string_view(s)); +} + +/** + * Compares two character sequences. + * + * @param[in] pos1 position of the first character in this view to compare + * @param[in] n1 number of characters of this view to compare + * @param[in] s pointer to the character string to compare to + * @param[in] n2 number of characters of the given string to compare + * + * @return negative value if this view is less than the other character + * sequence, zero if the both character sequences are equal, positive value if + * this view is greater than the other character sequence. + */ +template +inline int +basic_string_view::compare(size_type pos1, size_type n1, + const CharT *s, size_type n2) const +{ + return substr(pos1, n1).compare(basic_string_view(s, n2)); +} + +/** + * Compares this string_view with other. Works in the same way as + * std::basic_string::compare. + * + * @return 0 if both character sequences compare equal, + * positive value if this is lexicographically greater than other, + * negative value if this is lexicographically less than other. + */ +template +inline int +basic_string_view::compare(const basic_string_view &other) const + noexcept +{ + int ret = Traits::compare(data(), other.data(), + (std::min)(size(), other.size())); + if (ret != 0) + return ret; + if (size() < other.size()) + return -1; + if (size() > other.size()) + return 1; + return 0; +} + +/** + * Non-member equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator==(basic_string_view lhs, + basic_string_view rhs) +{ + return (lhs.size() != rhs.size() ? false : lhs.compare(rhs) == 0); +} + +/** + * Non-member equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator==( + basic_string_view lhs, + typename std::common_type>::type rhs) +{ + return (lhs.size() != rhs.size() ? false : lhs.compare(rhs) == 0); +} + +/** + * Non-member equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator==( + typename std::common_type>::type lhs, + basic_string_view rhs) +{ + return (lhs.size() != rhs.size() ? false : lhs.compare(rhs) == 0); +} + +/** + * Non-member not equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator!=(basic_string_view lhs, + basic_string_view rhs) +{ + return (lhs.size() != rhs.size() ? true : lhs.compare(rhs) != 0); +} + +/** + * Non-member not equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator!=( + typename std::common_type>::type lhs, + basic_string_view rhs) +{ + return (lhs.size() != rhs.size() ? true : lhs.compare(rhs) != 0); +} + +/** + * Non-member not equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator!=( + basic_string_view lhs, + typename std::common_type>::type rhs) +{ + return (lhs.size() != rhs.size() ? true : lhs.compare(rhs) != 0); +} + +/** + * Non-member less than operator. + * @relates basic_string_view + */ +template +constexpr bool +operator<(basic_string_view lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) < 0; +} + +/** + * Non-member less than operator. + * @relates basic_string_view + */ +template +constexpr bool +operator<(typename std::common_type>::type lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) < 0; +} + +/** + * Non-member less than operator. + * @relates basic_string_view + */ +template +constexpr bool +operator<(basic_string_view lhs, + typename std::common_type>::type rhs) +{ + return lhs.compare(rhs) < 0; +} + +/** + * Non-member less or equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator<=(basic_string_view lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) <= 0; +} + +/** + * Non-member less or equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator<=( + basic_string_view lhs, + typename std::common_type>::type rhs) +{ + return lhs.compare(rhs) <= 0; +} + +/** + * Non-member less or equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator<=( + typename std::common_type>::type lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) <= 0; +} + +/** + * Non-member greater than operator. + * @relates basic_string_view + */ +template +constexpr bool +operator>(basic_string_view lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) > 0; +} + +/** + * Non-member greater than operator. + * @relates basic_string_view + */ +template +constexpr bool +operator>(typename std::common_type>::type lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) > 0; +} + +/** + * Non-member greater than operator. + * @relates basic_string_view + */ +template +constexpr bool +operator>(basic_string_view lhs, + typename std::common_type>::type rhs) +{ + return lhs.compare(rhs) > 0; +} + +/** + * Non-member greater or equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator>=(basic_string_view lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) >= 0; +} + +/** + * Non-member greater or equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator>=( + typename std::common_type>::type lhs, + basic_string_view rhs) +{ + return lhs.compare(rhs) >= 0; +} + +/** + * Non-member greater or equal operator. + * @relates basic_string_view + */ +template +constexpr bool +operator>=( + basic_string_view lhs, + typename std::common_type>::type rhs) +{ + return lhs.compare(rhs) >= 0; +} +#endif + +} /* namespace obj */ +} /* namespace pmem */ + +#endif /* LIBPMEMOBJ_CPP_STRING_VIEW */ diff --git a/volatile/extern/numactl b/volatile/extern/numactl new file mode 160000 index 00000000..10c277c2 --- /dev/null +++ b/volatile/extern/numactl @@ -0,0 +1 @@ +Subproject commit 10c277c20768be9a563f75265bcd7e73954763ad diff --git a/volatile/extern/numactl-cmake/CMakeLists.txt b/volatile/extern/numactl-cmake/CMakeLists.txt new file mode 100644 index 00000000..69b4373a --- /dev/null +++ b/volatile/extern/numactl-cmake/CMakeLists.txt @@ -0,0 +1,38 @@ +if (NOT CMAKE_SYSTEM_NAME MATCHES "Linux") + message (FATAL_ERROR "Not supported for non-linux environment") +endif() + +message (STATUS "Configure to build libnuma") + +set (LIBNUMA_SOURCE_DIR "${PROJECT_SOURCE_DIR}/extern/numactl") + +set (SRCS + "${LIBNUMA_SOURCE_DIR}/libnuma.c" + "${LIBNUMA_SOURCE_DIR}/syscall.c" + "${LIBNUMA_SOURCE_DIR}/distance.c" + "${LIBNUMA_SOURCE_DIR}/affinity.c" + "${LIBNUMA_SOURCE_DIR}/sysfs.c" + "${LIBNUMA_SOURCE_DIR}/rtnetlink.c" +) + +add_library(_numa ${SRCS}) + +target_include_directories(_numa PRIVATE include) +target_include_directories(_numa SYSTEM PUBLIC "${LIBNUMA_SOURCE_DIR}") + +set_target_properties(_numa PROPERTIES COMPILE_FLAGS "-w -fPIC") + +# disable SYMVER, it make shared libraries failed to link +add_custom_target( + _disable_symver + COMMAND sed -i "'25s/.*/#define SYMVER(a,b)/'" ${LIBNUMA_SOURCE_DIR}/util.h +) + +add_dependencies(_numa _disable_symver) + +add_custom_command( + TARGET _numa POST_BUILD + COMMAND sed -i "'25s/.*/#define SYMVER(a,b) __asm__ (\".symver \" a \",\" b);/'" ${LIBNUMA_SOURCE_DIR}/util.h +) + +add_library(kvdk_extern_lib::numa ALIAS _numa) diff --git a/volatile/extern/numactl-cmake/include/config.h b/volatile/extern/numactl-cmake/include/config.h new file mode 100644 index 00000000..1ea6cd79 --- /dev/null +++ b/volatile/extern/numactl-cmake/include/config.h @@ -0,0 +1,68 @@ +/* config.h. Generated from config.h.in by configure. */ +/* config.h.in. Generated from configure.ac by autoheader. */ + +/* Checking for symver attribute */ +/* #undef HAVE_ATTRIBUTE_SYMVER */ + +/* Define to 1 if you have the header file. */ +#define HAVE_DLFCN_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_INTTYPES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_MEMORY_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRINGS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STAT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UNISTD_H 1 + +/* Define to the sub-directory where libtool stores uninstalled libraries. */ +#define LT_OBJDIR ".libs/" + +/* Name of package */ +#define PACKAGE "numactl" + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "numactl" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "numactl 2.0.14" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "numactl" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "2.0.14" + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* If the compiler supports a TLS storage class define it to that here */ +#define TLS __thread + +/* Version number of package */ +#define VERSION "2.0.14" diff --git a/volatile/extern/xxhash.h b/volatile/extern/xxhash.h new file mode 100644 index 00000000..f5b1bcaa --- /dev/null +++ b/volatile/extern/xxhash.h @@ -0,0 +1,5448 @@ +/* + * xxHash - Extremely Fast Hash algorithm + * Header File + * Copyright (C) 2012-2020 Yann Collet + * + * BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * You can contact the author at: + * - xxHash homepage: https://www.xxhash.com + * - xxHash source repository: https://github.com/Cyan4973/xxHash + */ +/*! + * @mainpage xxHash + * + * @file xxhash.h + * xxHash prototypes and implementation + */ +/* TODO: update */ +/* Notice extracted from xxHash homepage: + +xxHash is an extremely fast hash algorithm, running at RAM speed limits. +It also successfully passes all tests from the SMHasher suite. + +Comparison (single thread, Windows Seven 32 bits, using SMHasher on a Core 2 Duo @3GHz) + +Name Speed Q.Score Author +xxHash 5.4 GB/s 10 +CrapWow 3.2 GB/s 2 Andrew +MumurHash 3a 2.7 GB/s 10 Austin Appleby +SpookyHash 2.0 GB/s 10 Bob Jenkins +SBox 1.4 GB/s 9 Bret Mulvey +Lookup3 1.2 GB/s 9 Bob Jenkins +SuperFastHash 1.2 GB/s 1 Paul Hsieh +CityHash64 1.05 GB/s 10 Pike & Alakuijala +FNV 0.55 GB/s 5 Fowler, Noll, Vo +CRC32 0.43 GB/s 9 +MD5-32 0.33 GB/s 10 Ronald L. Rivest +SHA1-32 0.28 GB/s 10 + +Q.Score is a measure of quality of the hash function. +It depends on successfully passing SMHasher tests set. +10 is a perfect score. + +Note: SMHasher's CRC32 implementation is not the fastest one. +Other speed-oriented implementations can be faster, +especially in combination with PCLMUL instruction: +https://fastcompression.blogspot.com/2019/03/presenting-xxh3.html?showComment=1552696407071#c3490092340461170735 + +A 64-bit version, named XXH64, is available since r35. +It offers much better speed, but for 64-bit applications only. +Name Speed on 64 bits Speed on 32 bits +XXH64 13.8 GB/s 1.9 GB/s +XXH32 6.8 GB/s 6.0 GB/s +*/ + +#if defined (__cplusplus) +extern "C" { +#endif + +/* **************************** + * INLINE mode + ******************************/ +/*! + * XXH_INLINE_ALL (and XXH_PRIVATE_API) + * Use these build macros to inline xxhash into the target unit. + * Inlining improves performance on small inputs, especially when the length is + * expressed as a compile-time constant: + * + * https://fastcompression.blogspot.com/2018/03/xxhash-for-small-keys-impressive-power.html + * + * It also keeps xxHash symbols private to the unit, so they are not exported. + * + * Usage: + * #define XXH_INLINE_ALL + * #include "xxhash.h" + * + * Do not compile and link xxhash.o as a separate object, as it is not useful. + */ +#if (defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API)) \ + && !defined(XXH_INLINE_ALL_31684351384) + /* this section should be traversed only once */ +# define XXH_INLINE_ALL_31684351384 + /* give access to the advanced API, required to compile implementations */ +# undef XXH_STATIC_LINKING_ONLY /* avoid macro redef */ +# define XXH_STATIC_LINKING_ONLY + /* make all functions private */ +# undef XXH_PUBLIC_API +# if defined(__GNUC__) +# define XXH_PUBLIC_API static __inline __attribute__((unused)) +# elif defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) +# define XXH_PUBLIC_API static inline +# elif defined(_MSC_VER) +# define XXH_PUBLIC_API static __inline +# else + /* note: this version may generate warnings for unused static functions */ +# define XXH_PUBLIC_API static +# endif + + /* + * This part deals with the special case where a unit wants to inline xxHash, + * but "xxhash.h" has previously been included without XXH_INLINE_ALL, such + * as part of some previously included *.h header file. + * Without further action, the new include would just be ignored, + * and functions would effectively _not_ be inlined (silent failure). + * The following macros solve this situation by prefixing all inlined names, + * avoiding naming collision with previous inclusions. + */ +# ifdef XXH_NAMESPACE +# error "XXH_INLINE_ALL with XXH_NAMESPACE is not supported" + /* + * Note: Alternative: #undef all symbols (it's a pretty large list). + * Without #error: it compiles, but functions are actually not inlined. + */ +# endif +# define XXH_NAMESPACE XXH_INLINE_ + /* + * Some identifiers (enums, type names) are not symbols, but they must + * still be renamed to avoid redeclaration. + * Alternative solution: do not redeclare them. + * However, this requires some #ifdefs, and is a more dispersed action. + * Meanwhile, renaming can be achieved in a single block + */ +# define XXH_IPREF(Id) XXH_INLINE_ ## Id +# define XXH_OK XXH_IPREF(XXH_OK) +# define XXH_ERROR XXH_IPREF(XXH_ERROR) +# define XXH_errorcode XXH_IPREF(XXH_errorcode) +# define XXH32_canonical_t XXH_IPREF(XXH32_canonical_t) +# define XXH64_canonical_t XXH_IPREF(XXH64_canonical_t) +# define XXH128_canonical_t XXH_IPREF(XXH128_canonical_t) +# define XXH32_state_s XXH_IPREF(XXH32_state_s) +# define XXH32_state_t XXH_IPREF(XXH32_state_t) +# define XXH64_state_s XXH_IPREF(XXH64_state_s) +# define XXH64_state_t XXH_IPREF(XXH64_state_t) +# define XXH3_state_s XXH_IPREF(XXH3_state_s) +# define XXH3_state_t XXH_IPREF(XXH3_state_t) +# define XXH128_hash_t XXH_IPREF(XXH128_hash_t) + /* Ensure the header is parsed again, even if it was previously included */ +# undef XXHASH_H_5627135585666179 +# undef XXHASH_H_STATIC_13879238742 +#endif /* XXH_INLINE_ALL || XXH_PRIVATE_API */ + + + +/* **************************************************************** + * Stable API + *****************************************************************/ +#ifndef XXHASH_H_5627135585666179 +#define XXHASH_H_5627135585666179 1 + + +/*! + * @defgroup public Public API + * Contains details on the public xxHash functions. + * @{ + */ +/* specific declaration modes for Windows */ +#if !defined(XXH_INLINE_ALL) && !defined(XXH_PRIVATE_API) +# if defined(WIN32) && defined(_MSC_VER) && (defined(XXH_IMPORT) || defined(XXH_EXPORT)) +# ifdef XXH_EXPORT +# define XXH_PUBLIC_API __declspec(dllexport) +# elif XXH_IMPORT +# define XXH_PUBLIC_API __declspec(dllimport) +# endif +# else +# define XXH_PUBLIC_API /* do nothing */ +# endif +#endif + +#ifdef XXH_DOXYGEN +/*! + * @brief Emulate a namespace by transparently prefixing all symbols. + * + * If you want to include _and expose_ xxHash functions from within your own + * library, but also want to avoid symbol collisions with other libraries which + * may also include xxHash, you can use XXH_NAMESPACE to automatically prefix + * any public symbol from xxhash library with the value of XXH_NAMESPACE + * (therefore, avoid empty or numeric values). + * + * Note that no change is required within the calling program as long as it + * includes `xxhash.h`: Regular symbol names will be automatically translated + * by this header. + */ +# define XXH_NAMESPACE /* YOUR NAME HERE */ +# undef XXH_NAMESPACE +#endif + +#ifdef XXH_NAMESPACE +# define XXH_CAT(A,B) A##B +# define XXH_NAME2(A,B) XXH_CAT(A,B) +# define XXH_versionNumber XXH_NAME2(XXH_NAMESPACE, XXH_versionNumber) +/* XXH32 */ +# define XXH32 XXH_NAME2(XXH_NAMESPACE, XXH32) +# define XXH32_createState XXH_NAME2(XXH_NAMESPACE, XXH32_createState) +# define XXH32_freeState XXH_NAME2(XXH_NAMESPACE, XXH32_freeState) +# define XXH32_reset XXH_NAME2(XXH_NAMESPACE, XXH32_reset) +# define XXH32_update XXH_NAME2(XXH_NAMESPACE, XXH32_update) +# define XXH32_digest XXH_NAME2(XXH_NAMESPACE, XXH32_digest) +# define XXH32_copyState XXH_NAME2(XXH_NAMESPACE, XXH32_copyState) +# define XXH32_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH32_canonicalFromHash) +# define XXH32_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH32_hashFromCanonical) +/* XXH64 */ +# define XXH64 XXH_NAME2(XXH_NAMESPACE, XXH64) +# define XXH64_createState XXH_NAME2(XXH_NAMESPACE, XXH64_createState) +# define XXH64_freeState XXH_NAME2(XXH_NAMESPACE, XXH64_freeState) +# define XXH64_reset XXH_NAME2(XXH_NAMESPACE, XXH64_reset) +# define XXH64_update XXH_NAME2(XXH_NAMESPACE, XXH64_update) +# define XXH64_digest XXH_NAME2(XXH_NAMESPACE, XXH64_digest) +# define XXH64_copyState XXH_NAME2(XXH_NAMESPACE, XXH64_copyState) +# define XXH64_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH64_canonicalFromHash) +# define XXH64_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH64_hashFromCanonical) +/* XXH3_64bits */ +# define XXH3_64bits XXH_NAME2(XXH_NAMESPACE, XXH3_64bits) +# define XXH3_64bits_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSecret) +# define XXH3_64bits_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSeed) +# define XXH3_createState XXH_NAME2(XXH_NAMESPACE, XXH3_createState) +# define XXH3_freeState XXH_NAME2(XXH_NAMESPACE, XXH3_freeState) +# define XXH3_copyState XXH_NAME2(XXH_NAMESPACE, XXH3_copyState) +# define XXH3_64bits_reset XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset) +# define XXH3_64bits_reset_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSeed) +# define XXH3_64bits_reset_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSecret) +# define XXH3_64bits_update XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_update) +# define XXH3_64bits_digest XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_digest) +# define XXH3_generateSecret XXH_NAME2(XXH_NAMESPACE, XXH3_generateSecret) +/* XXH3_128bits */ +# define XXH128 XXH_NAME2(XXH_NAMESPACE, XXH128) +# define XXH3_128bits XXH_NAME2(XXH_NAMESPACE, XXH3_128bits) +# define XXH3_128bits_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSeed) +# define XXH3_128bits_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSecret) +# define XXH3_128bits_reset XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset) +# define XXH3_128bits_reset_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSeed) +# define XXH3_128bits_reset_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSecret) +# define XXH3_128bits_update XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_update) +# define XXH3_128bits_digest XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_digest) +# define XXH128_isEqual XXH_NAME2(XXH_NAMESPACE, XXH128_isEqual) +# define XXH128_cmp XXH_NAME2(XXH_NAMESPACE, XXH128_cmp) +# define XXH128_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH128_canonicalFromHash) +# define XXH128_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH128_hashFromCanonical) +#endif + + +/* ************************************* +* Version +***************************************/ +#define XXH_VERSION_MAJOR 0 +#define XXH_VERSION_MINOR 8 +#define XXH_VERSION_RELEASE 0 +#define XXH_VERSION_NUMBER (XXH_VERSION_MAJOR *100*100 + XXH_VERSION_MINOR *100 + XXH_VERSION_RELEASE) + +/*! + * @brief Obtains the xxHash version. + * + * This is only useful when xxHash is compiled as a shared library, as it is + * independent of the version defined in the header. + * + * @return `XXH_VERSION_NUMBER` as of when the function was compiled. + */ +XXH_PUBLIC_API unsigned XXH_versionNumber (void); + + +/* **************************** +* Definitions +******************************/ +#include /* size_t */ +typedef enum { XXH_OK=0, XXH_ERROR } XXH_errorcode; + + +/*-********************************************************************** +* 32-bit hash +************************************************************************/ +#if defined(XXH_DOXYGEN) /* Don't show include */ +/*! + * @brief An unsigned 32-bit integer. + * + * Not necessarily defined to `uint32_t` but functionally equivalent. + */ +typedef uint32_t XXH32_hash_t; +#elif !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# include + typedef uint32_t XXH32_hash_t; +#else +# include +# if UINT_MAX == 0xFFFFFFFFUL + typedef unsigned int XXH32_hash_t; +# else +# if ULONG_MAX == 0xFFFFFFFFUL + typedef unsigned long XXH32_hash_t; +# else +# error "unsupported platform: need a 32-bit type" +# endif +# endif +#endif + +/*! + * @} + * + * @defgroup xxh32_family XXH32 family + * @ingroup public + * Contains functions used in the classic 32-bit xxHash algorithm. + * + * @note + * XXH32 is considered rather weak by today's standards. + * The @ref xxh3_family provides competitive speed for both 32-bit and 64-bit + * systems, and offers true 64/128 bit hash results. It provides a superior + * level of dispersion, and greatly reduces the risks of collisions. + * + * @see @ref xxh64_family, @ref xxh3_family : Other xxHash families + * @see @ref xxh32_impl for implementation details + * @{ + */ + +/*! + * @brief Calculates the 32-bit hash of @p input using xxHash32. + * + * Speed on Core 2 Duo @ 3 GHz (single thread, SMHasher benchmark): 5.4 GB/s + * + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * @param seed The 32-bit seed to alter the hash's output predictably. + * + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return The calculated 32-bit hash value. + * + * @see + * XXH64(), XXH3_64bits_withSeed(), XXH3_128bits_withSeed(), XXH128(): + * Direct equivalents for the other variants of xxHash. + * @see + * XXH32_createState(), XXH32_update(), XXH32_digest(): Streaming version. + */ +XXH_PUBLIC_API XXH32_hash_t XXH32 (const void* input, size_t length, XXH32_hash_t seed); + +/*! + * Streaming functions generate the xxHash value from an incrememtal input. + * This method is slower than single-call functions, due to state management. + * For small inputs, prefer `XXH32()` and `XXH64()`, which are better optimized. + * + * An XXH state must first be allocated using `XXH*_createState()`. + * + * Start a new hash by initializing the state with a seed using `XXH*_reset()`. + * + * Then, feed the hash state by calling `XXH*_update()` as many times as necessary. + * + * The function returns an error code, with 0 meaning OK, and any other value + * meaning there is an error. + * + * Finally, a hash value can be produced anytime, by using `XXH*_digest()`. + * This function returns the nn-bits hash as an int or long long. + * + * It's still possible to continue inserting input into the hash state after a + * digest, and generate new hash values later on by invoking `XXH*_digest()`. + * + * When done, release the state using `XXH*_freeState()`. + * + * Example code for incrementally hashing a file: + * @code{.c} + * #include + * #include + * #define BUFFER_SIZE 256 + * + * // Note: XXH64 and XXH3 use the same interface. + * XXH32_hash_t + * hashFile(FILE* stream) + * { + * XXH32_state_t* state; + * unsigned char buf[BUFFER_SIZE]; + * size_t amt; + * XXH32_hash_t hash; + * + * state = XXH32_createState(); // Create a state + * assert(state != NULL); // Error check here + * XXH32_reset(state, 0xbaad5eed); // Reset state with our seed + * while ((amt = fread(buf, 1, sizeof(buf), stream)) != 0) { + * XXH32_update(state, buf, amt); // Hash the file in chunks + * } + * hash = XXH32_digest(state); // Finalize the hash + * XXH32_freeState(state); // Clean up + * return hash; + * } + * @endcode + */ + +/*! + * @typedef struct XXH32_state_s XXH32_state_t + * @brief The opaque state struct for the XXH32 streaming API. + * + * @see XXH32_state_s for details. + */ +typedef struct XXH32_state_s XXH32_state_t; + +/*! + * @brief Allocates an @ref XXH32_state_t. + * + * Must be freed with XXH32_freeState(). + * @return An allocated XXH32_state_t on success, `NULL` on failure. + */ +XXH_PUBLIC_API XXH32_state_t* XXH32_createState(void); +/*! + * @brief Frees an @ref XXH32_state_t. + * + * Must be allocated with XXH32_createState(). + * @param statePtr A pointer to an @ref XXH32_state_t allocated with @ref XXH32_createState(). + * @return XXH_OK. + */ +XXH_PUBLIC_API XXH_errorcode XXH32_freeState(XXH32_state_t* statePtr); +/*! + * @brief Copies one @ref XXH32_state_t to another. + * + * @param dst_state The state to copy to. + * @param src_state The state to copy from. + * @pre + * @p dst_state and @p src_state must not be `NULL` and must not overlap. + */ +XXH_PUBLIC_API void XXH32_copyState(XXH32_state_t* dst_state, const XXH32_state_t* src_state); + +/*! + * @brief Resets an @ref XXH32_state_t to begin a new hash. + * + * This function resets and seeds a state. Call it before @ref XXH32_update(). + * + * @param statePtr The state struct to reset. + * @param seed The 32-bit seed to alter the hash result predictably. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success, @ref XXH_ERROR on failure. + */ +XXH_PUBLIC_API XXH_errorcode XXH32_reset (XXH32_state_t* statePtr, XXH32_hash_t seed); + +/*! + * @brief Consumes a block of @p input to an @ref XXH32_state_t. + * + * Call this to incrementally consume blocks of data. + * + * @param statePtr The state struct to update. + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * + * @pre + * @p statePtr must not be `NULL`. + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return @ref XXH_OK on success, @ref XXH_ERROR on failure. + */ +XXH_PUBLIC_API XXH_errorcode XXH32_update (XXH32_state_t* statePtr, const void* input, size_t length); + +/*! + * @brief Returns the calculated hash value from an @ref XXH32_state_t. + * + * @note + * Calling XXH32_digest() will not affect @p statePtr, so you can update, + * digest, and update again. + * + * @param statePtr The state struct to calculate the hash from. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return The calculated xxHash32 value from that state. + */ +XXH_PUBLIC_API XXH32_hash_t XXH32_digest (const XXH32_state_t* statePtr); + +/******* Canonical representation *******/ + +/* + * The default return values from XXH functions are unsigned 32 and 64 bit + * integers. + * This the simplest and fastest format for further post-processing. + * + * However, this leaves open the question of what is the order on the byte level, + * since little and big endian conventions will store the same number differently. + * + * The canonical representation settles this issue by mandating big-endian + * convention, the same convention as human-readable numbers (large digits first). + * + * When writing hash values to storage, sending them over a network, or printing + * them, it's highly recommended to use the canonical representation to ensure + * portability across a wider range of systems, present and future. + * + * The following functions allow transformation of hash values to and from + * canonical format. + */ + +/*! + * @brief Canonical (big endian) representation of @ref XXH32_hash_t. + */ +typedef struct { + unsigned char digest[4]; /*!< Hash bytes, big endian */ +} XXH32_canonical_t; + +/*! + * @brief Converts an @ref XXH32_hash_t to a big endian @ref XXH32_canonical_t. + * + * @param dst The @ref XXH32_canonical_t pointer to be stored to. + * @param hash The @ref XXH32_hash_t to be converted. + * + * @pre + * @p dst must not be `NULL`. + */ +XXH_PUBLIC_API void XXH32_canonicalFromHash(XXH32_canonical_t* dst, XXH32_hash_t hash); + +/*! + * @brief Converts an @ref XXH32_canonical_t to a native @ref XXH32_hash_t. + * + * @param src The @ref XXH32_canonical_t to convert. + * + * @pre + * @p src must not be `NULL`. + * + * @return The converted hash. + */ +XXH_PUBLIC_API XXH32_hash_t XXH32_hashFromCanonical(const XXH32_canonical_t* src); + + +/*! + * @} + * @ingroup public + * @{ + */ + +#ifndef XXH_NO_LONG_LONG +/*-********************************************************************** +* 64-bit hash +************************************************************************/ +#if defined(XXH_DOXYGEN) /* don't include */ +/*! + * @brief An unsigned 64-bit integer. + * + * Not necessarily defined to `uint64_t` but functionally equivalent. + */ +typedef uint64_t XXH64_hash_t; +#elif !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# include + typedef uint64_t XXH64_hash_t; +#else +# include +# if defined(__LP64__) && ULONG_MAX == 0xFFFFFFFFFFFFFFFFULL + /* LP64 ABI says uint64_t is unsigned long */ + typedef unsigned long XXH64_hash_t; +# else + /* the following type must have a width of 64-bit */ + typedef unsigned long long XXH64_hash_t; +# endif +#endif + +/*! + * @} + * + * @defgroup xxh64_family XXH64 family + * @ingroup public + * @{ + * Contains functions used in the classic 64-bit xxHash algorithm. + * + * @note + * XXH3 provides competitive speed for both 32-bit and 64-bit systems, + * and offers true 64/128 bit hash results. It provides a superior level of + * dispersion, and greatly reduces the risks of collisions. + */ + + +/*! + * @brief Calculates the 64-bit hash of @p input using xxHash64. + * + * This function usually runs faster on 64-bit systems, but slower on 32-bit + * systems (see benchmark). + * + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * @param seed The 64-bit seed to alter the hash's output predictably. + * + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return The calculated 64-bit hash. + * + * @see + * XXH32(), XXH3_64bits_withSeed(), XXH3_128bits_withSeed(), XXH128(): + * Direct equivalents for the other variants of xxHash. + * @see + * XXH64_createState(), XXH64_update(), XXH64_digest(): Streaming version. + */ +XXH_PUBLIC_API XXH64_hash_t XXH64(const void* input, size_t length, XXH64_hash_t seed); + +/******* Streaming *******/ +/*! + * @brief The opaque state struct for the XXH64 streaming API. + * + * @see XXH64_state_s for details. + */ +typedef struct XXH64_state_s XXH64_state_t; /* incomplete type */ +XXH_PUBLIC_API XXH64_state_t* XXH64_createState(void); +XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr); +XXH_PUBLIC_API void XXH64_copyState(XXH64_state_t* dst_state, const XXH64_state_t* src_state); + +XXH_PUBLIC_API XXH_errorcode XXH64_reset (XXH64_state_t* statePtr, XXH64_hash_t seed); +XXH_PUBLIC_API XXH_errorcode XXH64_update (XXH64_state_t* statePtr, const void* input, size_t length); +XXH_PUBLIC_API XXH64_hash_t XXH64_digest (const XXH64_state_t* statePtr); + +/******* Canonical representation *******/ +typedef struct { unsigned char digest[sizeof(XXH64_hash_t)]; } XXH64_canonical_t; +XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash); +XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src); + +/*! + * @} + * ************************************************************************ + * @defgroup xxh3_family XXH3 family + * @ingroup public + * @{ + * + * XXH3 is a more recent hash algorithm featuring: + * - Improved speed for both small and large inputs + * - True 64-bit and 128-bit outputs + * - SIMD acceleration + * - Improved 32-bit viability + * + * Speed analysis methodology is explained here: + * + * https://fastcompression.blogspot.com/2019/03/presenting-xxh3.html + * + * Compared to XXH64, expect XXH3 to run approximately + * ~2x faster on large inputs and >3x faster on small ones, + * exact differences vary depending on platform. + * + * XXH3's speed benefits greatly from SIMD and 64-bit arithmetic, + * but does not require it. + * Any 32-bit and 64-bit targets that can run XXH32 smoothly + * can run XXH3 at competitive speeds, even without vector support. + * Further details are explained in the implementation. + * + * Optimized implementations are provided for AVX512, AVX2, SSE2, NEON, POWER8, + * ZVector and scalar targets. This can be controlled via the XXH_VECTOR macro. + * + * XXH3 implementation is portable: + * it has a generic C90 formulation that can be compiled on any platform, + * all implementations generage exactly the same hash value on all platforms. + * Starting from v0.8.0, it's also labelled "stable", meaning that + * any future version will also generate the same hash value. + * + * XXH3 offers 2 variants, _64bits and _128bits. + * + * When only 64 bits are needed, prefer invoking the _64bits variant, as it + * reduces the amount of mixing, resulting in faster speed on small inputs. + * It's also generally simpler to manipulate a scalar return type than a struct. + * + * The API supports one-shot hashing, streaming mode, and custom secrets. + */ + +/*-********************************************************************** +* XXH3 64-bit variant +************************************************************************/ + +/* XXH3_64bits(): + * default 64-bit variant, using default secret and default seed of 0. + * It's the fastest variant. */ +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits(const void* data, size_t len); + +/* + * XXH3_64bits_withSeed(): + * This variant generates a custom secret on the fly + * based on default secret altered using the `seed` value. + * While this operation is decently fast, note that it's not completely free. + * Note: seed==0 produces the same results as XXH3_64bits(). + */ +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_withSeed(const void* data, size_t len, XXH64_hash_t seed); + +/*! + * The bare minimum size for a custom secret. + * + * @see + * XXH3_64bits_withSecret(), XXH3_64bits_reset_withSecret(), + * XXH3_128bits_withSecret(), XXH3_128bits_reset_withSecret(). + */ +#define XXH3_SECRET_SIZE_MIN 136 + +/* + * XXH3_64bits_withSecret(): + * It's possible to provide any blob of bytes as a "secret" to generate the hash. + * This makes it more difficult for an external actor to prepare an intentional collision. + * The main condition is that secretSize *must* be large enough (>= XXH3_SECRET_SIZE_MIN). + * However, the quality of produced hash values depends on secret's entropy. + * Technically, the secret must look like a bunch of random bytes. + * Avoid "trivial" or structured data such as repeated sequences or a text document. + * Whenever unsure about the "randomness" of the blob of bytes, + * consider relabelling it as a "custom seed" instead, + * and employ "XXH3_generateSecret()" (see below) + * to generate a high entropy secret derived from the custom seed. + */ +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_withSecret(const void* data, size_t len, const void* secret, size_t secretSize); + + +/******* Streaming *******/ +/* + * Streaming requires state maintenance. + * This operation costs memory and CPU. + * As a consequence, streaming is slower than one-shot hashing. + * For better performance, prefer one-shot functions whenever applicable. + */ + +/*! + * @brief The state struct for the XXH3 streaming API. + * + * @see XXH3_state_s for details. + */ +typedef struct XXH3_state_s XXH3_state_t; +XXH_PUBLIC_API XXH3_state_t* XXH3_createState(void); +XXH_PUBLIC_API XXH_errorcode XXH3_freeState(XXH3_state_t* statePtr); +XXH_PUBLIC_API void XXH3_copyState(XXH3_state_t* dst_state, const XXH3_state_t* src_state); + +/* + * XXH3_64bits_reset(): + * Initialize with default parameters. + * digest will be equivalent to `XXH3_64bits()`. + */ +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset(XXH3_state_t* statePtr); +/* + * XXH3_64bits_reset_withSeed(): + * Generate a custom secret from `seed`, and store it into `statePtr`. + * digest will be equivalent to `XXH3_64bits_withSeed()`. + */ +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed); +/* + * XXH3_64bits_reset_withSecret(): + * `secret` is referenced, it _must outlive_ the hash streaming session. + * Similar to one-shot API, `secretSize` must be >= `XXH3_SECRET_SIZE_MIN`, + * and the quality of produced hash values depends on secret's entropy + * (secret's content should look like a bunch of random bytes). + * When in doubt about the randomness of a candidate `secret`, + * consider employing `XXH3_generateSecret()` instead (see below). + */ +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize); + +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_update (XXH3_state_t* statePtr, const void* input, size_t length); +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_digest (const XXH3_state_t* statePtr); + +/* note : canonical representation of XXH3 is the same as XXH64 + * since they both produce XXH64_hash_t values */ + + +/*-********************************************************************** +* XXH3 128-bit variant +************************************************************************/ + +/*! + * @brief The return value from 128-bit hashes. + * + * Stored in little endian order, although the fields themselves are in native + * endianness. + */ +typedef struct { + XXH64_hash_t low64; /*!< `value & 0xFFFFFFFFFFFFFFFF` */ + XXH64_hash_t high64; /*!< `value >> 64` */ +} XXH128_hash_t; + +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits(const void* data, size_t len); +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_withSeed(const void* data, size_t len, XXH64_hash_t seed); +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_withSecret(const void* data, size_t len, const void* secret, size_t secretSize); + +/******* Streaming *******/ +/* + * Streaming requires state maintenance. + * This operation costs memory and CPU. + * As a consequence, streaming is slower than one-shot hashing. + * For better performance, prefer one-shot functions whenever applicable. + * + * XXH3_128bits uses the same XXH3_state_t as XXH3_64bits(). + * Use already declared XXH3_createState() and XXH3_freeState(). + * + * All reset and streaming functions have same meaning as their 64-bit counterpart. + */ + +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset(XXH3_state_t* statePtr); +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed); +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize); + +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_update (XXH3_state_t* statePtr, const void* input, size_t length); +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_digest (const XXH3_state_t* statePtr); + +/* Following helper functions make it possible to compare XXH128_hast_t values. + * Since XXH128_hash_t is a structure, this capability is not offered by the language. + * Note: For better performance, these functions can be inlined using XXH_INLINE_ALL */ + +/*! + * XXH128_isEqual(): + * Return: 1 if `h1` and `h2` are equal, 0 if they are not. + */ +XXH_PUBLIC_API int XXH128_isEqual(XXH128_hash_t h1, XXH128_hash_t h2); + +/*! + * XXH128_cmp(): + * + * This comparator is compatible with stdlib's `qsort()`/`bsearch()`. + * + * return: >0 if *h128_1 > *h128_2 + * =0 if *h128_1 == *h128_2 + * <0 if *h128_1 < *h128_2 + */ +XXH_PUBLIC_API int XXH128_cmp(const void* h128_1, const void* h128_2); + + +/******* Canonical representation *******/ +typedef struct { unsigned char digest[sizeof(XXH128_hash_t)]; } XXH128_canonical_t; +XXH_PUBLIC_API void XXH128_canonicalFromHash(XXH128_canonical_t* dst, XXH128_hash_t hash); +XXH_PUBLIC_API XXH128_hash_t XXH128_hashFromCanonical(const XXH128_canonical_t* src); + + +#endif /* XXH_NO_LONG_LONG */ + +/*! + * @} + */ +#endif /* XXHASH_H_5627135585666179 */ + + + +#if defined(XXH_STATIC_LINKING_ONLY) && !defined(XXHASH_H_STATIC_13879238742) +#define XXHASH_H_STATIC_13879238742 +/* **************************************************************************** + * This section contains declarations which are not guaranteed to remain stable. + * They may change in future versions, becoming incompatible with a different + * version of the library. + * These declarations should only be used with static linking. + * Never use them in association with dynamic linking! + ***************************************************************************** */ + +/* + * These definitions are only present to allow static allocation + * of XXH states, on stack or in a struct, for example. + * Never **ever** access their members directly. + */ + +/*! + * @internal + * @brief Structure for XXH32 streaming API. + * + * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY, + * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. Otherwise it is + * an opaque type. This allows fields to safely be changed. + * + * Typedef'd to @ref XXH32_state_t. + * Do not access the members of this struct directly. + * @see XXH64_state_s, XXH3_state_s + */ +struct XXH32_state_s { + XXH32_hash_t total_len_32; /*!< Total length hashed, modulo 2^32 */ + XXH32_hash_t large_len; /*!< Whether the hash is >= 16 (handles @ref total_len_32 overflow) */ + XXH32_hash_t v1; /*!< First accumulator lane */ + XXH32_hash_t v2; /*!< Second accumulator lane */ + XXH32_hash_t v3; /*!< Third accumulator lane */ + XXH32_hash_t v4; /*!< Fourth accumulator lane */ + XXH32_hash_t mem32[4]; /*!< Internal buffer for partial reads. Treated as unsigned char[16]. */ + XXH32_hash_t memsize; /*!< Amount of data in @ref mem32 */ + XXH32_hash_t reserved; /*!< Reserved field. Do not read or write to it, it may be removed. */ +}; /* typedef'd to XXH32_state_t */ + + +#ifndef XXH_NO_LONG_LONG /* defined when there is no 64-bit support */ + +/*! + * @internal + * @brief Structure for XXH64 streaming API. + * + * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY, + * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. Otherwise it is + * an opaque type. This allows fields to safely be changed. + * + * Typedef'd to @ref XXH64_state_t. + * Do not access the members of this struct directly. + * @see XXH32_state_s, XXH3_state_s + */ +struct XXH64_state_s { + XXH64_hash_t total_len; /*!< Total length hashed. This is always 64-bit. */ + XXH64_hash_t v1; /*!< First accumulator lane */ + XXH64_hash_t v2; /*!< Second accumulator lane */ + XXH64_hash_t v3; /*!< Third accumulator lane */ + XXH64_hash_t v4; /*!< Fourth accumulator lane */ + XXH64_hash_t mem64[4]; /*!< Internal buffer for partial reads. Treated as unsigned char[32]. */ + XXH32_hash_t memsize; /*!< Amount of data in @ref mem64 */ + XXH32_hash_t reserved32; /*!< Reserved field, needed for padding anyways*/ + XXH64_hash_t reserved64; /*!< Reserved field. Do not read or write to it, it may be removed. */ +}; /* typedef'd to XXH64_state_t */ + +#if defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* C11+ */ +# include +# define XXH_ALIGN(n) alignas(n) +#elif defined(__GNUC__) +# define XXH_ALIGN(n) __attribute__ ((aligned(n))) +#elif defined(_MSC_VER) +# define XXH_ALIGN(n) __declspec(align(n)) +#else +# define XXH_ALIGN(n) /* disabled */ +#endif + +/* Old GCC versions only accept the attribute after the type in structures. */ +#if !(defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) /* C11+ */ \ + && defined(__GNUC__) +# define XXH_ALIGN_MEMBER(align, type) type XXH_ALIGN(align) +#else +# define XXH_ALIGN_MEMBER(align, type) XXH_ALIGN(align) type +#endif + +/*! + * @brief The size of the internal XXH3 buffer. + * + * This is the optimal update size for incremental hashing. + * + * @see XXH3_64b_update(), XXH3_128b_update(). + */ +#define XXH3_INTERNALBUFFER_SIZE 256 + +/*! + * @brief Default size of the secret buffer (and @ref XXH3_kSecret). + * + * This is the size used in @ref XXH3_kSecret and the seeded functions. + * + * Not to be confused with @ref XXH3_SECRET_SIZE_MIN. + */ +#define XXH3_SECRET_DEFAULT_SIZE 192 + +/*! + * @internal + * @brief Structure for XXH3 streaming API. + * + * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY, + * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. Otherwise it is + * an opaque type. This allows fields to safely be changed. + * + * @note **This structure has a strict alignment requirement of 64 bytes.** Do + * not allocate this with `malloc()` or `new`, it will not be sufficiently + * aligned. Use @ref XXH3_createState() and @ref XXH3_freeState(), or stack + * allocation. + * + * Typedef'd to @ref XXH3_state_t. + * Do not access the members of this struct directly. + * + * @see XXH3_INITSTATE() for stack initialization. + * @see XXH3_createState(), XXH3_freeState(). + * @see XXH32_state_s, XXH64_state_s + */ +struct XXH3_state_s { + XXH_ALIGN_MEMBER(64, XXH64_hash_t acc[8]); + /*!< The 8 accumulators. Similar to `vN` in @ref XXH32_state_s::v1 and @ref XXH64_state_s */ + XXH_ALIGN_MEMBER(64, unsigned char customSecret[XXH3_SECRET_DEFAULT_SIZE]); + /*!< Used to store a custom secret generated from a seed. */ + XXH_ALIGN_MEMBER(64, unsigned char buffer[XXH3_INTERNALBUFFER_SIZE]); + /*!< The internal buffer. @see XXH32_state_s::mem32 */ + XXH32_hash_t bufferedSize; + /*!< The amount of memory in @ref buffer, @see XXH32_state_s::memsize */ + XXH32_hash_t reserved32; + /*!< Reserved field. Needed for padding on 64-bit. */ + size_t nbStripesSoFar; + /*!< Number or stripes processed. */ + XXH64_hash_t totalLen; + /*!< Total length hashed. 64-bit even on 32-bit targets. */ + size_t nbStripesPerBlock; + /*!< Number of stripes per block. */ + size_t secretLimit; + /*!< Size of @ref customSecret or @ref extSecret */ + XXH64_hash_t seed; + /*!< Seed for _withSeed variants. Must be zero otherwise, @see XXH3_INITSTATE() */ + XXH64_hash_t reserved64; + /*!< Reserved field. */ + const unsigned char* extSecret; + /*!< Reference to an external secret for the _withSecret variants, NULL + * for other variants. */ + /* note: there may be some padding at the end due to alignment on 64 bytes */ +}; /* typedef'd to XXH3_state_t */ + +#undef XXH_ALIGN_MEMBER + +/*! + * @brief Initializes a stack-allocated `XXH3_state_s`. + * + * When the @ref XXH3_state_t structure is merely emplaced on stack, + * it should be initialized with XXH3_INITSTATE() or a memset() + * in case its first reset uses XXH3_NNbits_reset_withSeed(). + * This init can be omitted if the first reset uses default or _withSecret mode. + * This operation isn't necessary when the state is created with XXH3_createState(). + * Note that this doesn't prepare the state for a streaming operation, + * it's still necessary to use XXH3_NNbits_reset*() afterwards. + */ +#define XXH3_INITSTATE(XXH3_state_ptr) { (XXH3_state_ptr)->seed = 0; } + + +/* === Experimental API === */ +/* Symbols defined below must be considered tied to a specific library version. */ + +/* + * XXH3_generateSecret(): + * + * Derive a high-entropy secret from any user-defined content, named customSeed. + * The generated secret can be used in combination with `*_withSecret()` functions. + * The `_withSecret()` variants are useful to provide a higher level of protection than 64-bit seed, + * as it becomes much more difficult for an external actor to guess how to impact the calculation logic. + * + * The function accepts as input a custom seed of any length and any content, + * and derives from it a high-entropy secret of length XXH3_SECRET_DEFAULT_SIZE + * into an already allocated buffer secretBuffer. + * The generated secret is _always_ XXH_SECRET_DEFAULT_SIZE bytes long. + * + * The generated secret can then be used with any `*_withSecret()` variant. + * Functions `XXH3_128bits_withSecret()`, `XXH3_64bits_withSecret()`, + * `XXH3_128bits_reset_withSecret()` and `XXH3_64bits_reset_withSecret()` + * are part of this list. They all accept a `secret` parameter + * which must be very long for implementation reasons (>= XXH3_SECRET_SIZE_MIN) + * _and_ feature very high entropy (consist of random-looking bytes). + * These conditions can be a high bar to meet, so + * this function can be used to generate a secret of proper quality. + * + * customSeed can be anything. It can have any size, even small ones, + * and its content can be anything, even stupidly "low entropy" source such as a bunch of zeroes. + * The resulting `secret` will nonetheless provide all expected qualities. + * + * Supplying NULL as the customSeed copies the default secret into `secretBuffer`. + * When customSeedSize > 0, supplying NULL as customSeed is undefined behavior. + */ +XXH_PUBLIC_API void XXH3_generateSecret(void* secretBuffer, const void* customSeed, size_t customSeedSize); + + +/* simple short-cut to pre-selected XXH3_128bits variant */ +XXH_PUBLIC_API XXH128_hash_t XXH128(const void* data, size_t len, XXH64_hash_t seed); + + +#endif /* XXH_NO_LONG_LONG */ +#if defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API) +# define XXH_IMPLEMENTATION +#endif + +#endif /* defined(XXH_STATIC_LINKING_ONLY) && !defined(XXHASH_H_STATIC_13879238742) */ + + +/* ======================================================================== */ +/* ======================================================================== */ +/* ======================================================================== */ + + +/*-********************************************************************** + * xxHash implementation + *-********************************************************************** + * xxHash's implementation used to be hosted inside xxhash.c. + * + * However, inlining requires implementation to be visible to the compiler, + * hence be included alongside the header. + * Previously, implementation was hosted inside xxhash.c, + * which was then #included when inlining was activated. + * This construction created issues with a few build and install systems, + * as it required xxhash.c to be stored in /include directory. + * + * xxHash implementation is now directly integrated within xxhash.h. + * As a consequence, xxhash.c is no longer needed in /include. + * + * xxhash.c is still available and is still useful. + * In a "normal" setup, when xxhash is not inlined, + * xxhash.h only exposes the prototypes and public symbols, + * while xxhash.c can be built into an object file xxhash.o + * which can then be linked into the final binary. + ************************************************************************/ + +#if ( defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API) \ + || defined(XXH_IMPLEMENTATION) ) && !defined(XXH_IMPLEM_13a8737387) +# define XXH_IMPLEM_13a8737387 + +/* ************************************* +* Tuning parameters +***************************************/ + +/*! + * @defgroup tuning Tuning parameters + * @{ + * + * Various macros to control xxHash's behavior. + */ +#ifdef XXH_DOXYGEN +/*! + * @brief Define this to disable 64-bit code. + * + * Useful if only using the @ref xxh32_family and you have a strict C90 compiler. + */ +# define XXH_NO_LONG_LONG +# undef XXH_NO_LONG_LONG /* don't actually */ +/*! + * @brief Controls how unaligned memory is accessed. + * + * By default, access to unaligned memory is controlled by `memcpy()`, which is + * safe and portable. + * + * Unfortunately, on some target/compiler combinations, the generated assembly + * is sub-optimal. + * + * The below switch allow selection of a different access method + * in the search for improved performance. + * + * @par Possible options: + * + * - `XXH_FORCE_MEMORY_ACCESS=0` (default): `memcpy` + * @par + * Use `memcpy()`. Safe and portable. Note that most modern compilers will + * eliminate the function call and treat it as an unaligned access. + * + * - `XXH_FORCE_MEMORY_ACCESS=1`: `__attribute__((packed))` + * @par + * Depends on compiler extensions and is therefore not portable. + * This method is safe if your compiler supports it, and *generally* as + * fast or faster than `memcpy`. + * + * - `XXH_FORCE_MEMORY_ACCESS=2`: Direct cast + * @par + * Casts directly and dereferences. This method doesn't depend on the + * compiler, but it violates the C standard as it directly dereferences an + * unaligned pointer. It can generate buggy code on targets which do not + * support unaligned memory accesses, but in some circumstances, it's the + * only known way to get the most performance (example: GCC + ARMv6). + * + * - `XXH_FORCE_MEMORY_ACCESS=3`: Byteshift + * @par + * Also portable. This can generate the best code on old compilers which don't + * inline small `memcpy()` calls, and it might also be faster on big-endian + * systems which lack a native byteswap instruction. However, some compilers + * will emit literal byteshifts even if the target supports unaligned access. + * + * . + * + * @warning + * Methods 1 and 2 rely on implementation-defined behavior. Use these with + * care, as what works on one compiler/platform/optimization level may cause + * another to read garbage data or even crash. + * + * See https://stackoverflow.com/a/32095106/646947 for details. + * + * Prefer these methods in priority order (0 > 3 > 1 > 2) + */ +# define XXH_FORCE_MEMORY_ACCESS 0 +/*! + * @def XXH_ACCEPT_NULL_INPUT_POINTER + * @brief Whether to add explicit `NULL` checks. + * + * If the input pointer is `NULL` and the length is non-zero, xxHash's default + * behavior is to dereference it, triggering a segfault. + * + * When this macro is enabled, xxHash actively checks the input for a null pointer. + * If it is, the result for null input pointers is the same as a zero-length input. + */ +# define XXH_ACCEPT_NULL_INPUT_POINTER 0 +/*! + * @def XXH_FORCE_ALIGN_CHECK + * @brief If defined to non-zero, adds a special path for aligned inputs (XXH32() + * and XXH64() only). + * + * This is an important performance trick for architectures without decent + * unaligned memory access performance. + * + * It checks for input alignment, and when conditions are met, uses a "fast + * path" employing direct 32-bit/64-bit reads, resulting in _dramatically + * faster_ read speed. + * + * The check costs one initial branch per hash, which is generally negligible, + * but not zero. + * + * Moreover, it's not useful to generate an additional code path if memory + * access uses the same instruction for both aligned and unaligned + * adresses (e.g. x86 and aarch64). + * + * In these cases, the alignment check can be removed by setting this macro to 0. + * Then the code will always use unaligned memory access. + * Align check is automatically disabled on x86, x64 & arm64, + * which are platforms known to offer good unaligned memory accesses performance. + * + * This option does not affect XXH3 (only XXH32 and XXH64). + */ +# define XXH_FORCE_ALIGN_CHECK 0 + +/*! + * @def XXH_NO_INLINE_HINTS + * @brief When non-zero, sets all functions to `static`. + * + * By default, xxHash tries to force the compiler to inline almost all internal + * functions. + * + * This can usually improve performance due to reduced jumping and improved + * constant folding, but significantly increases the size of the binary which + * might not be favorable. + * + * Additionally, sometimes the forced inlining can be detrimental to performance, + * depending on the architecture. + * + * XXH_NO_INLINE_HINTS marks all internal functions as static, giving the + * compiler full control on whether to inline or not. + * + * When not optimizing (-O0), optimizing for size (-Os, -Oz), or using + * -fno-inline with GCC or Clang, this will automatically be defined. + */ +# define XXH_NO_INLINE_HINTS 0 + +/*! + * @def XXH_REROLL + * @brief Whether to reroll `XXH32_finalize` and `XXH64_finalize`. + * + * For performance, `XXH32_finalize` and `XXH64_finalize` use an unrolled loop + * in the form of a switch statement. + * + * This is not always desirable, as it generates larger code, and depending on + * the architecture, may even be slower + * + * This is automatically defined with `-Os`/`-Oz` on GCC and Clang. + */ +# define XXH_REROLL 0 + +/*! + * @internal + * @brief Redefines old internal names. + * + * For compatibility with code that uses xxHash's internals before the names + * were changed to improve namespacing. There is no other reason to use this. + */ +# define XXH_OLD_NAMES +# undef XXH_OLD_NAMES /* don't actually use, it is ugly. */ +#endif /* XXH_DOXYGEN */ +/*! + * @} + */ + +#ifndef XXH_FORCE_MEMORY_ACCESS /* can be defined externally, on command line for example */ +# if !defined(__clang__) && defined(__GNUC__) && defined(__ARM_FEATURE_UNALIGNED) && defined(__ARM_ARCH) && (__ARM_ARCH == 6) +# define XXH_FORCE_MEMORY_ACCESS 2 +# elif !defined(__clang__) && ((defined(__INTEL_COMPILER) && !defined(_WIN32)) || \ + (defined(__GNUC__) && (defined(__ARM_ARCH) && __ARM_ARCH >= 7))) +# define XXH_FORCE_MEMORY_ACCESS 1 +# endif +#endif + +#ifndef XXH_ACCEPT_NULL_INPUT_POINTER /* can be defined externally */ +# define XXH_ACCEPT_NULL_INPUT_POINTER 0 +#endif + +#ifndef XXH_FORCE_ALIGN_CHECK /* can be defined externally */ +# if defined(__i386) || defined(__x86_64__) || defined(__aarch64__) \ + || defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM64) /* visual */ +# define XXH_FORCE_ALIGN_CHECK 0 +# else +# define XXH_FORCE_ALIGN_CHECK 1 +# endif +#endif + +#ifndef XXH_NO_INLINE_HINTS +# if defined(__OPTIMIZE_SIZE__) /* -Os, -Oz */ \ + || defined(__NO_INLINE__) /* -O0, -fno-inline */ +# define XXH_NO_INLINE_HINTS 1 +# else +# define XXH_NO_INLINE_HINTS 0 +# endif +#endif + +#ifndef XXH_REROLL +# if defined(__OPTIMIZE_SIZE__) +# define XXH_REROLL 1 +# else +# define XXH_REROLL 0 +# endif +#endif + +/*! + * @defgroup impl Implementation + * @{ + */ + + +/* ************************************* +* Includes & Memory related functions +***************************************/ +/* + * Modify the local functions below should you wish to use + * different memory routines for malloc() and free() + */ +#include + +/*! + * @internal + * @brief Modify this function to use a different routine than malloc(). + */ +static void* XXH_malloc(size_t s) { return malloc(s); } + +/*! + * @internal + * @brief Modify this function to use a different routine than free(). + */ +static void XXH_free(void* p) { free(p); } + +#include + +/*! + * @internal + * @brief Modify this function to use a different routine than memcpy(). + */ +static void* XXH_memcpy(void* dest, const void* src, size_t size) +{ + return memcpy(dest,src,size); +} + +#include /* ULLONG_MAX */ + + +/* ************************************* +* Compiler Specific Options +***************************************/ +#ifdef _MSC_VER /* Visual Studio warning fix */ +# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */ +#endif + +#if XXH_NO_INLINE_HINTS /* disable inlining hints */ +# if defined(__GNUC__) +# define XXH_FORCE_INLINE static __attribute__((unused)) +# else +# define XXH_FORCE_INLINE static +# endif +# define XXH_NO_INLINE static +/* enable inlining hints */ +#elif defined(_MSC_VER) /* Visual Studio */ +# define XXH_FORCE_INLINE static __forceinline +# define XXH_NO_INLINE static __declspec(noinline) +#elif defined(__GNUC__) +# define XXH_FORCE_INLINE static __inline__ __attribute__((always_inline, unused)) +# define XXH_NO_INLINE static __attribute__((noinline)) +#elif defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) /* C99 */ +# define XXH_FORCE_INLINE static inline +# define XXH_NO_INLINE static +#else +# define XXH_FORCE_INLINE static +# define XXH_NO_INLINE static +#endif + + + +/* ************************************* +* Debug +***************************************/ +/*! + * @ingroup tuning + * @def XXH_DEBUGLEVEL + * @brief Sets the debugging level. + * + * XXH_DEBUGLEVEL is expected to be defined externally, typically via the + * compiler's command line options. The value must be a number. + */ +#ifndef XXH_DEBUGLEVEL +# ifdef DEBUGLEVEL /* backwards compat */ +# define XXH_DEBUGLEVEL DEBUGLEVEL +# else +# define XXH_DEBUGLEVEL 0 +# endif +#endif + +#if (XXH_DEBUGLEVEL>=1) +# include /* note: can still be disabled with NDEBUG */ +# define XXH_ASSERT(c) assert(c) +#else +# define XXH_ASSERT(c) ((void)0) +#endif + +/* note: use after variable declarations */ +#define XXH_STATIC_ASSERT(c) do { enum { XXH_sa = 1/(int)(!!(c)) }; } while (0) + + +/* ************************************* +* Basic Types +***************************************/ +#if !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# include + typedef uint8_t xxh_u8; +#else + typedef unsigned char xxh_u8; +#endif +typedef XXH32_hash_t xxh_u32; + +#ifdef XXH_OLD_NAMES +# define BYTE xxh_u8 +# define U8 xxh_u8 +# define U32 xxh_u32 +#endif + +/* *** Memory access *** */ + +/*! + * @internal + * @fn xxh_u32 XXH_read32(const void* ptr) + * @brief Reads an unaligned 32-bit integer from @p ptr in native endianness. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * + * @param ptr The pointer to read from. + * @return The 32-bit native endian integer from the bytes at @p ptr. + */ + +/*! + * @internal + * @fn xxh_u32 XXH_readLE32(const void* ptr) + * @brief Reads an unaligned 32-bit little endian integer from @p ptr. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * + * @param ptr The pointer to read from. + * @return The 32-bit little endian integer from the bytes at @p ptr. + */ + +/*! + * @internal + * @fn xxh_u32 XXH_readBE32(const void* ptr) + * @brief Reads an unaligned 32-bit big endian integer from @p ptr. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * + * @param ptr The pointer to read from. + * @return The 32-bit big endian integer from the bytes at @p ptr. + */ + +/*! + * @internal + * @fn xxh_u32 XXH_readLE32_align(const void* ptr, XXH_alignment align) + * @brief Like @ref XXH_readLE32(), but has an option for aligned reads. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * Note that when @ref XXH_FORCE_ALIGN_CHECK == 0, the @p align parameter is + * always @ref XXH_alignment::XXH_unaligned. + * + * @param ptr The pointer to read from. + * @param align Whether @p ptr is aligned. + * @pre + * If @p align == @ref XXH_alignment::XXH_aligned, @p ptr must be 4 byte + * aligned. + * @return The 32-bit little endian integer from the bytes at @p ptr. + */ + +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) +/* + * Manual byteshift. Best for old compilers which don't inline memcpy. + * We actually directly use XXH_readLE32 and XXH_readBE32. + */ +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2)) + +/* + * Force direct memory access. Only works on CPU which support unaligned memory + * access in hardware. + */ +static xxh_u32 XXH_read32(const void* memPtr) { return *(const xxh_u32*) memPtr; } + +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1)) + +/* + * __pack instructions are safer but compiler specific, hence potentially + * problematic for some compilers. + * + * Currently only defined for GCC and ICC. + */ +#ifdef XXH_OLD_NAMES +typedef union { xxh_u32 u32; } __attribute__((packed)) unalign; +#endif +static xxh_u32 XXH_read32(const void* ptr) +{ + typedef union { xxh_u32 u32; } __attribute__((packed)) xxh_unalign; + return ((const xxh_unalign*)ptr)->u32; +} + +#else + +/* + * Portable and safe solution. Generally efficient. + * see: https://stackoverflow.com/a/32095106/646947 + */ +static xxh_u32 XXH_read32(const void* memPtr) +{ + xxh_u32 val; + memcpy(&val, memPtr, sizeof(val)); + return val; +} + +#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */ + + +/* *** Endianess *** */ +typedef enum { XXH_bigEndian=0, XXH_littleEndian=1 } XXH_endianess; + +/*! + * @ingroup tuning + * @def XXH_CPU_LITTLE_ENDIAN + * @brief Whether the target is little endian. + * + * Defined to 1 if the target is little endian, or 0 if it is big endian. + * It can be defined externally, for example on the compiler command line. + * + * If it is not defined, a runtime check (which is usually constant folded) + * is used instead. + * + * @note + * This is not necessarily defined to an integer constant. + * + * @see XXH_isLittleEndian() for the runtime check. + */ +#ifndef XXH_CPU_LITTLE_ENDIAN +/* + * Try to detect endianness automatically, to avoid the nonstandard behavior + * in `XXH_isLittleEndian()` + */ +# if defined(_WIN32) /* Windows is always little endian */ \ + || defined(__LITTLE_ENDIAN__) \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +# define XXH_CPU_LITTLE_ENDIAN 1 +# elif defined(__BIG_ENDIAN__) \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +# define XXH_CPU_LITTLE_ENDIAN 0 +# else +/*! + * @internal + * @brief Runtime check for @ref XXH_CPU_LITTLE_ENDIAN. + * + * Most compilers will constant fold this. + */ +static int XXH_isLittleEndian(void) +{ + /* + * Portable and well-defined behavior. + * Don't use static: it is detrimental to performance. + */ + const union { xxh_u32 u; xxh_u8 c[4]; } one = { 1 }; + return one.c[0]; +} +# define XXH_CPU_LITTLE_ENDIAN XXH_isLittleEndian() +# endif +#endif + + + + +/* **************************************** +* Compiler-specific Functions and Macros +******************************************/ +#define XXH_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__) + +#ifdef __has_builtin +# define XXH_HAS_BUILTIN(x) __has_builtin(x) +#else +# define XXH_HAS_BUILTIN(x) 0 +#endif + +/*! + * @internal + * @def XXH_rotl32(x,r) + * @brief 32-bit rotate left. + * + * @param x The 32-bit integer to be rotated. + * @param r The number of bits to rotate. + * @pre + * @p r > 0 && @p r < 32 + * @note + * @p x and @p r may be evaluated multiple times. + * @return The rotated result. + */ +#if !defined(NO_CLANG_BUILTIN) && XXH_HAS_BUILTIN(__builtin_rotateleft32) \ + && XXH_HAS_BUILTIN(__builtin_rotateleft64) +# define XXH_rotl32 __builtin_rotateleft32 +# define XXH_rotl64 __builtin_rotateleft64 +/* Note: although _rotl exists for minGW (GCC under windows), performance seems poor */ +#elif defined(_MSC_VER) +# define XXH_rotl32(x,r) _rotl(x,r) +# define XXH_rotl64(x,r) _rotl64(x,r) +#else +# define XXH_rotl32(x,r) (((x) << (r)) | ((x) >> (32 - (r)))) +# define XXH_rotl64(x,r) (((x) << (r)) | ((x) >> (64 - (r)))) +#endif + +/*! + * @internal + * @fn xxh_u32 XXH_swap32(xxh_u32 x) + * @brief A 32-bit byteswap. + * + * @param x The 32-bit integer to byteswap. + * @return @p x, byteswapped. + */ +#if defined(_MSC_VER) /* Visual Studio */ +# define XXH_swap32 _byteswap_ulong +#elif XXH_GCC_VERSION >= 403 +# define XXH_swap32 __builtin_bswap32 +#else +static xxh_u32 XXH_swap32 (xxh_u32 x) +{ + return ((x << 24) & 0xff000000 ) | + ((x << 8) & 0x00ff0000 ) | + ((x >> 8) & 0x0000ff00 ) | + ((x >> 24) & 0x000000ff ); +} +#endif + + +/* *************************** +* Memory reads +*****************************/ + +/*! + * @internal + * @brief Enum to indicate whether a pointer is aligned. + */ +typedef enum { + XXH_aligned, /*!< Aligned */ + XXH_unaligned /*!< Possibly unaligned */ +} XXH_alignment; + +/* + * XXH_FORCE_MEMORY_ACCESS==3 is an endian-independent byteshift load. + * + * This is ideal for older compilers which don't inline memcpy. + */ +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) + +XXH_FORCE_INLINE xxh_u32 XXH_readLE32(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[0] + | ((xxh_u32)bytePtr[1] << 8) + | ((xxh_u32)bytePtr[2] << 16) + | ((xxh_u32)bytePtr[3] << 24); +} + +XXH_FORCE_INLINE xxh_u32 XXH_readBE32(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[3] + | ((xxh_u32)bytePtr[2] << 8) + | ((xxh_u32)bytePtr[1] << 16) + | ((xxh_u32)bytePtr[0] << 24); +} + +#else +XXH_FORCE_INLINE xxh_u32 XXH_readLE32(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_read32(ptr) : XXH_swap32(XXH_read32(ptr)); +} + +static xxh_u32 XXH_readBE32(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_swap32(XXH_read32(ptr)) : XXH_read32(ptr); +} +#endif + +XXH_FORCE_INLINE xxh_u32 +XXH_readLE32_align(const void* ptr, XXH_alignment align) +{ + if (align==XXH_unaligned) { + return XXH_readLE32(ptr); + } else { + return XXH_CPU_LITTLE_ENDIAN ? *(const xxh_u32*)ptr : XXH_swap32(*(const xxh_u32*)ptr); + } +} + + +/* ************************************* +* Misc +***************************************/ +/*! @ingroup public */ +XXH_PUBLIC_API unsigned XXH_versionNumber (void) { return XXH_VERSION_NUMBER; } + + +/* ******************************************************************* +* 32-bit hash functions +*********************************************************************/ +/*! + * @} + * @defgroup xxh32_impl XXH32 implementation + * @ingroup impl + * @{ + */ +static const xxh_u32 XXH_PRIME32_1 = 0x9E3779B1U; /*!< 0b10011110001101110111100110110001 */ +static const xxh_u32 XXH_PRIME32_2 = 0x85EBCA77U; /*!< 0b10000101111010111100101001110111 */ +static const xxh_u32 XXH_PRIME32_3 = 0xC2B2AE3DU; /*!< 0b11000010101100101010111000111101 */ +static const xxh_u32 XXH_PRIME32_4 = 0x27D4EB2FU; /*!< 0b00100111110101001110101100101111 */ +static const xxh_u32 XXH_PRIME32_5 = 0x165667B1U; /*!< 0b00010110010101100110011110110001 */ + +#ifdef XXH_OLD_NAMES +# define PRIME32_1 XXH_PRIME32_1 +# define PRIME32_2 XXH_PRIME32_2 +# define PRIME32_3 XXH_PRIME32_3 +# define PRIME32_4 XXH_PRIME32_4 +# define PRIME32_5 XXH_PRIME32_5 +#endif + +/*! + * @internal + * @brief Normal stripe processing routine. + * + * This shuffles the bits so that any bit from @p input impacts several bits in + * @p acc. + * + * @param acc The accumulator lane. + * @param input The stripe of input to mix. + * @return The mixed accumulator lane. + */ +static xxh_u32 XXH32_round(xxh_u32 acc, xxh_u32 input) +{ + acc += input * XXH_PRIME32_2; + acc = XXH_rotl32(acc, 13); + acc *= XXH_PRIME32_1; +#if defined(__GNUC__) && defined(__SSE4_1__) && !defined(XXH_ENABLE_AUTOVECTORIZE) + /* + * UGLY HACK: + * This inline assembly hack forces acc into a normal register. This is the + * only thing that prevents GCC and Clang from autovectorizing the XXH32 + * loop (pragmas and attributes don't work for some resason) without globally + * disabling SSE4.1. + * + * The reason we want to avoid vectorization is because despite working on + * 4 integers at a time, there are multiple factors slowing XXH32 down on + * SSE4: + * - There's a ridiculous amount of lag from pmulld (10 cycles of latency on + * newer chips!) making it slightly slower to multiply four integers at + * once compared to four integers independently. Even when pmulld was + * fastest, Sandy/Ivy Bridge, it is still not worth it to go into SSE + * just to multiply unless doing a long operation. + * + * - Four instructions are required to rotate, + * movqda tmp, v // not required with VEX encoding + * pslld tmp, 13 // tmp <<= 13 + * psrld v, 19 // x >>= 19 + * por v, tmp // x |= tmp + * compared to one for scalar: + * roll v, 13 // reliably fast across the board + * shldl v, v, 13 // Sandy Bridge and later prefer this for some reason + * + * - Instruction level parallelism is actually more beneficial here because + * the SIMD actually serializes this operation: While v1 is rotating, v2 + * can load data, while v3 can multiply. SSE forces them to operate + * together. + * + * How this hack works: + * __asm__("" // Declare an assembly block but don't declare any instructions + * : // However, as an Input/Output Operand, + * "+r" // constrain a read/write operand (+) as a general purpose register (r). + * (acc) // and set acc as the operand + * ); + * + * Because of the 'r', the compiler has promised that seed will be in a + * general purpose register and the '+' says that it will be 'read/write', + * so it has to assume it has changed. It is like volatile without all the + * loads and stores. + * + * Since the argument has to be in a normal register (not an SSE register), + * each time XXH32_round is called, it is impossible to vectorize. + */ + __asm__("" : "+r" (acc)); +#endif + return acc; +} + +/*! + * @internal + * @brief Mixes all bits to finalize the hash. + * + * The final mix ensures that all input bits have a chance to impact any bit in + * the output digest, resulting in an unbiased distribution. + * + * @param h32 The hash to avalanche. + * @return The avalanched hash. + */ +static xxh_u32 XXH32_avalanche(xxh_u32 h32) +{ + h32 ^= h32 >> 15; + h32 *= XXH_PRIME32_2; + h32 ^= h32 >> 13; + h32 *= XXH_PRIME32_3; + h32 ^= h32 >> 16; + return(h32); +} + +#define XXH_get32bits(p) XXH_readLE32_align(p, align) + +/*! + * @internal + * @brief Processes the last 0-15 bytes of @p ptr. + * + * There may be up to 15 bytes remaining to consume from the input. + * This final stage will digest them to ensure that all input bytes are present + * in the final mix. + * + * @param h32 The hash to finalize. + * @param ptr The pointer to the remaining input. + * @param len The remaining length, modulo 16. + * @param align Whether @p ptr is aligned. + * @return The finalized hash. + */ +static xxh_u32 +XXH32_finalize(xxh_u32 h32, const xxh_u8* ptr, size_t len, XXH_alignment align) +{ +#define XXH_PROCESS1 do { \ + h32 += (*ptr++) * XXH_PRIME32_5; \ + h32 = XXH_rotl32(h32, 11) * XXH_PRIME32_1; \ +} while (0) + +#define XXH_PROCESS4 do { \ + h32 += XXH_get32bits(ptr) * XXH_PRIME32_3; \ + ptr += 4; \ + h32 = XXH_rotl32(h32, 17) * XXH_PRIME32_4; \ +} while (0) + + /* Compact rerolled version */ + if (XXH_REROLL) { + len &= 15; + while (len >= 4) { + XXH_PROCESS4; + len -= 4; + } + while (len > 0) { + XXH_PROCESS1; + --len; + } + return XXH32_avalanche(h32); + } else { + switch(len&15) /* or switch(bEnd - p) */ { + case 12: XXH_PROCESS4; + /* fallthrough */ + case 8: XXH_PROCESS4; + /* fallthrough */ + case 4: XXH_PROCESS4; + return XXH32_avalanche(h32); + + case 13: XXH_PROCESS4; + /* fallthrough */ + case 9: XXH_PROCESS4; + /* fallthrough */ + case 5: XXH_PROCESS4; + XXH_PROCESS1; + return XXH32_avalanche(h32); + + case 14: XXH_PROCESS4; + /* fallthrough */ + case 10: XXH_PROCESS4; + /* fallthrough */ + case 6: XXH_PROCESS4; + XXH_PROCESS1; + XXH_PROCESS1; + return XXH32_avalanche(h32); + + case 15: XXH_PROCESS4; + /* fallthrough */ + case 11: XXH_PROCESS4; + /* fallthrough */ + case 7: XXH_PROCESS4; + /* fallthrough */ + case 3: XXH_PROCESS1; + /* fallthrough */ + case 2: XXH_PROCESS1; + /* fallthrough */ + case 1: XXH_PROCESS1; + /* fallthrough */ + case 0: return XXH32_avalanche(h32); + } + XXH_ASSERT(0); + return h32; /* reaching this point is deemed impossible */ + } +} + +#ifdef XXH_OLD_NAMES +# define PROCESS1 XXH_PROCESS1 +# define PROCESS4 XXH_PROCESS4 +#else +# undef XXH_PROCESS1 +# undef XXH_PROCESS4 +#endif + +/*! + * @internal + * @brief The implementation for @ref XXH32(). + * + * @param input, len, seed Directly passed from @ref XXH32(). + * @param align Whether @p input is aligned. + * @return The calculated hash. + */ +XXH_FORCE_INLINE xxh_u32 +XXH32_endian_align(const xxh_u8* input, size_t len, xxh_u32 seed, XXH_alignment align) +{ + const xxh_u8* bEnd = input + len; + xxh_u32 h32; + +#if defined(XXH_ACCEPT_NULL_INPUT_POINTER) && (XXH_ACCEPT_NULL_INPUT_POINTER>=1) + if (input==NULL) { + len=0; + bEnd=input=(const xxh_u8*)(size_t)16; + } +#endif + + if (len>=16) { + const xxh_u8* const limit = bEnd - 15; + xxh_u32 v1 = seed + XXH_PRIME32_1 + XXH_PRIME32_2; + xxh_u32 v2 = seed + XXH_PRIME32_2; + xxh_u32 v3 = seed + 0; + xxh_u32 v4 = seed - XXH_PRIME32_1; + + do { + v1 = XXH32_round(v1, XXH_get32bits(input)); input += 4; + v2 = XXH32_round(v2, XXH_get32bits(input)); input += 4; + v3 = XXH32_round(v3, XXH_get32bits(input)); input += 4; + v4 = XXH32_round(v4, XXH_get32bits(input)); input += 4; + } while (input < limit); + + h32 = XXH_rotl32(v1, 1) + XXH_rotl32(v2, 7) + + XXH_rotl32(v3, 12) + XXH_rotl32(v4, 18); + } else { + h32 = seed + XXH_PRIME32_5; + } + + h32 += (xxh_u32)len; + + return XXH32_finalize(h32, input, len&15, align); +} + +/*! @ingroup xxh32_family */ +XXH_PUBLIC_API XXH32_hash_t XXH32 (const void* input, size_t len, XXH32_hash_t seed) +{ +#if 0 + /* Simple version, good for code maintenance, but unfortunately slow for small inputs */ + XXH32_state_t state; + XXH32_reset(&state, seed); + XXH32_update(&state, (const xxh_u8*)input, len); + return XXH32_digest(&state); +#else + if (XXH_FORCE_ALIGN_CHECK) { + if ((((size_t)input) & 3) == 0) { /* Input is 4-bytes aligned, leverage the speed benefit */ + return XXH32_endian_align((const xxh_u8*)input, len, seed, XXH_aligned); + } } + + return XXH32_endian_align((const xxh_u8*)input, len, seed, XXH_unaligned); +#endif +} + + + +/******* Hash streaming *******/ +/*! + * @ingroup xxh32_family + */ +XXH_PUBLIC_API XXH32_state_t* XXH32_createState(void) +{ + return (XXH32_state_t*)XXH_malloc(sizeof(XXH32_state_t)); +} +/*! @ingroup xxh32_family */ +XXH_PUBLIC_API XXH_errorcode XXH32_freeState(XXH32_state_t* statePtr) +{ + XXH_free(statePtr); + return XXH_OK; +} + +/*! @ingroup xxh32_family */ +XXH_PUBLIC_API void XXH32_copyState(XXH32_state_t* dstState, const XXH32_state_t* srcState) +{ + memcpy(dstState, srcState, sizeof(*dstState)); +} + +/*! @ingroup xxh32_family */ +XXH_PUBLIC_API XXH_errorcode XXH32_reset(XXH32_state_t* statePtr, XXH32_hash_t seed) +{ + XXH32_state_t state; /* using a local state to memcpy() in order to avoid strict-aliasing warnings */ + memset(&state, 0, sizeof(state)); + state.v1 = seed + XXH_PRIME32_1 + XXH_PRIME32_2; + state.v2 = seed + XXH_PRIME32_2; + state.v3 = seed + 0; + state.v4 = seed - XXH_PRIME32_1; + /* do not write into reserved, planned to be removed in a future version */ + memcpy(statePtr, &state, sizeof(state) - sizeof(state.reserved)); + return XXH_OK; +} + + +/*! @ingroup xxh32_family */ +XXH_PUBLIC_API XXH_errorcode +XXH32_update(XXH32_state_t* state, const void* input, size_t len) +{ + if (input==NULL) +#if defined(XXH_ACCEPT_NULL_INPUT_POINTER) && (XXH_ACCEPT_NULL_INPUT_POINTER>=1) + return XXH_OK; +#else + return XXH_ERROR; +#endif + + { const xxh_u8* p = (const xxh_u8*)input; + const xxh_u8* const bEnd = p + len; + + state->total_len_32 += (XXH32_hash_t)len; + state->large_len |= (XXH32_hash_t)((len>=16) | (state->total_len_32>=16)); + + if (state->memsize + len < 16) { /* fill in tmp buffer */ + XXH_memcpy((xxh_u8*)(state->mem32) + state->memsize, input, len); + state->memsize += (XXH32_hash_t)len; + return XXH_OK; + } + + if (state->memsize) { /* some data left from previous update */ + XXH_memcpy((xxh_u8*)(state->mem32) + state->memsize, input, 16-state->memsize); + { const xxh_u32* p32 = state->mem32; + state->v1 = XXH32_round(state->v1, XXH_readLE32(p32)); p32++; + state->v2 = XXH32_round(state->v2, XXH_readLE32(p32)); p32++; + state->v3 = XXH32_round(state->v3, XXH_readLE32(p32)); p32++; + state->v4 = XXH32_round(state->v4, XXH_readLE32(p32)); + } + p += 16-state->memsize; + state->memsize = 0; + } + + if (p <= bEnd-16) { + const xxh_u8* const limit = bEnd - 16; + xxh_u32 v1 = state->v1; + xxh_u32 v2 = state->v2; + xxh_u32 v3 = state->v3; + xxh_u32 v4 = state->v4; + + do { + v1 = XXH32_round(v1, XXH_readLE32(p)); p+=4; + v2 = XXH32_round(v2, XXH_readLE32(p)); p+=4; + v3 = XXH32_round(v3, XXH_readLE32(p)); p+=4; + v4 = XXH32_round(v4, XXH_readLE32(p)); p+=4; + } while (p<=limit); + + state->v1 = v1; + state->v2 = v2; + state->v3 = v3; + state->v4 = v4; + } + + if (p < bEnd) { + XXH_memcpy(state->mem32, p, (size_t)(bEnd-p)); + state->memsize = (unsigned)(bEnd-p); + } + } + + return XXH_OK; +} + + +/*! @ingroup xxh32_family */ +XXH_PUBLIC_API XXH32_hash_t XXH32_digest(const XXH32_state_t* state) +{ + xxh_u32 h32; + + if (state->large_len) { + h32 = XXH_rotl32(state->v1, 1) + + XXH_rotl32(state->v2, 7) + + XXH_rotl32(state->v3, 12) + + XXH_rotl32(state->v4, 18); + } else { + h32 = state->v3 /* == seed */ + XXH_PRIME32_5; + } + + h32 += state->total_len_32; + + return XXH32_finalize(h32, (const xxh_u8*)state->mem32, state->memsize, XXH_aligned); +} + + +/******* Canonical representation *******/ + +/*! + * @ingroup xxh32_family + * The default return values from XXH functions are unsigned 32 and 64 bit + * integers. + * + * The canonical representation uses big endian convention, the same convention + * as human-readable numbers (large digits first). + * + * This way, hash values can be written into a file or buffer, remaining + * comparable across different systems. + * + * The following functions allow transformation of hash values to and from their + * canonical format. + */ +XXH_PUBLIC_API void XXH32_canonicalFromHash(XXH32_canonical_t* dst, XXH32_hash_t hash) +{ + XXH_STATIC_ASSERT(sizeof(XXH32_canonical_t) == sizeof(XXH32_hash_t)); + if (XXH_CPU_LITTLE_ENDIAN) hash = XXH_swap32(hash); + memcpy(dst, &hash, sizeof(*dst)); +} +/*! @ingroup xxh32_family */ +XXH_PUBLIC_API XXH32_hash_t XXH32_hashFromCanonical(const XXH32_canonical_t* src) +{ + return XXH_readBE32(src); +} + + +#ifndef XXH_NO_LONG_LONG + +/* ******************************************************************* +* 64-bit hash functions +*********************************************************************/ +/*! + * @} + * @ingroup impl + * @{ + */ +/******* Memory access *******/ + +typedef XXH64_hash_t xxh_u64; + +#ifdef XXH_OLD_NAMES +# define U64 xxh_u64 +#endif + +/*! + * XXH_REROLL_XXH64: + * Whether to reroll the XXH64_finalize() loop. + * + * Just like XXH32, we can unroll the XXH64_finalize() loop. This can be a + * performance gain on 64-bit hosts, as only one jump is required. + * + * However, on 32-bit hosts, because arithmetic needs to be done with two 32-bit + * registers, and 64-bit arithmetic needs to be simulated, it isn't beneficial + * to unroll. The code becomes ridiculously large (the largest function in the + * binary on i386!), and rerolling it saves anywhere from 3kB to 20kB. It is + * also slightly faster because it fits into cache better and is more likely + * to be inlined by the compiler. + * + * If XXH_REROLL is defined, this is ignored and the loop is always rerolled. + */ +#ifndef XXH_REROLL_XXH64 +# if (defined(__ILP32__) || defined(_ILP32)) /* ILP32 is often defined on 32-bit GCC family */ \ + || !(defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64) /* x86-64 */ \ + || defined(_M_ARM64) || defined(__aarch64__) || defined(__arm64__) /* aarch64 */ \ + || defined(__PPC64__) || defined(__PPC64LE__) || defined(__ppc64__) || defined(__powerpc64__) /* ppc64 */ \ + || defined(__mips64__) || defined(__mips64)) /* mips64 */ \ + || (!defined(SIZE_MAX) || SIZE_MAX < ULLONG_MAX) /* check limits */ +# define XXH_REROLL_XXH64 1 +# else +# define XXH_REROLL_XXH64 0 +# endif +#endif /* !defined(XXH_REROLL_XXH64) */ + +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) +/* + * Manual byteshift. Best for old compilers which don't inline memcpy. + * We actually directly use XXH_readLE64 and XXH_readBE64. + */ +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2)) + +/* Force direct memory access. Only works on CPU which support unaligned memory access in hardware */ +static xxh_u64 XXH_read64(const void* memPtr) +{ + return *(const xxh_u64*) memPtr; +} + +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1)) + +/* + * __pack instructions are safer, but compiler specific, hence potentially + * problematic for some compilers. + * + * Currently only defined for GCC and ICC. + */ +#ifdef XXH_OLD_NAMES +typedef union { xxh_u32 u32; xxh_u64 u64; } __attribute__((packed)) unalign64; +#endif +static xxh_u64 XXH_read64(const void* ptr) +{ + typedef union { xxh_u32 u32; xxh_u64 u64; } __attribute__((packed)) xxh_unalign64; + return ((const xxh_unalign64*)ptr)->u64; +} + +#else + +/* + * Portable and safe solution. Generally efficient. + * see: https://stackoverflow.com/a/32095106/646947 + */ +static xxh_u64 XXH_read64(const void* memPtr) +{ + xxh_u64 val; + memcpy(&val, memPtr, sizeof(val)); + return val; +} + +#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */ + +#if defined(_MSC_VER) /* Visual Studio */ +# define XXH_swap64 _byteswap_uint64 +#elif XXH_GCC_VERSION >= 403 +# define XXH_swap64 __builtin_bswap64 +#else +static xxh_u64 XXH_swap64(xxh_u64 x) +{ + return ((x << 56) & 0xff00000000000000ULL) | + ((x << 40) & 0x00ff000000000000ULL) | + ((x << 24) & 0x0000ff0000000000ULL) | + ((x << 8) & 0x000000ff00000000ULL) | + ((x >> 8) & 0x00000000ff000000ULL) | + ((x >> 24) & 0x0000000000ff0000ULL) | + ((x >> 40) & 0x000000000000ff00ULL) | + ((x >> 56) & 0x00000000000000ffULL); +} +#endif + + +/* XXH_FORCE_MEMORY_ACCESS==3 is an endian-independent byteshift load. */ +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) + +XXH_FORCE_INLINE xxh_u64 XXH_readLE64(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[0] + | ((xxh_u64)bytePtr[1] << 8) + | ((xxh_u64)bytePtr[2] << 16) + | ((xxh_u64)bytePtr[3] << 24) + | ((xxh_u64)bytePtr[4] << 32) + | ((xxh_u64)bytePtr[5] << 40) + | ((xxh_u64)bytePtr[6] << 48) + | ((xxh_u64)bytePtr[7] << 56); +} + +XXH_FORCE_INLINE xxh_u64 XXH_readBE64(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[7] + | ((xxh_u64)bytePtr[6] << 8) + | ((xxh_u64)bytePtr[5] << 16) + | ((xxh_u64)bytePtr[4] << 24) + | ((xxh_u64)bytePtr[3] << 32) + | ((xxh_u64)bytePtr[2] << 40) + | ((xxh_u64)bytePtr[1] << 48) + | ((xxh_u64)bytePtr[0] << 56); +} + +#else +XXH_FORCE_INLINE xxh_u64 XXH_readLE64(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_read64(ptr) : XXH_swap64(XXH_read64(ptr)); +} + +static xxh_u64 XXH_readBE64(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_swap64(XXH_read64(ptr)) : XXH_read64(ptr); +} +#endif + +XXH_FORCE_INLINE xxh_u64 +XXH_readLE64_align(const void* ptr, XXH_alignment align) +{ + if (align==XXH_unaligned) + return XXH_readLE64(ptr); + else + return XXH_CPU_LITTLE_ENDIAN ? *(const xxh_u64*)ptr : XXH_swap64(*(const xxh_u64*)ptr); +} + + +/******* xxh64 *******/ +/*! + * @} + * @defgroup xxh64_impl XXH64 implementation + * @ingroup impl + * @{ + */ +static const xxh_u64 XXH_PRIME64_1 = 0x9E3779B185EBCA87ULL; /*!< 0b1001111000110111011110011011000110000101111010111100101010000111 */ +static const xxh_u64 XXH_PRIME64_2 = 0xC2B2AE3D27D4EB4FULL; /*!< 0b1100001010110010101011100011110100100111110101001110101101001111 */ +static const xxh_u64 XXH_PRIME64_3 = 0x165667B19E3779F9ULL; /*!< 0b0001011001010110011001111011000110011110001101110111100111111001 */ +static const xxh_u64 XXH_PRIME64_4 = 0x85EBCA77C2B2AE63ULL; /*!< 0b1000010111101011110010100111011111000010101100101010111001100011 */ +static const xxh_u64 XXH_PRIME64_5 = 0x27D4EB2F165667C5ULL; /*!< 0b0010011111010100111010110010111100010110010101100110011111000101 */ + +#ifdef XXH_OLD_NAMES +# define PRIME64_1 XXH_PRIME64_1 +# define PRIME64_2 XXH_PRIME64_2 +# define PRIME64_3 XXH_PRIME64_3 +# define PRIME64_4 XXH_PRIME64_4 +# define PRIME64_5 XXH_PRIME64_5 +#endif + +static xxh_u64 XXH64_round(xxh_u64 acc, xxh_u64 input) +{ + acc += input * XXH_PRIME64_2; + acc = XXH_rotl64(acc, 31); + acc *= XXH_PRIME64_1; + return acc; +} + +static xxh_u64 XXH64_mergeRound(xxh_u64 acc, xxh_u64 val) +{ + val = XXH64_round(0, val); + acc ^= val; + acc = acc * XXH_PRIME64_1 + XXH_PRIME64_4; + return acc; +} + +static xxh_u64 XXH64_avalanche(xxh_u64 h64) +{ + h64 ^= h64 >> 33; + h64 *= XXH_PRIME64_2; + h64 ^= h64 >> 29; + h64 *= XXH_PRIME64_3; + h64 ^= h64 >> 32; + return h64; +} + + +#define XXH_get64bits(p) XXH_readLE64_align(p, align) + +static xxh_u64 +XXH64_finalize(xxh_u64 h64, const xxh_u8* ptr, size_t len, XXH_alignment align) +{ +#define XXH_PROCESS1_64 do { \ + h64 ^= (*ptr++) * XXH_PRIME64_5; \ + h64 = XXH_rotl64(h64, 11) * XXH_PRIME64_1; \ +} while (0) + +#define XXH_PROCESS4_64 do { \ + h64 ^= (xxh_u64)(XXH_get32bits(ptr)) * XXH_PRIME64_1; \ + ptr += 4; \ + h64 = XXH_rotl64(h64, 23) * XXH_PRIME64_2 + XXH_PRIME64_3; \ +} while (0) + +#define XXH_PROCESS8_64 do { \ + xxh_u64 const k1 = XXH64_round(0, XXH_get64bits(ptr)); \ + ptr += 8; \ + h64 ^= k1; \ + h64 = XXH_rotl64(h64,27) * XXH_PRIME64_1 + XXH_PRIME64_4; \ +} while (0) + + /* Rerolled version for 32-bit targets is faster and much smaller. */ + if (XXH_REROLL || XXH_REROLL_XXH64) { + len &= 31; + while (len >= 8) { + XXH_PROCESS8_64; + len -= 8; + } + if (len >= 4) { + XXH_PROCESS4_64; + len -= 4; + } + while (len > 0) { + XXH_PROCESS1_64; + --len; + } + return XXH64_avalanche(h64); + } else { + switch(len & 31) { + case 24: XXH_PROCESS8_64; + /* fallthrough */ + case 16: XXH_PROCESS8_64; + /* fallthrough */ + case 8: XXH_PROCESS8_64; + return XXH64_avalanche(h64); + + case 28: XXH_PROCESS8_64; + /* fallthrough */ + case 20: XXH_PROCESS8_64; + /* fallthrough */ + case 12: XXH_PROCESS8_64; + /* fallthrough */ + case 4: XXH_PROCESS4_64; + return XXH64_avalanche(h64); + + case 25: XXH_PROCESS8_64; + /* fallthrough */ + case 17: XXH_PROCESS8_64; + /* fallthrough */ + case 9: XXH_PROCESS8_64; + XXH_PROCESS1_64; + return XXH64_avalanche(h64); + + case 29: XXH_PROCESS8_64; + /* fallthrough */ + case 21: XXH_PROCESS8_64; + /* fallthrough */ + case 13: XXH_PROCESS8_64; + /* fallthrough */ + case 5: XXH_PROCESS4_64; + XXH_PROCESS1_64; + return XXH64_avalanche(h64); + + case 26: XXH_PROCESS8_64; + /* fallthrough */ + case 18: XXH_PROCESS8_64; + /* fallthrough */ + case 10: XXH_PROCESS8_64; + XXH_PROCESS1_64; + XXH_PROCESS1_64; + return XXH64_avalanche(h64); + + case 30: XXH_PROCESS8_64; + /* fallthrough */ + case 22: XXH_PROCESS8_64; + /* fallthrough */ + case 14: XXH_PROCESS8_64; + /* fallthrough */ + case 6: XXH_PROCESS4_64; + XXH_PROCESS1_64; + XXH_PROCESS1_64; + return XXH64_avalanche(h64); + + case 27: XXH_PROCESS8_64; + /* fallthrough */ + case 19: XXH_PROCESS8_64; + /* fallthrough */ + case 11: XXH_PROCESS8_64; + XXH_PROCESS1_64; + XXH_PROCESS1_64; + XXH_PROCESS1_64; + return XXH64_avalanche(h64); + + case 31: XXH_PROCESS8_64; + /* fallthrough */ + case 23: XXH_PROCESS8_64; + /* fallthrough */ + case 15: XXH_PROCESS8_64; + /* fallthrough */ + case 7: XXH_PROCESS4_64; + /* fallthrough */ + case 3: XXH_PROCESS1_64; + /* fallthrough */ + case 2: XXH_PROCESS1_64; + /* fallthrough */ + case 1: XXH_PROCESS1_64; + /* fallthrough */ + case 0: return XXH64_avalanche(h64); + } + } + /* impossible to reach */ + XXH_ASSERT(0); + return 0; /* unreachable, but some compilers complain without it */ +} + +#ifdef XXH_OLD_NAMES +# define PROCESS1_64 XXH_PROCESS1_64 +# define PROCESS4_64 XXH_PROCESS4_64 +# define PROCESS8_64 XXH_PROCESS8_64 +#else +# undef XXH_PROCESS1_64 +# undef XXH_PROCESS4_64 +# undef XXH_PROCESS8_64 +#endif + +XXH_FORCE_INLINE xxh_u64 +XXH64_endian_align(const xxh_u8* input, size_t len, xxh_u64 seed, XXH_alignment align) +{ + const xxh_u8* bEnd = input + len; + xxh_u64 h64; + +#if defined(XXH_ACCEPT_NULL_INPUT_POINTER) && (XXH_ACCEPT_NULL_INPUT_POINTER>=1) + if (input==NULL) { + len=0; + bEnd=input=(const xxh_u8*)(size_t)32; + } +#endif + + if (len>=32) { + const xxh_u8* const limit = bEnd - 32; + xxh_u64 v1 = seed + XXH_PRIME64_1 + XXH_PRIME64_2; + xxh_u64 v2 = seed + XXH_PRIME64_2; + xxh_u64 v3 = seed + 0; + xxh_u64 v4 = seed - XXH_PRIME64_1; + + do { + v1 = XXH64_round(v1, XXH_get64bits(input)); input+=8; + v2 = XXH64_round(v2, XXH_get64bits(input)); input+=8; + v3 = XXH64_round(v3, XXH_get64bits(input)); input+=8; + v4 = XXH64_round(v4, XXH_get64bits(input)); input+=8; + } while (input<=limit); + + h64 = XXH_rotl64(v1, 1) + XXH_rotl64(v2, 7) + XXH_rotl64(v3, 12) + XXH_rotl64(v4, 18); + h64 = XXH64_mergeRound(h64, v1); + h64 = XXH64_mergeRound(h64, v2); + h64 = XXH64_mergeRound(h64, v3); + h64 = XXH64_mergeRound(h64, v4); + + } else { + h64 = seed + XXH_PRIME64_5; + } + + h64 += (xxh_u64) len; + + return XXH64_finalize(h64, input, len, align); +} + + +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API XXH64_hash_t XXH64 (const void* input, size_t len, XXH64_hash_t seed) +{ +#if 0 + /* Simple version, good for code maintenance, but unfortunately slow for small inputs */ + XXH64_state_t state; + XXH64_reset(&state, seed); + XXH64_update(&state, (const xxh_u8*)input, len); + return XXH64_digest(&state); +#else + if (XXH_FORCE_ALIGN_CHECK) { + if ((((size_t)input) & 7)==0) { /* Input is aligned, let's leverage the speed advantage */ + return XXH64_endian_align((const xxh_u8*)input, len, seed, XXH_aligned); + } } + + return XXH64_endian_align((const xxh_u8*)input, len, seed, XXH_unaligned); + +#endif +} + +/******* Hash Streaming *******/ + +/*! @ingroup xxh64_family*/ +XXH_PUBLIC_API XXH64_state_t* XXH64_createState(void) +{ + return (XXH64_state_t*)XXH_malloc(sizeof(XXH64_state_t)); +} +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr) +{ + XXH_free(statePtr); + return XXH_OK; +} + +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API void XXH64_copyState(XXH64_state_t* dstState, const XXH64_state_t* srcState) +{ + memcpy(dstState, srcState, sizeof(*dstState)); +} + +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API XXH_errorcode XXH64_reset(XXH64_state_t* statePtr, XXH64_hash_t seed) +{ + XXH64_state_t state; /* use a local state to memcpy() in order to avoid strict-aliasing warnings */ + memset(&state, 0, sizeof(state)); + state.v1 = seed + XXH_PRIME64_1 + XXH_PRIME64_2; + state.v2 = seed + XXH_PRIME64_2; + state.v3 = seed + 0; + state.v4 = seed - XXH_PRIME64_1; + /* do not write into reserved64, might be removed in a future version */ + memcpy(statePtr, &state, sizeof(state) - sizeof(state.reserved64)); + return XXH_OK; +} + +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API XXH_errorcode +XXH64_update (XXH64_state_t* state, const void* input, size_t len) +{ + if (input==NULL) +#if defined(XXH_ACCEPT_NULL_INPUT_POINTER) && (XXH_ACCEPT_NULL_INPUT_POINTER>=1) + return XXH_OK; +#else + return XXH_ERROR; +#endif + + { const xxh_u8* p = (const xxh_u8*)input; + const xxh_u8* const bEnd = p + len; + + state->total_len += len; + + if (state->memsize + len < 32) { /* fill in tmp buffer */ + XXH_memcpy(((xxh_u8*)state->mem64) + state->memsize, input, len); + state->memsize += (xxh_u32)len; + return XXH_OK; + } + + if (state->memsize) { /* tmp buffer is full */ + XXH_memcpy(((xxh_u8*)state->mem64) + state->memsize, input, 32-state->memsize); + state->v1 = XXH64_round(state->v1, XXH_readLE64(state->mem64+0)); + state->v2 = XXH64_round(state->v2, XXH_readLE64(state->mem64+1)); + state->v3 = XXH64_round(state->v3, XXH_readLE64(state->mem64+2)); + state->v4 = XXH64_round(state->v4, XXH_readLE64(state->mem64+3)); + p += 32 - state->memsize; + state->memsize = 0; + } + + if (p+32 <= bEnd) { + const xxh_u8* const limit = bEnd - 32; + xxh_u64 v1 = state->v1; + xxh_u64 v2 = state->v2; + xxh_u64 v3 = state->v3; + xxh_u64 v4 = state->v4; + + do { + v1 = XXH64_round(v1, XXH_readLE64(p)); p+=8; + v2 = XXH64_round(v2, XXH_readLE64(p)); p+=8; + v3 = XXH64_round(v3, XXH_readLE64(p)); p+=8; + v4 = XXH64_round(v4, XXH_readLE64(p)); p+=8; + } while (p<=limit); + + state->v1 = v1; + state->v2 = v2; + state->v3 = v3; + state->v4 = v4; + } + + if (p < bEnd) { + XXH_memcpy(state->mem64, p, (size_t)(bEnd-p)); + state->memsize = (unsigned)(bEnd-p); + } + } + + return XXH_OK; +} + + +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API XXH64_hash_t XXH64_digest(const XXH64_state_t* state) +{ + xxh_u64 h64; + + if (state->total_len >= 32) { + xxh_u64 const v1 = state->v1; + xxh_u64 const v2 = state->v2; + xxh_u64 const v3 = state->v3; + xxh_u64 const v4 = state->v4; + + h64 = XXH_rotl64(v1, 1) + XXH_rotl64(v2, 7) + XXH_rotl64(v3, 12) + XXH_rotl64(v4, 18); + h64 = XXH64_mergeRound(h64, v1); + h64 = XXH64_mergeRound(h64, v2); + h64 = XXH64_mergeRound(h64, v3); + h64 = XXH64_mergeRound(h64, v4); + } else { + h64 = state->v3 /*seed*/ + XXH_PRIME64_5; + } + + h64 += (xxh_u64) state->total_len; + + return XXH64_finalize(h64, (const xxh_u8*)state->mem64, (size_t)state->total_len, XXH_aligned); +} + + +/******* Canonical representation *******/ + +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash) +{ + XXH_STATIC_ASSERT(sizeof(XXH64_canonical_t) == sizeof(XXH64_hash_t)); + if (XXH_CPU_LITTLE_ENDIAN) hash = XXH_swap64(hash); + memcpy(dst, &hash, sizeof(*dst)); +} + +/*! @ingroup xxh64_family */ +XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src) +{ + return XXH_readBE64(src); +} + + + +/* ********************************************************************* +* XXH3 +* New generation hash designed for speed on small keys and vectorization +************************************************************************ */ +/*! + * @} + * @defgroup xxh3_impl XXH3 implementation + * @ingroup impl + * @{ + */ + +/* === Compiler specifics === */ + +#if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* >= C99 */ +# define XXH_RESTRICT restrict +#else +/* Note: it might be useful to define __restrict or __restrict__ for some C++ compilers */ +# define XXH_RESTRICT /* disable */ +#endif + +#if (defined(__GNUC__) && (__GNUC__ >= 3)) \ + || (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 800)) \ + || defined(__clang__) +# define XXH_likely(x) __builtin_expect(x, 1) +# define XXH_unlikely(x) __builtin_expect(x, 0) +#else +# define XXH_likely(x) (x) +# define XXH_unlikely(x) (x) +#endif + +#if defined(__GNUC__) +# if defined(__AVX2__) +# include +# elif defined(__SSE2__) +# include +# elif defined(__ARM_NEON__) || defined(__ARM_NEON) +# define inline __inline__ /* circumvent a clang bug */ +# include +# undef inline +# endif +#elif defined(_MSC_VER) +# include +#endif + +/* + * One goal of XXH3 is to make it fast on both 32-bit and 64-bit, while + * remaining a true 64-bit/128-bit hash function. + * + * This is done by prioritizing a subset of 64-bit operations that can be + * emulated without too many steps on the average 32-bit machine. + * + * For example, these two lines seem similar, and run equally fast on 64-bit: + * + * xxh_u64 x; + * x ^= (x >> 47); // good + * x ^= (x >> 13); // bad + * + * However, to a 32-bit machine, there is a major difference. + * + * x ^= (x >> 47) looks like this: + * + * x.lo ^= (x.hi >> (47 - 32)); + * + * while x ^= (x >> 13) looks like this: + * + * // note: funnel shifts are not usually cheap. + * x.lo ^= (x.lo >> 13) | (x.hi << (32 - 13)); + * x.hi ^= (x.hi >> 13); + * + * The first one is significantly faster than the second, simply because the + * shift is larger than 32. This means: + * - All the bits we need are in the upper 32 bits, so we can ignore the lower + * 32 bits in the shift. + * - The shift result will always fit in the lower 32 bits, and therefore, + * we can ignore the upper 32 bits in the xor. + * + * Thanks to this optimization, XXH3 only requires these features to be efficient: + * + * - Usable unaligned access + * - A 32-bit or 64-bit ALU + * - If 32-bit, a decent ADC instruction + * - A 32 or 64-bit multiply with a 64-bit result + * - For the 128-bit variant, a decent byteswap helps short inputs. + * + * The first two are already required by XXH32, and almost all 32-bit and 64-bit + * platforms which can run XXH32 can run XXH3 efficiently. + * + * Thumb-1, the classic 16-bit only subset of ARM's instruction set, is one + * notable exception. + * + * First of all, Thumb-1 lacks support for the UMULL instruction which + * performs the important long multiply. This means numerous __aeabi_lmul + * calls. + * + * Second of all, the 8 functional registers are just not enough. + * Setup for __aeabi_lmul, byteshift loads, pointers, and all arithmetic need + * Lo registers, and this shuffling results in thousands more MOVs than A32. + * + * A32 and T32 don't have this limitation. They can access all 14 registers, + * do a 32->64 multiply with UMULL, and the flexible operand allowing free + * shifts is helpful, too. + * + * Therefore, we do a quick sanity check. + * + * If compiling Thumb-1 for a target which supports ARM instructions, we will + * emit a warning, as it is not a "sane" platform to compile for. + * + * Usually, if this happens, it is because of an accident and you probably need + * to specify -march, as you likely meant to compile for a newer architecture. + * + * Credit: large sections of the vectorial and asm source code paths + * have been contributed by @easyaspi314 + */ +#if defined(__thumb__) && !defined(__thumb2__) && defined(__ARM_ARCH_ISA_ARM) +# warning "XXH3 is highly inefficient without ARM or Thumb-2." +#endif + +/* ========================================== + * Vectorization detection + * ========================================== */ + +#ifdef XXH_DOXYGEN +/*! + * @ingroup tuning + * @brief Overrides the vectorization implementation chosen for XXH3. + * + * Can be defined to 0 to disable SIMD or any of the values mentioned in + * @ref XXH_VECTOR_TYPE. + * + * If this is not defined, it uses predefined macros to determine the best + * implementation. + */ +# define XXH_VECTOR XXH_SCALAR +/*! + * @ingroup tuning + * @brief Possible values for @ref XXH_VECTOR. + * + * Note that these are actually implemented as macros. + * + * If this is not defined, it is detected automatically. + * @ref XXH_X86DISPATCH overrides this. + */ +enum XXH_VECTOR_TYPE /* fake enum */ { + XXH_SCALAR = 0, /*!< Portable scalar version */ + XXH_SSE2 = 1, /*!< + * SSE2 for Pentium 4, Opteron, all x86_64. + * + * @note SSE2 is also guaranteed on Windows 10, macOS, and + * Android x86. + */ + XXH_AVX2 = 2, /*!< AVX2 for Haswell and Bulldozer */ + XXH_AVX512 = 3, /*!< AVX512 for Skylake and Icelake */ + XXH_NEON = 4, /*!< NEON for most ARMv7-A and all AArch64 */ + XXH_VSX = 5, /*!< VSX and ZVector for POWER8/z13 (64-bit) */ +}; +/*! + * @ingroup tuning + * @brief Selects the minumum alignment for XXH3's accumulators. + * + * When using SIMD, this should match the alignment reqired for said vector + * type, so, for example, 32 for AVX2. + * + * Default: Auto detected. + */ +# define XXH_ACC_ALIGN 8 +#endif + +/* Actual definition */ +#ifndef XXH_DOXYGEN +# define XXH_SCALAR 0 +# define XXH_SSE2 1 +# define XXH_AVX2 2 +# define XXH_AVX512 3 +# define XXH_NEON 4 +# define XXH_VSX 5 +#endif + +#ifndef XXH_VECTOR /* can be defined on command line */ +# if defined(__AVX512F__) +# define XXH_VECTOR XXH_AVX512 +# elif defined(__AVX2__) +# define XXH_VECTOR XXH_AVX2 +# elif defined(__SSE2__) || defined(_M_AMD64) || defined(_M_X64) || (defined(_M_IX86_FP) && (_M_IX86_FP == 2)) +# define XXH_VECTOR XXH_SSE2 +# elif defined(__GNUC__) /* msvc support maybe later */ \ + && (defined(__ARM_NEON__) || defined(__ARM_NEON)) \ + && (defined(__LITTLE_ENDIAN__) /* We only support little endian NEON */ \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) +# define XXH_VECTOR XXH_NEON +# elif (defined(__PPC64__) && defined(__POWER8_VECTOR__)) \ + || (defined(__s390x__) && defined(__VEC__)) \ + && defined(__GNUC__) /* TODO: IBM XL */ +# define XXH_VECTOR XXH_VSX +# else +# define XXH_VECTOR XXH_SCALAR +# endif +#endif + +/* + * Controls the alignment of the accumulator, + * for compatibility with aligned vector loads, which are usually faster. + */ +#ifndef XXH_ACC_ALIGN +# if defined(XXH_X86DISPATCH) +# define XXH_ACC_ALIGN 64 /* for compatibility with avx512 */ +# elif XXH_VECTOR == XXH_SCALAR /* scalar */ +# define XXH_ACC_ALIGN 8 +# elif XXH_VECTOR == XXH_SSE2 /* sse2 */ +# define XXH_ACC_ALIGN 16 +# elif XXH_VECTOR == XXH_AVX2 /* avx2 */ +# define XXH_ACC_ALIGN 32 +# elif XXH_VECTOR == XXH_NEON /* neon */ +# define XXH_ACC_ALIGN 16 +# elif XXH_VECTOR == XXH_VSX /* vsx */ +# define XXH_ACC_ALIGN 16 +# elif XXH_VECTOR == XXH_AVX512 /* avx512 */ +# define XXH_ACC_ALIGN 64 +# endif +#endif + +#if defined(XXH_X86DISPATCH) || XXH_VECTOR == XXH_SSE2 \ + || XXH_VECTOR == XXH_AVX2 || XXH_VECTOR == XXH_AVX512 +# define XXH_SEC_ALIGN XXH_ACC_ALIGN +#else +# define XXH_SEC_ALIGN 8 +#endif + +/* + * UGLY HACK: + * GCC usually generates the best code with -O3 for xxHash. + * + * However, when targeting AVX2, it is overzealous in its unrolling resulting + * in code roughly 3/4 the speed of Clang. + * + * There are other issues, such as GCC splitting _mm256_loadu_si256 into + * _mm_loadu_si128 + _mm256_inserti128_si256. This is an optimization which + * only applies to Sandy and Ivy Bridge... which don't even support AVX2. + * + * That is why when compiling the AVX2 version, it is recommended to use either + * -O2 -mavx2 -march=haswell + * or + * -O2 -mavx2 -mno-avx256-split-unaligned-load + * for decent performance, or to use Clang instead. + * + * Fortunately, we can control the first one with a pragma that forces GCC into + * -O2, but the other one we can't control without "failed to inline always + * inline function due to target mismatch" warnings. + */ +#if XXH_VECTOR == XXH_AVX2 /* AVX2 */ \ + && defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \ + && defined(__OPTIMIZE__) && !defined(__OPTIMIZE_SIZE__) /* respect -O0 and -Os */ +# pragma GCC push_options +# pragma GCC optimize("-O2") +#endif + + +#if XXH_VECTOR == XXH_NEON +/* + * NEON's setup for vmlal_u32 is a little more complicated than it is on + * SSE2, AVX2, and VSX. + * + * While PMULUDQ and VMULEUW both perform a mask, VMLAL.U32 performs an upcast. + * + * To do the same operation, the 128-bit 'Q' register needs to be split into + * two 64-bit 'D' registers, performing this operation:: + * + * [ a | b ] + * | '---------. .--------' | + * | x | + * | .---------' '--------. | + * [ a & 0xFFFFFFFF | b & 0xFFFFFFFF ],[ a >> 32 | b >> 32 ] + * + * Due to significant changes in aarch64, the fastest method for aarch64 is + * completely different than the fastest method for ARMv7-A. + * + * ARMv7-A treats D registers as unions overlaying Q registers, so modifying + * D11 will modify the high half of Q5. This is similar to how modifying AH + * will only affect bits 8-15 of AX on x86. + * + * VZIP takes two registers, and puts even lanes in one register and odd lanes + * in the other. + * + * On ARMv7-A, this strangely modifies both parameters in place instead of + * taking the usual 3-operand form. + * + * Therefore, if we want to do this, we can simply use a D-form VZIP.32 on the + * lower and upper halves of the Q register to end up with the high and low + * halves where we want - all in one instruction. + * + * vzip.32 d10, d11 @ d10 = { d10[0], d11[0] }; d11 = { d10[1], d11[1] } + * + * Unfortunately we need inline assembly for this: Instructions modifying two + * registers at once is not possible in GCC or Clang's IR, and they have to + * create a copy. + * + * aarch64 requires a different approach. + * + * In order to make it easier to write a decent compiler for aarch64, many + * quirks were removed, such as conditional execution. + * + * NEON was also affected by this. + * + * aarch64 cannot access the high bits of a Q-form register, and writes to a + * D-form register zero the high bits, similar to how writes to W-form scalar + * registers (or DWORD registers on x86_64) work. + * + * The formerly free vget_high intrinsics now require a vext (with a few + * exceptions) + * + * Additionally, VZIP was replaced by ZIP1 and ZIP2, which are the equivalent + * of PUNPCKL* and PUNPCKH* in SSE, respectively, in order to only modify one + * operand. + * + * The equivalent of the VZIP.32 on the lower and upper halves would be this + * mess: + * + * ext v2.4s, v0.4s, v0.4s, #2 // v2 = { v0[2], v0[3], v0[0], v0[1] } + * zip1 v1.2s, v0.2s, v2.2s // v1 = { v0[0], v2[0] } + * zip2 v0.2s, v0.2s, v1.2s // v0 = { v0[1], v2[1] } + * + * Instead, we use a literal downcast, vmovn_u64 (XTN), and vshrn_n_u64 (SHRN): + * + * shrn v1.2s, v0.2d, #32 // v1 = (uint32x2_t)(v0 >> 32); + * xtn v0.2s, v0.2d // v0 = (uint32x2_t)(v0 & 0xFFFFFFFF); + * + * This is available on ARMv7-A, but is less efficient than a single VZIP.32. + */ + +/*! + * Function-like macro: + * void XXH_SPLIT_IN_PLACE(uint64x2_t &in, uint32x2_t &outLo, uint32x2_t &outHi) + * { + * outLo = (uint32x2_t)(in & 0xFFFFFFFF); + * outHi = (uint32x2_t)(in >> 32); + * in = UNDEFINED; + * } + */ +# if !defined(XXH_NO_VZIP_HACK) /* define to disable */ \ + && defined(__GNUC__) \ + && !defined(__aarch64__) && !defined(__arm64__) +# define XXH_SPLIT_IN_PLACE(in, outLo, outHi) \ + do { \ + /* Undocumented GCC/Clang operand modifier: %e0 = lower D half, %f0 = upper D half */ \ + /* https://github.com/gcc-mirror/gcc/blob/38cf91e5/gcc/config/arm/arm.c#L22486 */ \ + /* https://github.com/llvm-mirror/llvm/blob/2c4ca683/lib/Target/ARM/ARMAsmPrinter.cpp#L399 */ \ + __asm__("vzip.32 %e0, %f0" : "+w" (in)); \ + (outLo) = vget_low_u32 (vreinterpretq_u32_u64(in)); \ + (outHi) = vget_high_u32(vreinterpretq_u32_u64(in)); \ + } while (0) +# else +# define XXH_SPLIT_IN_PLACE(in, outLo, outHi) \ + do { \ + (outLo) = vmovn_u64 (in); \ + (outHi) = vshrn_n_u64 ((in), 32); \ + } while (0) +# endif +#endif /* XXH_VECTOR == XXH_NEON */ + +/* + * VSX and Z Vector helpers. + * + * This is very messy, and any pull requests to clean this up are welcome. + * + * There are a lot of problems with supporting VSX and s390x, due to + * inconsistent intrinsics, spotty coverage, and multiple endiannesses. + */ +#if XXH_VECTOR == XXH_VSX +# if defined(__s390x__) +# include +# else +/* gcc's altivec.h can have the unwanted consequence to unconditionally + * #define bool, vector, and pixel keywords, + * with bad consequences for programs already using these keywords for other purposes. + * The paragraph defining these macros is skipped when __APPLE_ALTIVEC__ is defined. + * __APPLE_ALTIVEC__ is _generally_ defined automatically by the compiler, + * but it seems that, in some cases, it isn't. + * Force the build macro to be defined, so that keywords are not altered. + */ +# if defined(__GNUC__) && !defined(__APPLE_ALTIVEC__) +# define __APPLE_ALTIVEC__ +# endif +# include +# endif + +typedef __vector unsigned long long xxh_u64x2; +typedef __vector unsigned char xxh_u8x16; +typedef __vector unsigned xxh_u32x4; + +# ifndef XXH_VSX_BE +# if defined(__BIG_ENDIAN__) \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +# define XXH_VSX_BE 1 +# elif defined(__VEC_ELEMENT_REG_ORDER__) && __VEC_ELEMENT_REG_ORDER__ == __ORDER_BIG_ENDIAN__ +# warning "-maltivec=be is not recommended. Please use native endianness." +# define XXH_VSX_BE 1 +# else +# define XXH_VSX_BE 0 +# endif +# endif /* !defined(XXH_VSX_BE) */ + +# if XXH_VSX_BE +# if defined(__POWER9_VECTOR__) || (defined(__clang__) && defined(__s390x__)) +# define XXH_vec_revb vec_revb +# else +/*! + * A polyfill for POWER9's vec_revb(). + */ +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_revb(xxh_u64x2 val) +{ + xxh_u8x16 const vByteSwap = { 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, + 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08 }; + return vec_perm(val, val, vByteSwap); +} +# endif +# endif /* XXH_VSX_BE */ + +/*! + * Performs an unaligned vector load and byte swaps it on big endian. + */ +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_loadu(const void *ptr) +{ + xxh_u64x2 ret; + memcpy(&ret, ptr, sizeof(xxh_u64x2)); +# if XXH_VSX_BE + ret = XXH_vec_revb(ret); +# endif + return ret; +} + +/* + * vec_mulo and vec_mule are very problematic intrinsics on PowerPC + * + * These intrinsics weren't added until GCC 8, despite existing for a while, + * and they are endian dependent. Also, their meaning swap depending on version. + * */ +# if defined(__s390x__) + /* s390x is always big endian, no issue on this platform */ +# define XXH_vec_mulo vec_mulo +# define XXH_vec_mule vec_mule +# elif defined(__clang__) && XXH_HAS_BUILTIN(__builtin_altivec_vmuleuw) +/* Clang has a better way to control this, we can just use the builtin which doesn't swap. */ +# define XXH_vec_mulo __builtin_altivec_vmulouw +# define XXH_vec_mule __builtin_altivec_vmuleuw +# else +/* gcc needs inline assembly */ +/* Adapted from https://github.com/google/highwayhash/blob/master/highwayhash/hh_vsx.h. */ +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_mulo(xxh_u32x4 a, xxh_u32x4 b) +{ + xxh_u64x2 result; + __asm__("vmulouw %0, %1, %2" : "=v" (result) : "v" (a), "v" (b)); + return result; +} +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_mule(xxh_u32x4 a, xxh_u32x4 b) +{ + xxh_u64x2 result; + __asm__("vmuleuw %0, %1, %2" : "=v" (result) : "v" (a), "v" (b)); + return result; +} +# endif /* XXH_vec_mulo, XXH_vec_mule */ +#endif /* XXH_VECTOR == XXH_VSX */ + + +/* prefetch + * can be disabled, by declaring XXH_NO_PREFETCH build macro */ +#if defined(XXH_NO_PREFETCH) +# define XXH_PREFETCH(ptr) (void)(ptr) /* disabled */ +#else +# if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) /* _mm_prefetch() not defined outside of x86/x64 */ +# include /* https://msdn.microsoft.com/fr-fr/library/84szxsww(v=vs.90).aspx */ +# define XXH_PREFETCH(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T0) +# elif defined(__GNUC__) && ( (__GNUC__ >= 4) || ( (__GNUC__ == 3) && (__GNUC_MINOR__ >= 1) ) ) +# define XXH_PREFETCH(ptr) __builtin_prefetch((ptr), 0 /* rw==read */, 3 /* locality */) +# else +# define XXH_PREFETCH(ptr) (void)(ptr) /* disabled */ +# endif +#endif /* XXH_NO_PREFETCH */ + + +/* ========================================== + * XXH3 default settings + * ========================================== */ + +#define XXH_SECRET_DEFAULT_SIZE 192 /* minimum XXH3_SECRET_SIZE_MIN */ + +#if (XXH_SECRET_DEFAULT_SIZE < XXH3_SECRET_SIZE_MIN) +# error "default keyset is not large enough" +#endif + +/*! Pseudorandom secret taken directly from FARSH. */ +XXH_ALIGN(64) static const xxh_u8 XXH3_kSecret[XXH_SECRET_DEFAULT_SIZE] = { + 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c, + 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f, + 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21, + 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6, 0x81, 0x3a, 0x26, 0x4c, + 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3, + 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8, + 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7, 0xc7, 0x0b, 0x4f, 0x1d, + 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64, + 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb, + 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26, 0x29, 0xd4, 0x68, 0x9e, + 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce, + 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e, +}; + + +#ifdef XXH_OLD_NAMES +# define kSecret XXH3_kSecret +#endif + +#ifdef XXH_DOXYGEN +/*! + * @brief Calculates a 32-bit to 64-bit long multiply. + * + * Implemented as a macro. + * + * Wraps `__emulu` on MSVC x86 because it tends to call `__allmul` when it doesn't + * need to (but it shouldn't need to anyways, it is about 7 instructions to do + * a 64x64 multiply...). Since we know that this will _always_ emit `MULL`, we + * use that instead of the normal method. + * + * If you are compiling for platforms like Thumb-1 and don't have a better option, + * you may also want to write your own long multiply routine here. + * + * @param x, y Numbers to be multiplied + * @return 64-bit product of the low 32 bits of @p x and @p y. + */ +XXH_FORCE_INLINE xxh_u64 +XXH_mult32to64(xxh_u64 x, xxh_u64 y) +{ + return (x & 0xFFFFFFFF) * (y & 0xFFFFFFFF); +} +#elif defined(_MSC_VER) && defined(_M_IX86) +# include +# define XXH_mult32to64(x, y) __emulu((unsigned)(x), (unsigned)(y)) +#else +/* + * Downcast + upcast is usually better than masking on older compilers like + * GCC 4.2 (especially 32-bit ones), all without affecting newer compilers. + * + * The other method, (x & 0xFFFFFFFF) * (y & 0xFFFFFFFF), will AND both operands + * and perform a full 64x64 multiply -- entirely redundant on 32-bit. + */ +# define XXH_mult32to64(x, y) ((xxh_u64)(xxh_u32)(x) * (xxh_u64)(xxh_u32)(y)) +#endif + +/*! + * @brief Calculates a 64->128-bit long multiply. + * + * Uses `__uint128_t` and `_umul128` if available, otherwise uses a scalar + * version. + * + * @param lhs, rhs The 64-bit integers to be multiplied + * @return The 128-bit result represented in an @ref XXH128_hash_t. + */ +static XXH128_hash_t +XXH_mult64to128(xxh_u64 lhs, xxh_u64 rhs) +{ + /* + * GCC/Clang __uint128_t method. + * + * On most 64-bit targets, GCC and Clang define a __uint128_t type. + * This is usually the best way as it usually uses a native long 64-bit + * multiply, such as MULQ on x86_64 or MUL + UMULH on aarch64. + * + * Usually. + * + * Despite being a 32-bit platform, Clang (and emscripten) define this type + * despite not having the arithmetic for it. This results in a laggy + * compiler builtin call which calculates a full 128-bit multiply. + * In that case it is best to use the portable one. + * https://github.com/Cyan4973/xxHash/issues/211#issuecomment-515575677 + */ +#if defined(__GNUC__) && !defined(__wasm__) \ + && defined(__SIZEOF_INT128__) \ + || (defined(_INTEGRAL_MAX_BITS) && _INTEGRAL_MAX_BITS >= 128) + + __uint128_t const product = (__uint128_t)lhs * (__uint128_t)rhs; + XXH128_hash_t r128; + r128.low64 = (xxh_u64)(product); + r128.high64 = (xxh_u64)(product >> 64); + return r128; + + /* + * MSVC for x64's _umul128 method. + * + * xxh_u64 _umul128(xxh_u64 Multiplier, xxh_u64 Multiplicand, xxh_u64 *HighProduct); + * + * This compiles to single operand MUL on x64. + */ +#elif defined(_M_X64) || defined(_M_IA64) + +#ifndef _MSC_VER +# pragma intrinsic(_umul128) +#endif + xxh_u64 product_high; + xxh_u64 const product_low = _umul128(lhs, rhs, &product_high); + XXH128_hash_t r128; + r128.low64 = product_low; + r128.high64 = product_high; + return r128; + +#else + /* + * Portable scalar method. Optimized for 32-bit and 64-bit ALUs. + * + * This is a fast and simple grade school multiply, which is shown below + * with base 10 arithmetic instead of base 0x100000000. + * + * 9 3 // D2 lhs = 93 + * x 7 5 // D2 rhs = 75 + * ---------- + * 1 5 // D2 lo_lo = (93 % 10) * (75 % 10) = 15 + * 4 5 | // D2 hi_lo = (93 / 10) * (75 % 10) = 45 + * 2 1 | // D2 lo_hi = (93 % 10) * (75 / 10) = 21 + * + 6 3 | | // D2 hi_hi = (93 / 10) * (75 / 10) = 63 + * --------- + * 2 7 | // D2 cross = (15 / 10) + (45 % 10) + 21 = 27 + * + 6 7 | | // D2 upper = (27 / 10) + (45 / 10) + 63 = 67 + * --------- + * 6 9 7 5 // D4 res = (27 * 10) + (15 % 10) + (67 * 100) = 6975 + * + * The reasons for adding the products like this are: + * 1. It avoids manual carry tracking. Just like how + * (9 * 9) + 9 + 9 = 99, the same applies with this for UINT64_MAX. + * This avoids a lot of complexity. + * + * 2. It hints for, and on Clang, compiles to, the powerful UMAAL + * instruction available in ARM's Digital Signal Processing extension + * in 32-bit ARMv6 and later, which is shown below: + * + * void UMAAL(xxh_u32 *RdLo, xxh_u32 *RdHi, xxh_u32 Rn, xxh_u32 Rm) + * { + * xxh_u64 product = (xxh_u64)*RdLo * (xxh_u64)*RdHi + Rn + Rm; + * *RdLo = (xxh_u32)(product & 0xFFFFFFFF); + * *RdHi = (xxh_u32)(product >> 32); + * } + * + * This instruction was designed for efficient long multiplication, and + * allows this to be calculated in only 4 instructions at speeds + * comparable to some 64-bit ALUs. + * + * 3. It isn't terrible on other platforms. Usually this will be a couple + * of 32-bit ADD/ADCs. + */ + + /* First calculate all of the cross products. */ + xxh_u64 const lo_lo = XXH_mult32to64(lhs & 0xFFFFFFFF, rhs & 0xFFFFFFFF); + xxh_u64 const hi_lo = XXH_mult32to64(lhs >> 32, rhs & 0xFFFFFFFF); + xxh_u64 const lo_hi = XXH_mult32to64(lhs & 0xFFFFFFFF, rhs >> 32); + xxh_u64 const hi_hi = XXH_mult32to64(lhs >> 32, rhs >> 32); + + /* Now add the products together. These will never overflow. */ + xxh_u64 const cross = (lo_lo >> 32) + (hi_lo & 0xFFFFFFFF) + lo_hi; + xxh_u64 const upper = (hi_lo >> 32) + (cross >> 32) + hi_hi; + xxh_u64 const lower = (cross << 32) | (lo_lo & 0xFFFFFFFF); + + XXH128_hash_t r128; + r128.low64 = lower; + r128.high64 = upper; + return r128; +#endif +} + +/*! + * @brief Calculates a 64-bit to 128-bit multiply, then XOR folds it. + * + * The reason for the separate function is to prevent passing too many structs + * around by value. This will hopefully inline the multiply, but we don't force it. + * + * @param lhs, rhs The 64-bit integers to multiply + * @return The low 64 bits of the product XOR'd by the high 64 bits. + * @see XXH_mult64to128() + */ +static xxh_u64 +XXH3_mul128_fold64(xxh_u64 lhs, xxh_u64 rhs) +{ + XXH128_hash_t product = XXH_mult64to128(lhs, rhs); + return product.low64 ^ product.high64; +} + +/*! Seems to produce slightly better code on GCC for some reason. */ +XXH_FORCE_INLINE xxh_u64 XXH_xorshift64(xxh_u64 v64, int shift) +{ + XXH_ASSERT(0 <= shift && shift < 64); + return v64 ^ (v64 >> shift); +} + +/* + * This is a fast avalanche stage, + * suitable when input bits are already partially mixed + */ +static XXH64_hash_t XXH3_avalanche(xxh_u64 h64) +{ + h64 = XXH_xorshift64(h64, 37); + h64 *= 0x165667919E3779F9ULL; + h64 = XXH_xorshift64(h64, 32); + return h64; +} + +/* + * This is a stronger avalanche, + * inspired by Pelle Evensen's rrmxmx + * preferable when input has not been previously mixed + */ +static XXH64_hash_t XXH3_rrmxmx(xxh_u64 h64, xxh_u64 len) +{ + /* this mix is inspired by Pelle Evensen's rrmxmx */ + h64 ^= XXH_rotl64(h64, 49) ^ XXH_rotl64(h64, 24); + h64 *= 0x9FB21C651E98DF25ULL; + h64 ^= (h64 >> 35) + len ; + h64 *= 0x9FB21C651E98DF25ULL; + return XXH_xorshift64(h64, 28); +} + + +/* ========================================== + * Short keys + * ========================================== + * One of the shortcomings of XXH32 and XXH64 was that their performance was + * sub-optimal on short lengths. It used an iterative algorithm which strongly + * favored lengths that were a multiple of 4 or 8. + * + * Instead of iterating over individual inputs, we use a set of single shot + * functions which piece together a range of lengths and operate in constant time. + * + * Additionally, the number of multiplies has been significantly reduced. This + * reduces latency, especially when emulating 64-bit multiplies on 32-bit. + * + * Depending on the platform, this may or may not be faster than XXH32, but it + * is almost guaranteed to be faster than XXH64. + */ + +/* + * At very short lengths, there isn't enough input to fully hide secrets, or use + * the entire secret. + * + * There is also only a limited amount of mixing we can do before significantly + * impacting performance. + * + * Therefore, we use different sections of the secret and always mix two secret + * samples with an XOR. This should have no effect on performance on the + * seedless or withSeed variants because everything _should_ be constant folded + * by modern compilers. + * + * The XOR mixing hides individual parts of the secret and increases entropy. + * + * This adds an extra layer of strength for custom secrets. + */ +XXH_FORCE_INLINE XXH64_hash_t +XXH3_len_1to3_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(1 <= len && len <= 3); + XXH_ASSERT(secret != NULL); + /* + * len = 1: combined = { input[0], 0x01, input[0], input[0] } + * len = 2: combined = { input[1], 0x02, input[0], input[1] } + * len = 3: combined = { input[2], 0x03, input[0], input[1] } + */ + { xxh_u8 const c1 = input[0]; + xxh_u8 const c2 = input[len >> 1]; + xxh_u8 const c3 = input[len - 1]; + xxh_u32 const combined = ((xxh_u32)c1 << 16) | ((xxh_u32)c2 << 24) + | ((xxh_u32)c3 << 0) | ((xxh_u32)len << 8); + xxh_u64 const bitflip = (XXH_readLE32(secret) ^ XXH_readLE32(secret+4)) + seed; + xxh_u64 const keyed = (xxh_u64)combined ^ bitflip; + return XXH64_avalanche(keyed); + } +} + +XXH_FORCE_INLINE XXH64_hash_t +XXH3_len_4to8_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(4 <= len && len < 8); + seed ^= (xxh_u64)XXH_swap32((xxh_u32)seed) << 32; + { xxh_u32 const input1 = XXH_readLE32(input); + xxh_u32 const input2 = XXH_readLE32(input + len - 4); + xxh_u64 const bitflip = (XXH_readLE64(secret+8) ^ XXH_readLE64(secret+16)) - seed; + xxh_u64 const input64 = input2 + (((xxh_u64)input1) << 32); + xxh_u64 const keyed = input64 ^ bitflip; + return XXH3_rrmxmx(keyed, len); + } +} + +XXH_FORCE_INLINE XXH64_hash_t +XXH3_len_9to16_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(8 <= len && len <= 16); + { xxh_u64 const bitflip1 = (XXH_readLE64(secret+24) ^ XXH_readLE64(secret+32)) + seed; + xxh_u64 const bitflip2 = (XXH_readLE64(secret+40) ^ XXH_readLE64(secret+48)) - seed; + xxh_u64 const input_lo = XXH_readLE64(input) ^ bitflip1; + xxh_u64 const input_hi = XXH_readLE64(input + len - 8) ^ bitflip2; + xxh_u64 const acc = len + + XXH_swap64(input_lo) + input_hi + + XXH3_mul128_fold64(input_lo, input_hi); + return XXH3_avalanche(acc); + } +} + +XXH_FORCE_INLINE XXH64_hash_t +XXH3_len_0to16_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(len <= 16); + { if (XXH_likely(len > 8)) return XXH3_len_9to16_64b(input, len, secret, seed); + if (XXH_likely(len >= 4)) return XXH3_len_4to8_64b(input, len, secret, seed); + if (len) return XXH3_len_1to3_64b(input, len, secret, seed); + return XXH64_avalanche(seed ^ (XXH_readLE64(secret+56) ^ XXH_readLE64(secret+64))); + } +} + +/* + * DISCLAIMER: There are known *seed-dependent* multicollisions here due to + * multiplication by zero, affecting hashes of lengths 17 to 240. + * + * However, they are very unlikely. + * + * Keep this in mind when using the unseeded XXH3_64bits() variant: As with all + * unseeded non-cryptographic hashes, it does not attempt to defend itself + * against specially crafted inputs, only random inputs. + * + * Compared to classic UMAC where a 1 in 2^31 chance of 4 consecutive bytes + * cancelling out the secret is taken an arbitrary number of times (addressed + * in XXH3_accumulate_512), this collision is very unlikely with random inputs + * and/or proper seeding: + * + * This only has a 1 in 2^63 chance of 8 consecutive bytes cancelling out, in a + * function that is only called up to 16 times per hash with up to 240 bytes of + * input. + * + * This is not too bad for a non-cryptographic hash function, especially with + * only 64 bit outputs. + * + * The 128-bit variant (which trades some speed for strength) is NOT affected + * by this, although it is always a good idea to use a proper seed if you care + * about strength. + */ +XXH_FORCE_INLINE xxh_u64 XXH3_mix16B(const xxh_u8* XXH_RESTRICT input, + const xxh_u8* XXH_RESTRICT secret, xxh_u64 seed64) +{ +#if defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \ + && defined(__i386__) && defined(__SSE2__) /* x86 + SSE2 */ \ + && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable like XXH32 hack */ + /* + * UGLY HACK: + * GCC for x86 tends to autovectorize the 128-bit multiply, resulting in + * slower code. + * + * By forcing seed64 into a register, we disrupt the cost model and + * cause it to scalarize. See `XXH32_round()` + * + * FIXME: Clang's output is still _much_ faster -- On an AMD Ryzen 3600, + * XXH3_64bits @ len=240 runs at 4.6 GB/s with Clang 9, but 3.3 GB/s on + * GCC 9.2, despite both emitting scalar code. + * + * GCC generates much better scalar code than Clang for the rest of XXH3, + * which is why finding a more optimal codepath is an interest. + */ + __asm__ ("" : "+r" (seed64)); +#endif + { xxh_u64 const input_lo = XXH_readLE64(input); + xxh_u64 const input_hi = XXH_readLE64(input+8); + return XXH3_mul128_fold64( + input_lo ^ (XXH_readLE64(secret) + seed64), + input_hi ^ (XXH_readLE64(secret+8) - seed64) + ); + } +} + +/* For mid range keys, XXH3 uses a Mum-hash variant. */ +XXH_FORCE_INLINE XXH64_hash_t +XXH3_len_17to128_64b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(16 < len && len <= 128); + + { xxh_u64 acc = len * XXH_PRIME64_1; + if (len > 32) { + if (len > 64) { + if (len > 96) { + acc += XXH3_mix16B(input+48, secret+96, seed); + acc += XXH3_mix16B(input+len-64, secret+112, seed); + } + acc += XXH3_mix16B(input+32, secret+64, seed); + acc += XXH3_mix16B(input+len-48, secret+80, seed); + } + acc += XXH3_mix16B(input+16, secret+32, seed); + acc += XXH3_mix16B(input+len-32, secret+48, seed); + } + acc += XXH3_mix16B(input+0, secret+0, seed); + acc += XXH3_mix16B(input+len-16, secret+16, seed); + + return XXH3_avalanche(acc); + } +} + +#define XXH3_MIDSIZE_MAX 240 + +XXH_NO_INLINE XXH64_hash_t +XXH3_len_129to240_64b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(128 < len && len <= XXH3_MIDSIZE_MAX); + + #define XXH3_MIDSIZE_STARTOFFSET 3 + #define XXH3_MIDSIZE_LASTOFFSET 17 + + { xxh_u64 acc = len * XXH_PRIME64_1; + int const nbRounds = (int)len / 16; + int i; + for (i=0; i<8; i++) { + acc += XXH3_mix16B(input+(16*i), secret+(16*i), seed); + } + acc = XXH3_avalanche(acc); + XXH_ASSERT(nbRounds >= 8); +#if defined(__clang__) /* Clang */ \ + && (defined(__ARM_NEON) || defined(__ARM_NEON__)) /* NEON */ \ + && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable */ + /* + * UGLY HACK: + * Clang for ARMv7-A tries to vectorize this loop, similar to GCC x86. + * In everywhere else, it uses scalar code. + * + * For 64->128-bit multiplies, even if the NEON was 100% optimal, it + * would still be slower than UMAAL (see XXH_mult64to128). + * + * Unfortunately, Clang doesn't handle the long multiplies properly and + * converts them to the nonexistent "vmulq_u64" intrinsic, which is then + * scalarized into an ugly mess of VMOV.32 instructions. + * + * This mess is difficult to avoid without turning autovectorization + * off completely, but they are usually relatively minor and/or not + * worth it to fix. + * + * This loop is the easiest to fix, as unlike XXH32, this pragma + * _actually works_ because it is a loop vectorization instead of an + * SLP vectorization. + */ + #pragma clang loop vectorize(disable) +#endif + for (i=8 ; i < nbRounds; i++) { + acc += XXH3_mix16B(input+(16*i), secret+(16*(i-8)) + XXH3_MIDSIZE_STARTOFFSET, seed); + } + /* last bytes */ + acc += XXH3_mix16B(input + len - 16, secret + XXH3_SECRET_SIZE_MIN - XXH3_MIDSIZE_LASTOFFSET, seed); + return XXH3_avalanche(acc); + } +} + + +/* ======= Long Keys ======= */ + +#define XXH_STRIPE_LEN 64 +#define XXH_SECRET_CONSUME_RATE 8 /* nb of secret bytes consumed at each accumulation */ +#define XXH_ACC_NB (XXH_STRIPE_LEN / sizeof(xxh_u64)) + +#ifdef XXH_OLD_NAMES +# define STRIPE_LEN XXH_STRIPE_LEN +# define ACC_NB XXH_ACC_NB +#endif + +XXH_FORCE_INLINE void XXH_writeLE64(void* dst, xxh_u64 v64) +{ + if (!XXH_CPU_LITTLE_ENDIAN) v64 = XXH_swap64(v64); + memcpy(dst, &v64, sizeof(v64)); +} + +/* Several intrinsic functions below are supposed to accept __int64 as argument, + * as documented in https://software.intel.com/sites/landingpage/IntrinsicsGuide/ . + * However, several environments do not define __int64 type, + * requiring a workaround. + */ +#if !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) + typedef int64_t xxh_i64; +#else + /* the following type must have a width of 64-bit */ + typedef long long xxh_i64; +#endif + +/* + * XXH3_accumulate_512 is the tightest loop for long inputs, and it is the most optimized. + * + * It is a hardened version of UMAC, based off of FARSH's implementation. + * + * This was chosen because it adapts quite well to 32-bit, 64-bit, and SIMD + * implementations, and it is ridiculously fast. + * + * We harden it by mixing the original input to the accumulators as well as the product. + * + * This means that in the (relatively likely) case of a multiply by zero, the + * original input is preserved. + * + * On 128-bit inputs, we swap 64-bit pairs when we add the input to improve + * cross-pollination, as otherwise the upper and lower halves would be + * essentially independent. + * + * This doesn't matter on 64-bit hashes since they all get merged together in + * the end, so we skip the extra step. + * + * Both XXH3_64bits and XXH3_128bits use this subroutine. + */ + +#if (XXH_VECTOR == XXH_AVX512) || defined(XXH_X86DISPATCH) + +#ifndef XXH_TARGET_AVX512 +# define XXH_TARGET_AVX512 /* disable attribute target */ +#endif + +XXH_FORCE_INLINE XXH_TARGET_AVX512 void +XXH3_accumulate_512_avx512(void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + XXH_ALIGN(64) __m512i* const xacc = (__m512i *) acc; + XXH_ASSERT((((size_t)acc) & 63) == 0); + XXH_STATIC_ASSERT(XXH_STRIPE_LEN == sizeof(__m512i)); + + { + /* data_vec = input[0]; */ + __m512i const data_vec = _mm512_loadu_si512 (input); + /* key_vec = secret[0]; */ + __m512i const key_vec = _mm512_loadu_si512 (secret); + /* data_key = data_vec ^ key_vec; */ + __m512i const data_key = _mm512_xor_si512 (data_vec, key_vec); + /* data_key_lo = data_key >> 32; */ + __m512i const data_key_lo = _mm512_shuffle_epi32 (data_key, (_MM_PERM_ENUM)_MM_SHUFFLE(0, 3, 0, 1)); + /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */ + __m512i const product = _mm512_mul_epu32 (data_key, data_key_lo); + /* xacc[0] += swap(data_vec); */ + __m512i const data_swap = _mm512_shuffle_epi32(data_vec, (_MM_PERM_ENUM)_MM_SHUFFLE(1, 0, 3, 2)); + __m512i const sum = _mm512_add_epi64(*xacc, data_swap); + /* xacc[0] += product; */ + *xacc = _mm512_add_epi64(product, sum); + } +} + +/* + * XXH3_scrambleAcc: Scrambles the accumulators to improve mixing. + * + * Multiplication isn't perfect, as explained by Google in HighwayHash: + * + * // Multiplication mixes/scrambles bytes 0-7 of the 64-bit result to + * // varying degrees. In descending order of goodness, bytes + * // 3 4 2 5 1 6 0 7 have quality 228 224 164 160 100 96 36 32. + * // As expected, the upper and lower bytes are much worse. + * + * Source: https://github.com/google/highwayhash/blob/0aaf66b/highwayhash/hh_avx2.h#L291 + * + * Since our algorithm uses a pseudorandom secret to add some variance into the + * mix, we don't need to (or want to) mix as often or as much as HighwayHash does. + * + * This isn't as tight as XXH3_accumulate, but still written in SIMD to avoid + * extraction. + * + * Both XXH3_64bits and XXH3_128bits use this subroutine. + */ + +XXH_FORCE_INLINE XXH_TARGET_AVX512 void +XXH3_scrambleAcc_avx512(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 63) == 0); + XXH_STATIC_ASSERT(XXH_STRIPE_LEN == sizeof(__m512i)); + { XXH_ALIGN(64) __m512i* const xacc = (__m512i*) acc; + const __m512i prime32 = _mm512_set1_epi32((int)XXH_PRIME32_1); + + /* xacc[0] ^= (xacc[0] >> 47) */ + __m512i const acc_vec = *xacc; + __m512i const shifted = _mm512_srli_epi64 (acc_vec, 47); + __m512i const data_vec = _mm512_xor_si512 (acc_vec, shifted); + /* xacc[0] ^= secret; */ + __m512i const key_vec = _mm512_loadu_si512 (secret); + __m512i const data_key = _mm512_xor_si512 (data_vec, key_vec); + + /* xacc[0] *= XXH_PRIME32_1; */ + __m512i const data_key_hi = _mm512_shuffle_epi32 (data_key, (_MM_PERM_ENUM)_MM_SHUFFLE(0, 3, 0, 1)); + __m512i const prod_lo = _mm512_mul_epu32 (data_key, prime32); + __m512i const prod_hi = _mm512_mul_epu32 (data_key_hi, prime32); + *xacc = _mm512_add_epi64(prod_lo, _mm512_slli_epi64(prod_hi, 32)); + } +} + +XXH_FORCE_INLINE XXH_TARGET_AVX512 void +XXH3_initCustomSecret_avx512(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 63) == 0); + XXH_STATIC_ASSERT(XXH_SEC_ALIGN == 64); + XXH_ASSERT(((size_t)customSecret & 63) == 0); + (void)(&XXH_writeLE64); + { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / sizeof(__m512i); + __m512i const seed = _mm512_mask_set1_epi64(_mm512_set1_epi64((xxh_i64)seed64), 0xAA, -(xxh_i64)seed64); + + XXH_ALIGN(64) const __m512i* const src = (const __m512i*) XXH3_kSecret; + XXH_ALIGN(64) __m512i* const dest = ( __m512i*) customSecret; + int i; + for (i=0; i < nbRounds; ++i) { + /* GCC has a bug, _mm512_stream_load_si512 accepts 'void*', not 'void const*', + * this will warn "discards ‘const’ qualifier". */ + union { + XXH_ALIGN(64) const __m512i* cp; + XXH_ALIGN(64) void* p; + } remote_const_void; + remote_const_void.cp = src + i; + dest[i] = _mm512_add_epi64(_mm512_stream_load_si512(remote_const_void.p), seed); + } } +} + +#endif + +#if (XXH_VECTOR == XXH_AVX2) || defined(XXH_X86DISPATCH) + +#ifndef XXH_TARGET_AVX2 +# define XXH_TARGET_AVX2 /* disable attribute target */ +#endif + +XXH_FORCE_INLINE XXH_TARGET_AVX2 void +XXH3_accumulate_512_avx2( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 31) == 0); + { XXH_ALIGN(32) __m256i* const xacc = (__m256i *) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */ + const __m256i* const xinput = (const __m256i *) input; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */ + const __m256i* const xsecret = (const __m256i *) secret; + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m256i); i++) { + /* data_vec = xinput[i]; */ + __m256i const data_vec = _mm256_loadu_si256 (xinput+i); + /* key_vec = xsecret[i]; */ + __m256i const key_vec = _mm256_loadu_si256 (xsecret+i); + /* data_key = data_vec ^ key_vec; */ + __m256i const data_key = _mm256_xor_si256 (data_vec, key_vec); + /* data_key_lo = data_key >> 32; */ + __m256i const data_key_lo = _mm256_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1)); + /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */ + __m256i const product = _mm256_mul_epu32 (data_key, data_key_lo); + /* xacc[i] += swap(data_vec); */ + __m256i const data_swap = _mm256_shuffle_epi32(data_vec, _MM_SHUFFLE(1, 0, 3, 2)); + __m256i const sum = _mm256_add_epi64(xacc[i], data_swap); + /* xacc[i] += product; */ + xacc[i] = _mm256_add_epi64(product, sum); + } } +} + +XXH_FORCE_INLINE XXH_TARGET_AVX2 void +XXH3_scrambleAcc_avx2(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 31) == 0); + { XXH_ALIGN(32) __m256i* const xacc = (__m256i*) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */ + const __m256i* const xsecret = (const __m256i *) secret; + const __m256i prime32 = _mm256_set1_epi32((int)XXH_PRIME32_1); + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m256i); i++) { + /* xacc[i] ^= (xacc[i] >> 47) */ + __m256i const acc_vec = xacc[i]; + __m256i const shifted = _mm256_srli_epi64 (acc_vec, 47); + __m256i const data_vec = _mm256_xor_si256 (acc_vec, shifted); + /* xacc[i] ^= xsecret; */ + __m256i const key_vec = _mm256_loadu_si256 (xsecret+i); + __m256i const data_key = _mm256_xor_si256 (data_vec, key_vec); + + /* xacc[i] *= XXH_PRIME32_1; */ + __m256i const data_key_hi = _mm256_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1)); + __m256i const prod_lo = _mm256_mul_epu32 (data_key, prime32); + __m256i const prod_hi = _mm256_mul_epu32 (data_key_hi, prime32); + xacc[i] = _mm256_add_epi64(prod_lo, _mm256_slli_epi64(prod_hi, 32)); + } + } +} + +XXH_FORCE_INLINE XXH_TARGET_AVX2 void XXH3_initCustomSecret_avx2(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 31) == 0); + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE / sizeof(__m256i)) == 6); + XXH_STATIC_ASSERT(XXH_SEC_ALIGN <= 64); + (void)(&XXH_writeLE64); + XXH_PREFETCH(customSecret); + { __m256i const seed = _mm256_set_epi64x(-(xxh_i64)seed64, (xxh_i64)seed64, -(xxh_i64)seed64, (xxh_i64)seed64); + + XXH_ALIGN(64) const __m256i* const src = (const __m256i*) XXH3_kSecret; + XXH_ALIGN(64) __m256i* dest = ( __m256i*) customSecret; + +# if defined(__GNUC__) || defined(__clang__) + /* + * On GCC & Clang, marking 'dest' as modified will cause the compiler: + * - do not extract the secret from sse registers in the internal loop + * - use less common registers, and avoid pushing these reg into stack + * The asm hack causes Clang to assume that XXH3_kSecretPtr aliases with + * customSecret, and on aarch64, this prevented LDP from merging two + * loads together for free. Putting the loads together before the stores + * properly generates LDP. + */ + __asm__("" : "+r" (dest)); +# endif + + /* GCC -O2 need unroll loop manually */ + dest[0] = _mm256_add_epi64(_mm256_stream_load_si256(src+0), seed); + dest[1] = _mm256_add_epi64(_mm256_stream_load_si256(src+1), seed); + dest[2] = _mm256_add_epi64(_mm256_stream_load_si256(src+2), seed); + dest[3] = _mm256_add_epi64(_mm256_stream_load_si256(src+3), seed); + dest[4] = _mm256_add_epi64(_mm256_stream_load_si256(src+4), seed); + dest[5] = _mm256_add_epi64(_mm256_stream_load_si256(src+5), seed); + } +} + +#endif + +#if (XXH_VECTOR == XXH_SSE2) || defined(XXH_X86DISPATCH) + +#ifndef XXH_TARGET_SSE2 +# define XXH_TARGET_SSE2 /* disable attribute target */ +#endif + +XXH_FORCE_INLINE XXH_TARGET_SSE2 void +XXH3_accumulate_512_sse2( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + /* SSE2 is just a half-scale version of the AVX2 version. */ + XXH_ASSERT((((size_t)acc) & 15) == 0); + { XXH_ALIGN(16) __m128i* const xacc = (__m128i *) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */ + const __m128i* const xinput = (const __m128i *) input; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */ + const __m128i* const xsecret = (const __m128i *) secret; + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m128i); i++) { + /* data_vec = xinput[i]; */ + __m128i const data_vec = _mm_loadu_si128 (xinput+i); + /* key_vec = xsecret[i]; */ + __m128i const key_vec = _mm_loadu_si128 (xsecret+i); + /* data_key = data_vec ^ key_vec; */ + __m128i const data_key = _mm_xor_si128 (data_vec, key_vec); + /* data_key_lo = data_key >> 32; */ + __m128i const data_key_lo = _mm_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1)); + /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */ + __m128i const product = _mm_mul_epu32 (data_key, data_key_lo); + /* xacc[i] += swap(data_vec); */ + __m128i const data_swap = _mm_shuffle_epi32(data_vec, _MM_SHUFFLE(1,0,3,2)); + __m128i const sum = _mm_add_epi64(xacc[i], data_swap); + /* xacc[i] += product; */ + xacc[i] = _mm_add_epi64(product, sum); + } } +} + +XXH_FORCE_INLINE XXH_TARGET_SSE2 void +XXH3_scrambleAcc_sse2(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + { XXH_ALIGN(16) __m128i* const xacc = (__m128i*) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */ + const __m128i* const xsecret = (const __m128i *) secret; + const __m128i prime32 = _mm_set1_epi32((int)XXH_PRIME32_1); + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m128i); i++) { + /* xacc[i] ^= (xacc[i] >> 47) */ + __m128i const acc_vec = xacc[i]; + __m128i const shifted = _mm_srli_epi64 (acc_vec, 47); + __m128i const data_vec = _mm_xor_si128 (acc_vec, shifted); + /* xacc[i] ^= xsecret[i]; */ + __m128i const key_vec = _mm_loadu_si128 (xsecret+i); + __m128i const data_key = _mm_xor_si128 (data_vec, key_vec); + + /* xacc[i] *= XXH_PRIME32_1; */ + __m128i const data_key_hi = _mm_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1)); + __m128i const prod_lo = _mm_mul_epu32 (data_key, prime32); + __m128i const prod_hi = _mm_mul_epu32 (data_key_hi, prime32); + xacc[i] = _mm_add_epi64(prod_lo, _mm_slli_epi64(prod_hi, 32)); + } + } +} + +XXH_FORCE_INLINE XXH_TARGET_SSE2 void XXH3_initCustomSecret_sse2(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 15) == 0); + (void)(&XXH_writeLE64); + { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / sizeof(__m128i); + +# if defined(_MSC_VER) && defined(_M_IX86) && _MSC_VER < 1900 + // MSVC 32bit mode does not support _mm_set_epi64x before 2015 + XXH_ALIGN(16) const xxh_i64 seed64x2[2] = { (xxh_i64)seed64, -(xxh_i64)seed64 }; + __m128i const seed = _mm_load_si128((__m128i const*)seed64x2); +# else + __m128i const seed = _mm_set_epi64x(-(xxh_i64)seed64, (xxh_i64)seed64); +# endif + int i; + + XXH_ALIGN(64) const float* const src = (float const*) XXH3_kSecret; + XXH_ALIGN(XXH_SEC_ALIGN) __m128i* dest = (__m128i*) customSecret; +# if defined(__GNUC__) || defined(__clang__) + /* + * On GCC & Clang, marking 'dest' as modified will cause the compiler: + * - do not extract the secret from sse registers in the internal loop + * - use less common registers, and avoid pushing these reg into stack + */ + __asm__("" : "+r" (dest)); +# endif + + for (i=0; i < nbRounds; ++i) { + dest[i] = _mm_add_epi64(_mm_castps_si128(_mm_load_ps(src+i*4)), seed); + } } +} + +#endif + +#if (XXH_VECTOR == XXH_NEON) + +XXH_FORCE_INLINE void +XXH3_accumulate_512_neon( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + { + XXH_ALIGN(16) uint64x2_t* const xacc = (uint64x2_t *) acc; + /* We don't use a uint32x4_t pointer because it causes bus errors on ARMv7. */ + uint8_t const* const xinput = (const uint8_t *) input; + uint8_t const* const xsecret = (const uint8_t *) secret; + + size_t i; + for (i=0; i < XXH_STRIPE_LEN / sizeof(uint64x2_t); i++) { + /* data_vec = xinput[i]; */ + uint8x16_t data_vec = vld1q_u8(xinput + (i * 16)); + /* key_vec = xsecret[i]; */ + uint8x16_t key_vec = vld1q_u8(xsecret + (i * 16)); + uint64x2_t data_key; + uint32x2_t data_key_lo, data_key_hi; + /* xacc[i] += swap(data_vec); */ + uint64x2_t const data64 = vreinterpretq_u64_u8(data_vec); + uint64x2_t const swapped = vextq_u64(data64, data64, 1); + xacc[i] = vaddq_u64 (xacc[i], swapped); + /* data_key = data_vec ^ key_vec; */ + data_key = vreinterpretq_u64_u8(veorq_u8(data_vec, key_vec)); + /* data_key_lo = (uint32x2_t) (data_key & 0xFFFFFFFF); + * data_key_hi = (uint32x2_t) (data_key >> 32); + * data_key = UNDEFINED; */ + XXH_SPLIT_IN_PLACE(data_key, data_key_lo, data_key_hi); + /* xacc[i] += (uint64x2_t) data_key_lo * (uint64x2_t) data_key_hi; */ + xacc[i] = vmlal_u32 (xacc[i], data_key_lo, data_key_hi); + + } + } +} + +XXH_FORCE_INLINE void +XXH3_scrambleAcc_neon(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + + { uint64x2_t* xacc = (uint64x2_t*) acc; + uint8_t const* xsecret = (uint8_t const*) secret; + uint32x2_t prime = vdup_n_u32 (XXH_PRIME32_1); + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(uint64x2_t); i++) { + /* xacc[i] ^= (xacc[i] >> 47); */ + uint64x2_t acc_vec = xacc[i]; + uint64x2_t shifted = vshrq_n_u64 (acc_vec, 47); + uint64x2_t data_vec = veorq_u64 (acc_vec, shifted); + + /* xacc[i] ^= xsecret[i]; */ + uint8x16_t key_vec = vld1q_u8(xsecret + (i * 16)); + uint64x2_t data_key = veorq_u64(data_vec, vreinterpretq_u64_u8(key_vec)); + + /* xacc[i] *= XXH_PRIME32_1 */ + uint32x2_t data_key_lo, data_key_hi; + /* data_key_lo = (uint32x2_t) (xacc[i] & 0xFFFFFFFF); + * data_key_hi = (uint32x2_t) (xacc[i] >> 32); + * xacc[i] = UNDEFINED; */ + XXH_SPLIT_IN_PLACE(data_key, data_key_lo, data_key_hi); + { /* + * prod_hi = (data_key >> 32) * XXH_PRIME32_1; + * + * Avoid vmul_u32 + vshll_n_u32 since Clang 6 and 7 will + * incorrectly "optimize" this: + * tmp = vmul_u32(vmovn_u64(a), vmovn_u64(b)); + * shifted = vshll_n_u32(tmp, 32); + * to this: + * tmp = "vmulq_u64"(a, b); // no such thing! + * shifted = vshlq_n_u64(tmp, 32); + * + * However, unlike SSE, Clang lacks a 64-bit multiply routine + * for NEON, and it scalarizes two 64-bit multiplies instead. + * + * vmull_u32 has the same timing as vmul_u32, and it avoids + * this bug completely. + * See https://bugs.llvm.org/show_bug.cgi?id=39967 + */ + uint64x2_t prod_hi = vmull_u32 (data_key_hi, prime); + /* xacc[i] = prod_hi << 32; */ + xacc[i] = vshlq_n_u64(prod_hi, 32); + /* xacc[i] += (prod_hi & 0xFFFFFFFF) * XXH_PRIME32_1; */ + xacc[i] = vmlal_u32(xacc[i], data_key_lo, prime); + } + } } +} + +#endif + +#if (XXH_VECTOR == XXH_VSX) + +XXH_FORCE_INLINE void +XXH3_accumulate_512_vsx( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + xxh_u64x2* const xacc = (xxh_u64x2*) acc; /* presumed aligned */ + xxh_u64x2 const* const xinput = (xxh_u64x2 const*) input; /* no alignment restriction */ + xxh_u64x2 const* const xsecret = (xxh_u64x2 const*) secret; /* no alignment restriction */ + xxh_u64x2 const v32 = { 32, 32 }; + size_t i; + for (i = 0; i < XXH_STRIPE_LEN / sizeof(xxh_u64x2); i++) { + /* data_vec = xinput[i]; */ + xxh_u64x2 const data_vec = XXH_vec_loadu(xinput + i); + /* key_vec = xsecret[i]; */ + xxh_u64x2 const key_vec = XXH_vec_loadu(xsecret + i); + xxh_u64x2 const data_key = data_vec ^ key_vec; + /* shuffled = (data_key << 32) | (data_key >> 32); */ + xxh_u32x4 const shuffled = (xxh_u32x4)vec_rl(data_key, v32); + /* product = ((xxh_u64x2)data_key & 0xFFFFFFFF) * ((xxh_u64x2)shuffled & 0xFFFFFFFF); */ + xxh_u64x2 const product = XXH_vec_mulo((xxh_u32x4)data_key, shuffled); + xacc[i] += product; + + /* swap high and low halves */ +#ifdef __s390x__ + xacc[i] += vec_permi(data_vec, data_vec, 2); +#else + xacc[i] += vec_xxpermdi(data_vec, data_vec, 2); +#endif + } +} + +XXH_FORCE_INLINE void +XXH3_scrambleAcc_vsx(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + + { xxh_u64x2* const xacc = (xxh_u64x2*) acc; + const xxh_u64x2* const xsecret = (const xxh_u64x2*) secret; + /* constants */ + xxh_u64x2 const v32 = { 32, 32 }; + xxh_u64x2 const v47 = { 47, 47 }; + xxh_u32x4 const prime = { XXH_PRIME32_1, XXH_PRIME32_1, XXH_PRIME32_1, XXH_PRIME32_1 }; + size_t i; + for (i = 0; i < XXH_STRIPE_LEN / sizeof(xxh_u64x2); i++) { + /* xacc[i] ^= (xacc[i] >> 47); */ + xxh_u64x2 const acc_vec = xacc[i]; + xxh_u64x2 const data_vec = acc_vec ^ (acc_vec >> v47); + + /* xacc[i] ^= xsecret[i]; */ + xxh_u64x2 const key_vec = XXH_vec_loadu(xsecret + i); + xxh_u64x2 const data_key = data_vec ^ key_vec; + + /* xacc[i] *= XXH_PRIME32_1 */ + /* prod_lo = ((xxh_u64x2)data_key & 0xFFFFFFFF) * ((xxh_u64x2)prime & 0xFFFFFFFF); */ + xxh_u64x2 const prod_even = XXH_vec_mule((xxh_u32x4)data_key, prime); + /* prod_hi = ((xxh_u64x2)data_key >> 32) * ((xxh_u64x2)prime >> 32); */ + xxh_u64x2 const prod_odd = XXH_vec_mulo((xxh_u32x4)data_key, prime); + xacc[i] = prod_odd + (prod_even << v32); + } } +} + +#endif + +/* scalar variants - universal */ + +XXH_FORCE_INLINE void +XXH3_accumulate_512_scalar(void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64* const xacc = (xxh_u64*) acc; /* presumed aligned */ + const xxh_u8* const xinput = (const xxh_u8*) input; /* no alignment restriction */ + const xxh_u8* const xsecret = (const xxh_u8*) secret; /* no alignment restriction */ + size_t i; + XXH_ASSERT(((size_t)acc & (XXH_ACC_ALIGN-1)) == 0); + for (i=0; i < XXH_ACC_NB; i++) { + xxh_u64 const data_val = XXH_readLE64(xinput + 8*i); + xxh_u64 const data_key = data_val ^ XXH_readLE64(xsecret + i*8); + xacc[i ^ 1] += data_val; /* swap adjacent lanes */ + xacc[i] += XXH_mult32to64(data_key & 0xFFFFFFFF, data_key >> 32); + } +} + +XXH_FORCE_INLINE void +XXH3_scrambleAcc_scalar(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64* const xacc = (xxh_u64*) acc; /* presumed aligned */ + const xxh_u8* const xsecret = (const xxh_u8*) secret; /* no alignment restriction */ + size_t i; + XXH_ASSERT((((size_t)acc) & (XXH_ACC_ALIGN-1)) == 0); + for (i=0; i < XXH_ACC_NB; i++) { + xxh_u64 const key64 = XXH_readLE64(xsecret + 8*i); + xxh_u64 acc64 = xacc[i]; + acc64 = XXH_xorshift64(acc64, 47); + acc64 ^= key64; + acc64 *= XXH_PRIME32_1; + xacc[i] = acc64; + } +} + +XXH_FORCE_INLINE void +XXH3_initCustomSecret_scalar(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + /* + * We need a separate pointer for the hack below, + * which requires a non-const pointer. + * Any decent compiler will optimize this out otherwise. + */ + const xxh_u8* kSecretPtr = XXH3_kSecret; + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 15) == 0); + +#if defined(__clang__) && defined(__aarch64__) + /* + * UGLY HACK: + * Clang generates a bunch of MOV/MOVK pairs for aarch64, and they are + * placed sequentially, in order, at the top of the unrolled loop. + * + * While MOVK is great for generating constants (2 cycles for a 64-bit + * constant compared to 4 cycles for LDR), long MOVK chains stall the + * integer pipelines: + * I L S + * MOVK + * MOVK + * MOVK + * MOVK + * ADD + * SUB STR + * STR + * By forcing loads from memory (as the asm line causes Clang to assume + * that XXH3_kSecretPtr has been changed), the pipelines are used more + * efficiently: + * I L S + * LDR + * ADD LDR + * SUB STR + * STR + * XXH3_64bits_withSeed, len == 256, Snapdragon 835 + * without hack: 2654.4 MB/s + * with hack: 3202.9 MB/s + */ + __asm__("" : "+r" (kSecretPtr)); +#endif + /* + * Note: in debug mode, this overrides the asm optimization + * and Clang will emit MOVK chains again. + */ + XXH_ASSERT(kSecretPtr == XXH3_kSecret); + + { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / 16; + int i; + for (i=0; i < nbRounds; i++) { + /* + * The asm hack causes Clang to assume that kSecretPtr aliases with + * customSecret, and on aarch64, this prevented LDP from merging two + * loads together for free. Putting the loads together before the stores + * properly generates LDP. + */ + xxh_u64 lo = XXH_readLE64(kSecretPtr + 16*i) + seed64; + xxh_u64 hi = XXH_readLE64(kSecretPtr + 16*i + 8) - seed64; + XXH_writeLE64((xxh_u8*)customSecret + 16*i, lo); + XXH_writeLE64((xxh_u8*)customSecret + 16*i + 8, hi); + } } +} + + +typedef void (*XXH3_f_accumulate_512)(void* XXH_RESTRICT, const void*, const void*); +typedef void (*XXH3_f_scrambleAcc)(void* XXH_RESTRICT, const void*); +typedef void (*XXH3_f_initCustomSecret)(void* XXH_RESTRICT, xxh_u64); + + +#if (XXH_VECTOR == XXH_AVX512) + +#define XXH3_accumulate_512 XXH3_accumulate_512_avx512 +#define XXH3_scrambleAcc XXH3_scrambleAcc_avx512 +#define XXH3_initCustomSecret XXH3_initCustomSecret_avx512 + +#elif (XXH_VECTOR == XXH_AVX2) + +#define XXH3_accumulate_512 XXH3_accumulate_512_avx2 +#define XXH3_scrambleAcc XXH3_scrambleAcc_avx2 +#define XXH3_initCustomSecret XXH3_initCustomSecret_avx2 + +#elif (XXH_VECTOR == XXH_SSE2) + +#define XXH3_accumulate_512 XXH3_accumulate_512_sse2 +#define XXH3_scrambleAcc XXH3_scrambleAcc_sse2 +#define XXH3_initCustomSecret XXH3_initCustomSecret_sse2 + +#elif (XXH_VECTOR == XXH_NEON) + +#define XXH3_accumulate_512 XXH3_accumulate_512_neon +#define XXH3_scrambleAcc XXH3_scrambleAcc_neon +#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar + +#elif (XXH_VECTOR == XXH_VSX) + +#define XXH3_accumulate_512 XXH3_accumulate_512_vsx +#define XXH3_scrambleAcc XXH3_scrambleAcc_vsx +#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar + +#else /* scalar */ + +#define XXH3_accumulate_512 XXH3_accumulate_512_scalar +#define XXH3_scrambleAcc XXH3_scrambleAcc_scalar +#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar + +#endif + + + +#ifndef XXH_PREFETCH_DIST +# ifdef __clang__ +# define XXH_PREFETCH_DIST 320 +# else +# if (XXH_VECTOR == XXH_AVX512) +# define XXH_PREFETCH_DIST 512 +# else +# define XXH_PREFETCH_DIST 384 +# endif +# endif /* __clang__ */ +#endif /* XXH_PREFETCH_DIST */ + +/* + * XXH3_accumulate() + * Loops over XXH3_accumulate_512(). + * Assumption: nbStripes will not overflow the secret size + */ +XXH_FORCE_INLINE void +XXH3_accumulate( xxh_u64* XXH_RESTRICT acc, + const xxh_u8* XXH_RESTRICT input, + const xxh_u8* XXH_RESTRICT secret, + size_t nbStripes, + XXH3_f_accumulate_512 f_acc512) +{ + size_t n; + for (n = 0; n < nbStripes; n++ ) { + const xxh_u8* const in = input + n*XXH_STRIPE_LEN; + XXH_PREFETCH(in + XXH_PREFETCH_DIST); + f_acc512(acc, + in, + secret + n*XXH_SECRET_CONSUME_RATE); + } +} + +XXH_FORCE_INLINE void +XXH3_hashLong_internal_loop(xxh_u64* XXH_RESTRICT acc, + const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH3_f_accumulate_512 f_acc512, + XXH3_f_scrambleAcc f_scramble) +{ + size_t const nbStripesPerBlock = (secretSize - XXH_STRIPE_LEN) / XXH_SECRET_CONSUME_RATE; + size_t const block_len = XXH_STRIPE_LEN * nbStripesPerBlock; + size_t const nb_blocks = (len - 1) / block_len; + + size_t n; + + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); + + for (n = 0; n < nb_blocks; n++) { + XXH3_accumulate(acc, input + n*block_len, secret, nbStripesPerBlock, f_acc512); + f_scramble(acc, secret + secretSize - XXH_STRIPE_LEN); + } + + /* last partial block */ + XXH_ASSERT(len > XXH_STRIPE_LEN); + { size_t const nbStripes = ((len - 1) - (block_len * nb_blocks)) / XXH_STRIPE_LEN; + XXH_ASSERT(nbStripes <= (secretSize / XXH_SECRET_CONSUME_RATE)); + XXH3_accumulate(acc, input + nb_blocks*block_len, secret, nbStripes, f_acc512); + + /* last stripe */ + { const xxh_u8* const p = input + len - XXH_STRIPE_LEN; +#define XXH_SECRET_LASTACC_START 7 /* not aligned on 8, last secret is different from acc & scrambler */ + f_acc512(acc, p, secret + secretSize - XXH_STRIPE_LEN - XXH_SECRET_LASTACC_START); + } } +} + +XXH_FORCE_INLINE xxh_u64 +XXH3_mix2Accs(const xxh_u64* XXH_RESTRICT acc, const xxh_u8* XXH_RESTRICT secret) +{ + return XXH3_mul128_fold64( + acc[0] ^ XXH_readLE64(secret), + acc[1] ^ XXH_readLE64(secret+8) ); +} + +static XXH64_hash_t +XXH3_mergeAccs(const xxh_u64* XXH_RESTRICT acc, const xxh_u8* XXH_RESTRICT secret, xxh_u64 start) +{ + xxh_u64 result64 = start; + size_t i = 0; + + for (i = 0; i < 4; i++) { + result64 += XXH3_mix2Accs(acc+2*i, secret + 16*i); +#if defined(__clang__) /* Clang */ \ + && (defined(__arm__) || defined(__thumb__)) /* ARMv7 */ \ + && (defined(__ARM_NEON) || defined(__ARM_NEON__)) /* NEON */ \ + && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable */ + /* + * UGLY HACK: + * Prevent autovectorization on Clang ARMv7-a. Exact same problem as + * the one in XXH3_len_129to240_64b. Speeds up shorter keys > 240b. + * XXH3_64bits, len == 256, Snapdragon 835: + * without hack: 2063.7 MB/s + * with hack: 2560.7 MB/s + */ + __asm__("" : "+r" (result64)); +#endif + } + + return XXH3_avalanche(result64); +} + +#define XXH3_INIT_ACC { XXH_PRIME32_3, XXH_PRIME64_1, XXH_PRIME64_2, XXH_PRIME64_3, \ + XXH_PRIME64_4, XXH_PRIME32_2, XXH_PRIME64_5, XXH_PRIME32_1 } + +XXH_FORCE_INLINE XXH64_hash_t +XXH3_hashLong_64b_internal(const void* XXH_RESTRICT input, size_t len, + const void* XXH_RESTRICT secret, size_t secretSize, + XXH3_f_accumulate_512 f_acc512, + XXH3_f_scrambleAcc f_scramble) +{ + XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[XXH_ACC_NB] = XXH3_INIT_ACC; + + XXH3_hashLong_internal_loop(acc, (const xxh_u8*)input, len, (const xxh_u8*)secret, secretSize, f_acc512, f_scramble); + + /* converge into final hash */ + XXH_STATIC_ASSERT(sizeof(acc) == 64); + /* do not align on 8, so that the secret is different from the accumulator */ +#define XXH_SECRET_MERGEACCS_START 11 + XXH_ASSERT(secretSize >= sizeof(acc) + XXH_SECRET_MERGEACCS_START); + return XXH3_mergeAccs(acc, (const xxh_u8*)secret + XXH_SECRET_MERGEACCS_START, (xxh_u64)len * XXH_PRIME64_1); +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + */ +XXH_NO_INLINE XXH64_hash_t +XXH3_hashLong_64b_withSecret(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; + return XXH3_hashLong_64b_internal(input, len, secret, secretLen, XXH3_accumulate_512, XXH3_scrambleAcc); +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + * Since the function is not inlined, the compiler may not be able to understand that, + * in some scenarios, its `secret` argument is actually a compile time constant. + * This variant enforces that the compiler can detect that, + * and uses this opportunity to streamline the generated code for better performance. + */ +XXH_NO_INLINE XXH64_hash_t +XXH3_hashLong_64b_default(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; (void)secret; (void)secretLen; + return XXH3_hashLong_64b_internal(input, len, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_accumulate_512, XXH3_scrambleAcc); +} + +/* + * XXH3_hashLong_64b_withSeed(): + * Generate a custom key based on alteration of default XXH3_kSecret with the seed, + * and then use this key for long mode hashing. + * + * This operation is decently fast but nonetheless costs a little bit of time. + * Try to avoid it whenever possible (typically when seed==0). + * + * It's important for performance that XXH3_hashLong is not inlined. Not sure + * why (uop cache maybe?), but the difference is large and easily measurable. + */ +XXH_FORCE_INLINE XXH64_hash_t +XXH3_hashLong_64b_withSeed_internal(const void* input, size_t len, + XXH64_hash_t seed, + XXH3_f_accumulate_512 f_acc512, + XXH3_f_scrambleAcc f_scramble, + XXH3_f_initCustomSecret f_initSec) +{ + if (seed == 0) + return XXH3_hashLong_64b_internal(input, len, + XXH3_kSecret, sizeof(XXH3_kSecret), + f_acc512, f_scramble); + { XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE]; + f_initSec(secret, seed); + return XXH3_hashLong_64b_internal(input, len, secret, sizeof(secret), + f_acc512, f_scramble); + } +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + */ +XXH_NO_INLINE XXH64_hash_t +XXH3_hashLong_64b_withSeed(const void* input, size_t len, + XXH64_hash_t seed, const xxh_u8* secret, size_t secretLen) +{ + (void)secret; (void)secretLen; + return XXH3_hashLong_64b_withSeed_internal(input, len, seed, + XXH3_accumulate_512, XXH3_scrambleAcc, XXH3_initCustomSecret); +} + + +typedef XXH64_hash_t (*XXH3_hashLong64_f)(const void* XXH_RESTRICT, size_t, + XXH64_hash_t, const xxh_u8* XXH_RESTRICT, size_t); + +XXH_FORCE_INLINE XXH64_hash_t +XXH3_64bits_internal(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen, + XXH3_hashLong64_f f_hashLong) +{ + XXH_ASSERT(secretLen >= XXH3_SECRET_SIZE_MIN); + /* + * If an action is to be taken if `secretLen` condition is not respected, + * it should be done here. + * For now, it's a contract pre-condition. + * Adding a check and a branch here would cost performance at every hash. + * Also, note that function signature doesn't offer room to return an error. + */ + if (len <= 16) + return XXH3_len_0to16_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, seed64); + if (len <= 128) + return XXH3_len_17to128_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + if (len <= XXH3_MIDSIZE_MAX) + return XXH3_len_129to240_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + return f_hashLong(input, len, seed64, (const xxh_u8*)secret, secretLen); +} + + +/* === Public entry point === */ + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits(const void* input, size_t len) +{ + return XXH3_64bits_internal(input, len, 0, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_default); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH64_hash_t +XXH3_64bits_withSecret(const void* input, size_t len, const void* secret, size_t secretSize) +{ + return XXH3_64bits_internal(input, len, 0, secret, secretSize, XXH3_hashLong_64b_withSecret); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH64_hash_t +XXH3_64bits_withSeed(const void* input, size_t len, XXH64_hash_t seed) +{ + return XXH3_64bits_internal(input, len, seed, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_withSeed); +} + + +/* === XXH3 streaming === */ + +/* + * Malloc's a pointer that is always aligned to align. + * + * This must be freed with `XXH_alignedFree()`. + * + * malloc typically guarantees 16 byte alignment on 64-bit systems and 8 byte + * alignment on 32-bit. This isn't enough for the 32 byte aligned loads in AVX2 + * or on 32-bit, the 16 byte aligned loads in SSE2 and NEON. + * + * This underalignment previously caused a rather obvious crash which went + * completely unnoticed due to XXH3_createState() not actually being tested. + * Credit to RedSpah for noticing this bug. + * + * The alignment is done manually: Functions like posix_memalign or _mm_malloc + * are avoided: To maintain portability, we would have to write a fallback + * like this anyways, and besides, testing for the existence of library + * functions without relying on external build tools is impossible. + * + * The method is simple: Overallocate, manually align, and store the offset + * to the original behind the returned pointer. + * + * Align must be a power of 2 and 8 <= align <= 128. + */ +static void* XXH_alignedMalloc(size_t s, size_t align) +{ + XXH_ASSERT(align <= 128 && align >= 8); /* range check */ + XXH_ASSERT((align & (align-1)) == 0); /* power of 2 */ + XXH_ASSERT(s != 0 && s < (s + align)); /* empty/overflow */ + { /* Overallocate to make room for manual realignment and an offset byte */ + xxh_u8* base = (xxh_u8*)XXH_malloc(s + align); + if (base != NULL) { + /* + * Get the offset needed to align this pointer. + * + * Even if the returned pointer is aligned, there will always be + * at least one byte to store the offset to the original pointer. + */ + size_t offset = align - ((size_t)base & (align - 1)); /* base % align */ + /* Add the offset for the now-aligned pointer */ + xxh_u8* ptr = base + offset; + + XXH_ASSERT((size_t)ptr % align == 0); + + /* Store the offset immediately before the returned pointer. */ + ptr[-1] = (xxh_u8)offset; + return ptr; + } + return NULL; + } +} +/* + * Frees an aligned pointer allocated by XXH_alignedMalloc(). Don't pass + * normal malloc'd pointers, XXH_alignedMalloc has a specific data layout. + */ +static void XXH_alignedFree(void* p) +{ + if (p != NULL) { + xxh_u8* ptr = (xxh_u8*)p; + /* Get the offset byte we added in XXH_malloc. */ + xxh_u8 offset = ptr[-1]; + /* Free the original malloc'd pointer */ + xxh_u8* base = ptr - offset; + XXH_free(base); + } +} +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH3_state_t* XXH3_createState(void) +{ + XXH3_state_t* const state = (XXH3_state_t*)XXH_alignedMalloc(sizeof(XXH3_state_t), 64); + if (state==NULL) return NULL; + XXH3_INITSTATE(state); + return state; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode XXH3_freeState(XXH3_state_t* statePtr) +{ + XXH_alignedFree(statePtr); + return XXH_OK; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API void +XXH3_copyState(XXH3_state_t* dst_state, const XXH3_state_t* src_state) +{ + memcpy(dst_state, src_state, sizeof(*dst_state)); +} + +static void +XXH3_64bits_reset_internal(XXH3_state_t* statePtr, + XXH64_hash_t seed, + const void* secret, size_t secretSize) +{ + size_t const initStart = offsetof(XXH3_state_t, bufferedSize); + size_t const initLength = offsetof(XXH3_state_t, nbStripesPerBlock) - initStart; + XXH_ASSERT(offsetof(XXH3_state_t, nbStripesPerBlock) > initStart); + XXH_ASSERT(statePtr != NULL); + /* set members from bufferedSize to nbStripesPerBlock (excluded) to 0 */ + memset((char*)statePtr + initStart, 0, initLength); + statePtr->acc[0] = XXH_PRIME32_3; + statePtr->acc[1] = XXH_PRIME64_1; + statePtr->acc[2] = XXH_PRIME64_2; + statePtr->acc[3] = XXH_PRIME64_3; + statePtr->acc[4] = XXH_PRIME64_4; + statePtr->acc[5] = XXH_PRIME32_2; + statePtr->acc[6] = XXH_PRIME64_5; + statePtr->acc[7] = XXH_PRIME32_1; + statePtr->seed = seed; + statePtr->extSecret = (const unsigned char*)secret; + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); + statePtr->secretLimit = secretSize - XXH_STRIPE_LEN; + statePtr->nbStripesPerBlock = statePtr->secretLimit / XXH_SECRET_CONSUME_RATE; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset(XXH3_state_t* statePtr) +{ + if (statePtr == NULL) return XXH_ERROR; + XXH3_64bits_reset_internal(statePtr, 0, XXH3_kSecret, XXH_SECRET_DEFAULT_SIZE); + return XXH_OK; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize) +{ + if (statePtr == NULL) return XXH_ERROR; + XXH3_64bits_reset_internal(statePtr, 0, secret, secretSize); + if (secret == NULL) return XXH_ERROR; + if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR; + return XXH_OK; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed) +{ + if (statePtr == NULL) return XXH_ERROR; + if (seed==0) return XXH3_64bits_reset(statePtr); + if (seed != statePtr->seed) XXH3_initCustomSecret(statePtr->customSecret, seed); + XXH3_64bits_reset_internal(statePtr, seed, NULL, XXH_SECRET_DEFAULT_SIZE); + return XXH_OK; +} + +/* Note : when XXH3_consumeStripes() is invoked, + * there must be a guarantee that at least one more byte must be consumed from input + * so that the function can blindly consume all stripes using the "normal" secret segment */ +XXH_FORCE_INLINE void +XXH3_consumeStripes(xxh_u64* XXH_RESTRICT acc, + size_t* XXH_RESTRICT nbStripesSoFarPtr, size_t nbStripesPerBlock, + const xxh_u8* XXH_RESTRICT input, size_t nbStripes, + const xxh_u8* XXH_RESTRICT secret, size_t secretLimit, + XXH3_f_accumulate_512 f_acc512, + XXH3_f_scrambleAcc f_scramble) +{ + XXH_ASSERT(nbStripes <= nbStripesPerBlock); /* can handle max 1 scramble per invocation */ + XXH_ASSERT(*nbStripesSoFarPtr < nbStripesPerBlock); + if (nbStripesPerBlock - *nbStripesSoFarPtr <= nbStripes) { + /* need a scrambling operation */ + size_t const nbStripesToEndofBlock = nbStripesPerBlock - *nbStripesSoFarPtr; + size_t const nbStripesAfterBlock = nbStripes - nbStripesToEndofBlock; + XXH3_accumulate(acc, input, secret + nbStripesSoFarPtr[0] * XXH_SECRET_CONSUME_RATE, nbStripesToEndofBlock, f_acc512); + f_scramble(acc, secret + secretLimit); + XXH3_accumulate(acc, input + nbStripesToEndofBlock * XXH_STRIPE_LEN, secret, nbStripesAfterBlock, f_acc512); + *nbStripesSoFarPtr = nbStripesAfterBlock; + } else { + XXH3_accumulate(acc, input, secret + nbStripesSoFarPtr[0] * XXH_SECRET_CONSUME_RATE, nbStripes, f_acc512); + *nbStripesSoFarPtr += nbStripes; + } +} + +/* + * Both XXH3_64bits_update and XXH3_128bits_update use this routine. + */ +XXH_FORCE_INLINE XXH_errorcode +XXH3_update(XXH3_state_t* state, + const xxh_u8* input, size_t len, + XXH3_f_accumulate_512 f_acc512, + XXH3_f_scrambleAcc f_scramble) +{ + if (input==NULL) +#if defined(XXH_ACCEPT_NULL_INPUT_POINTER) && (XXH_ACCEPT_NULL_INPUT_POINTER>=1) + return XXH_OK; +#else + return XXH_ERROR; +#endif + + { const xxh_u8* const bEnd = input + len; + const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret; + + state->totalLen += len; + + if (state->bufferedSize + len <= XXH3_INTERNALBUFFER_SIZE) { /* fill in tmp buffer */ + XXH_memcpy(state->buffer + state->bufferedSize, input, len); + state->bufferedSize += (XXH32_hash_t)len; + return XXH_OK; + } + /* total input is now > XXH3_INTERNALBUFFER_SIZE */ + + #define XXH3_INTERNALBUFFER_STRIPES (XXH3_INTERNALBUFFER_SIZE / XXH_STRIPE_LEN) + XXH_STATIC_ASSERT(XXH3_INTERNALBUFFER_SIZE % XXH_STRIPE_LEN == 0); /* clean multiple */ + + /* + * Internal buffer is partially filled (always, except at beginning) + * Complete it, then consume it. + */ + if (state->bufferedSize) { + size_t const loadSize = XXH3_INTERNALBUFFER_SIZE - state->bufferedSize; + XXH_memcpy(state->buffer + state->bufferedSize, input, loadSize); + input += loadSize; + XXH3_consumeStripes(state->acc, + &state->nbStripesSoFar, state->nbStripesPerBlock, + state->buffer, XXH3_INTERNALBUFFER_STRIPES, + secret, state->secretLimit, + f_acc512, f_scramble); + state->bufferedSize = 0; + } + XXH_ASSERT(input < bEnd); + + /* Consume input by a multiple of internal buffer size */ + if (input+XXH3_INTERNALBUFFER_SIZE < bEnd) { + const xxh_u8* const limit = bEnd - XXH3_INTERNALBUFFER_SIZE; + do { + XXH3_consumeStripes(state->acc, + &state->nbStripesSoFar, state->nbStripesPerBlock, + input, XXH3_INTERNALBUFFER_STRIPES, + secret, state->secretLimit, + f_acc512, f_scramble); + input += XXH3_INTERNALBUFFER_SIZE; + } while (inputbuffer + sizeof(state->buffer) - XXH_STRIPE_LEN, input - XXH_STRIPE_LEN, XXH_STRIPE_LEN); + } + XXH_ASSERT(input < bEnd); + + /* Some remaining input (always) : buffer it */ + XXH_memcpy(state->buffer, input, (size_t)(bEnd-input)); + state->bufferedSize = (XXH32_hash_t)(bEnd-input); + } + + return XXH_OK; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_update(XXH3_state_t* state, const void* input, size_t len) +{ + return XXH3_update(state, (const xxh_u8*)input, len, + XXH3_accumulate_512, XXH3_scrambleAcc); +} + + +XXH_FORCE_INLINE void +XXH3_digest_long (XXH64_hash_t* acc, + const XXH3_state_t* state, + const unsigned char* secret) +{ + /* + * Digest on a local copy. This way, the state remains unaltered, and it can + * continue ingesting more input afterwards. + */ + memcpy(acc, state->acc, sizeof(state->acc)); + if (state->bufferedSize >= XXH_STRIPE_LEN) { + size_t const nbStripes = (state->bufferedSize - 1) / XXH_STRIPE_LEN; + size_t nbStripesSoFar = state->nbStripesSoFar; + XXH3_consumeStripes(acc, + &nbStripesSoFar, state->nbStripesPerBlock, + state->buffer, nbStripes, + secret, state->secretLimit, + XXH3_accumulate_512, XXH3_scrambleAcc); + /* last stripe */ + XXH3_accumulate_512(acc, + state->buffer + state->bufferedSize - XXH_STRIPE_LEN, + secret + state->secretLimit - XXH_SECRET_LASTACC_START); + } else { /* bufferedSize < XXH_STRIPE_LEN */ + xxh_u8 lastStripe[XXH_STRIPE_LEN]; + size_t const catchupSize = XXH_STRIPE_LEN - state->bufferedSize; + XXH_ASSERT(state->bufferedSize > 0); /* there is always some input buffered */ + memcpy(lastStripe, state->buffer + sizeof(state->buffer) - catchupSize, catchupSize); + memcpy(lastStripe + catchupSize, state->buffer, state->bufferedSize); + XXH3_accumulate_512(acc, + lastStripe, + secret + state->secretLimit - XXH_SECRET_LASTACC_START); + } +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_digest (const XXH3_state_t* state) +{ + const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret; + if (state->totalLen > XXH3_MIDSIZE_MAX) { + XXH_ALIGN(XXH_ACC_ALIGN) XXH64_hash_t acc[XXH_ACC_NB]; + XXH3_digest_long(acc, state, secret); + return XXH3_mergeAccs(acc, + secret + XXH_SECRET_MERGEACCS_START, + (xxh_u64)state->totalLen * XXH_PRIME64_1); + } + /* totalLen <= XXH3_MIDSIZE_MAX: digesting a short input */ + if (state->seed) + return XXH3_64bits_withSeed(state->buffer, (size_t)state->totalLen, state->seed); + return XXH3_64bits_withSecret(state->buffer, (size_t)(state->totalLen), + secret, state->secretLimit + XXH_STRIPE_LEN); +} + + +#define XXH_MIN(x, y) (((x) > (y)) ? (y) : (x)) + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API void +XXH3_generateSecret(void* secretBuffer, const void* customSeed, size_t customSeedSize) +{ + XXH_ASSERT(secretBuffer != NULL); + if (customSeedSize == 0) { + memcpy(secretBuffer, XXH3_kSecret, XXH_SECRET_DEFAULT_SIZE); + return; + } + XXH_ASSERT(customSeed != NULL); + + { size_t const segmentSize = sizeof(XXH128_hash_t); + size_t const nbSegments = XXH_SECRET_DEFAULT_SIZE / segmentSize; + XXH128_canonical_t scrambler; + XXH64_hash_t seeds[12]; + size_t segnb; + XXH_ASSERT(nbSegments == 12); + XXH_ASSERT(segmentSize * nbSegments == XXH_SECRET_DEFAULT_SIZE); /* exact multiple */ + XXH128_canonicalFromHash(&scrambler, XXH128(customSeed, customSeedSize, 0)); + + /* + * Copy customSeed to seeds[], truncating or repeating as necessary. + */ + { size_t toFill = XXH_MIN(customSeedSize, sizeof(seeds)); + size_t filled = toFill; + memcpy(seeds, customSeed, toFill); + while (filled < sizeof(seeds)) { + toFill = XXH_MIN(filled, sizeof(seeds) - filled); + memcpy((char*)seeds + filled, seeds, toFill); + filled += toFill; + } } + + /* generate secret */ + memcpy(secretBuffer, &scrambler, sizeof(scrambler)); + for (segnb=1; segnb < nbSegments; segnb++) { + size_t const segmentStart = segnb * segmentSize; + XXH128_canonical_t segment; + XXH128_canonicalFromHash(&segment, + XXH128(&scrambler, sizeof(scrambler), XXH_readLE64(seeds + segnb) + segnb) ); + memcpy((char*)secretBuffer + segmentStart, &segment, sizeof(segment)); + } } +} + + +/* ========================================== + * XXH3 128 bits (a.k.a XXH128) + * ========================================== + * XXH3's 128-bit variant has better mixing and strength than the 64-bit variant, + * even without counting the significantly larger output size. + * + * For example, extra steps are taken to avoid the seed-dependent collisions + * in 17-240 byte inputs (See XXH3_mix16B and XXH128_mix32B). + * + * This strength naturally comes at the cost of some speed, especially on short + * lengths. Note that longer hashes are about as fast as the 64-bit version + * due to it using only a slight modification of the 64-bit loop. + * + * XXH128 is also more oriented towards 64-bit machines. It is still extremely + * fast for a _128-bit_ hash on 32-bit (it usually clears XXH64). + */ + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_len_1to3_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + /* A doubled version of 1to3_64b with different constants. */ + XXH_ASSERT(input != NULL); + XXH_ASSERT(1 <= len && len <= 3); + XXH_ASSERT(secret != NULL); + /* + * len = 1: combinedl = { input[0], 0x01, input[0], input[0] } + * len = 2: combinedl = { input[1], 0x02, input[0], input[1] } + * len = 3: combinedl = { input[2], 0x03, input[0], input[1] } + */ + { xxh_u8 const c1 = input[0]; + xxh_u8 const c2 = input[len >> 1]; + xxh_u8 const c3 = input[len - 1]; + xxh_u32 const combinedl = ((xxh_u32)c1 <<16) | ((xxh_u32)c2 << 24) + | ((xxh_u32)c3 << 0) | ((xxh_u32)len << 8); + xxh_u32 const combinedh = XXH_rotl32(XXH_swap32(combinedl), 13); + xxh_u64 const bitflipl = (XXH_readLE32(secret) ^ XXH_readLE32(secret+4)) + seed; + xxh_u64 const bitfliph = (XXH_readLE32(secret+8) ^ XXH_readLE32(secret+12)) - seed; + xxh_u64 const keyed_lo = (xxh_u64)combinedl ^ bitflipl; + xxh_u64 const keyed_hi = (xxh_u64)combinedh ^ bitfliph; + XXH128_hash_t h128; + h128.low64 = XXH64_avalanche(keyed_lo); + h128.high64 = XXH64_avalanche(keyed_hi); + return h128; + } +} + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_len_4to8_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(4 <= len && len <= 8); + seed ^= (xxh_u64)XXH_swap32((xxh_u32)seed) << 32; + { xxh_u32 const input_lo = XXH_readLE32(input); + xxh_u32 const input_hi = XXH_readLE32(input + len - 4); + xxh_u64 const input_64 = input_lo + ((xxh_u64)input_hi << 32); + xxh_u64 const bitflip = (XXH_readLE64(secret+16) ^ XXH_readLE64(secret+24)) + seed; + xxh_u64 const keyed = input_64 ^ bitflip; + + /* Shift len to the left to ensure it is even, this avoids even multiplies. */ + XXH128_hash_t m128 = XXH_mult64to128(keyed, XXH_PRIME64_1 + (len << 2)); + + m128.high64 += (m128.low64 << 1); + m128.low64 ^= (m128.high64 >> 3); + + m128.low64 = XXH_xorshift64(m128.low64, 35); + m128.low64 *= 0x9FB21C651E98DF25ULL; + m128.low64 = XXH_xorshift64(m128.low64, 28); + m128.high64 = XXH3_avalanche(m128.high64); + return m128; + } +} + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_len_9to16_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(9 <= len && len <= 16); + { xxh_u64 const bitflipl = (XXH_readLE64(secret+32) ^ XXH_readLE64(secret+40)) - seed; + xxh_u64 const bitfliph = (XXH_readLE64(secret+48) ^ XXH_readLE64(secret+56)) + seed; + xxh_u64 const input_lo = XXH_readLE64(input); + xxh_u64 input_hi = XXH_readLE64(input + len - 8); + XXH128_hash_t m128 = XXH_mult64to128(input_lo ^ input_hi ^ bitflipl, XXH_PRIME64_1); + /* + * Put len in the middle of m128 to ensure that the length gets mixed to + * both the low and high bits in the 128x64 multiply below. + */ + m128.low64 += (xxh_u64)(len - 1) << 54; + input_hi ^= bitfliph; + /* + * Add the high 32 bits of input_hi to the high 32 bits of m128, then + * add the long product of the low 32 bits of input_hi and XXH_PRIME32_2 to + * the high 64 bits of m128. + * + * The best approach to this operation is different on 32-bit and 64-bit. + */ + if (sizeof(void *) < sizeof(xxh_u64)) { /* 32-bit */ + /* + * 32-bit optimized version, which is more readable. + * + * On 32-bit, it removes an ADC and delays a dependency between the two + * halves of m128.high64, but it generates an extra mask on 64-bit. + */ + m128.high64 += (input_hi & 0xFFFFFFFF00000000ULL) + XXH_mult32to64((xxh_u32)input_hi, XXH_PRIME32_2); + } else { + /* + * 64-bit optimized (albeit more confusing) version. + * + * Uses some properties of addition and multiplication to remove the mask: + * + * Let: + * a = input_hi.lo = (input_hi & 0x00000000FFFFFFFF) + * b = input_hi.hi = (input_hi & 0xFFFFFFFF00000000) + * c = XXH_PRIME32_2 + * + * a + (b * c) + * Inverse Property: x + y - x == y + * a + (b * (1 + c - 1)) + * Distributive Property: x * (y + z) == (x * y) + (x * z) + * a + (b * 1) + (b * (c - 1)) + * Identity Property: x * 1 == x + * a + b + (b * (c - 1)) + * + * Substitute a, b, and c: + * input_hi.hi + input_hi.lo + ((xxh_u64)input_hi.lo * (XXH_PRIME32_2 - 1)) + * + * Since input_hi.hi + input_hi.lo == input_hi, we get this: + * input_hi + ((xxh_u64)input_hi.lo * (XXH_PRIME32_2 - 1)) + */ + m128.high64 += input_hi + XXH_mult32to64((xxh_u32)input_hi, XXH_PRIME32_2 - 1); + } + /* m128 ^= XXH_swap64(m128 >> 64); */ + m128.low64 ^= XXH_swap64(m128.high64); + + { /* 128x64 multiply: h128 = m128 * XXH_PRIME64_2; */ + XXH128_hash_t h128 = XXH_mult64to128(m128.low64, XXH_PRIME64_2); + h128.high64 += m128.high64 * XXH_PRIME64_2; + + h128.low64 = XXH3_avalanche(h128.low64); + h128.high64 = XXH3_avalanche(h128.high64); + return h128; + } } +} + +/* + * Assumption: `secret` size is >= XXH3_SECRET_SIZE_MIN + */ +XXH_FORCE_INLINE XXH128_hash_t +XXH3_len_0to16_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(len <= 16); + { if (len > 8) return XXH3_len_9to16_128b(input, len, secret, seed); + if (len >= 4) return XXH3_len_4to8_128b(input, len, secret, seed); + if (len) return XXH3_len_1to3_128b(input, len, secret, seed); + { XXH128_hash_t h128; + xxh_u64 const bitflipl = XXH_readLE64(secret+64) ^ XXH_readLE64(secret+72); + xxh_u64 const bitfliph = XXH_readLE64(secret+80) ^ XXH_readLE64(secret+88); + h128.low64 = XXH64_avalanche(seed ^ bitflipl); + h128.high64 = XXH64_avalanche( seed ^ bitfliph); + return h128; + } } +} + +/* + * A bit slower than XXH3_mix16B, but handles multiply by zero better. + */ +XXH_FORCE_INLINE XXH128_hash_t +XXH128_mix32B(XXH128_hash_t acc, const xxh_u8* input_1, const xxh_u8* input_2, + const xxh_u8* secret, XXH64_hash_t seed) +{ + acc.low64 += XXH3_mix16B (input_1, secret+0, seed); + acc.low64 ^= XXH_readLE64(input_2) + XXH_readLE64(input_2 + 8); + acc.high64 += XXH3_mix16B (input_2, secret+16, seed); + acc.high64 ^= XXH_readLE64(input_1) + XXH_readLE64(input_1 + 8); + return acc; +} + + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_len_17to128_128b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(16 < len && len <= 128); + + { XXH128_hash_t acc; + acc.low64 = len * XXH_PRIME64_1; + acc.high64 = 0; + if (len > 32) { + if (len > 64) { + if (len > 96) { + acc = XXH128_mix32B(acc, input+48, input+len-64, secret+96, seed); + } + acc = XXH128_mix32B(acc, input+32, input+len-48, secret+64, seed); + } + acc = XXH128_mix32B(acc, input+16, input+len-32, secret+32, seed); + } + acc = XXH128_mix32B(acc, input, input+len-16, secret, seed); + { XXH128_hash_t h128; + h128.low64 = acc.low64 + acc.high64; + h128.high64 = (acc.low64 * XXH_PRIME64_1) + + (acc.high64 * XXH_PRIME64_4) + + ((len - seed) * XXH_PRIME64_2); + h128.low64 = XXH3_avalanche(h128.low64); + h128.high64 = (XXH64_hash_t)0 - XXH3_avalanche(h128.high64); + return h128; + } + } +} + +XXH_NO_INLINE XXH128_hash_t +XXH3_len_129to240_128b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(128 < len && len <= XXH3_MIDSIZE_MAX); + + { XXH128_hash_t acc; + int const nbRounds = (int)len / 32; + int i; + acc.low64 = len * XXH_PRIME64_1; + acc.high64 = 0; + for (i=0; i<4; i++) { + acc = XXH128_mix32B(acc, + input + (32 * i), + input + (32 * i) + 16, + secret + (32 * i), + seed); + } + acc.low64 = XXH3_avalanche(acc.low64); + acc.high64 = XXH3_avalanche(acc.high64); + XXH_ASSERT(nbRounds >= 4); + for (i=4 ; i < nbRounds; i++) { + acc = XXH128_mix32B(acc, + input + (32 * i), + input + (32 * i) + 16, + secret + XXH3_MIDSIZE_STARTOFFSET + (32 * (i - 4)), + seed); + } + /* last bytes */ + acc = XXH128_mix32B(acc, + input + len - 16, + input + len - 32, + secret + XXH3_SECRET_SIZE_MIN - XXH3_MIDSIZE_LASTOFFSET - 16, + 0ULL - seed); + + { XXH128_hash_t h128; + h128.low64 = acc.low64 + acc.high64; + h128.high64 = (acc.low64 * XXH_PRIME64_1) + + (acc.high64 * XXH_PRIME64_4) + + ((len - seed) * XXH_PRIME64_2); + h128.low64 = XXH3_avalanche(h128.low64); + h128.high64 = (XXH64_hash_t)0 - XXH3_avalanche(h128.high64); + return h128; + } + } +} + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_hashLong_128b_internal(const void* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH3_f_accumulate_512 f_acc512, + XXH3_f_scrambleAcc f_scramble) +{ + XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[XXH_ACC_NB] = XXH3_INIT_ACC; + + XXH3_hashLong_internal_loop(acc, (const xxh_u8*)input, len, secret, secretSize, f_acc512, f_scramble); + + /* converge into final hash */ + XXH_STATIC_ASSERT(sizeof(acc) == 64); + XXH_ASSERT(secretSize >= sizeof(acc) + XXH_SECRET_MERGEACCS_START); + { XXH128_hash_t h128; + h128.low64 = XXH3_mergeAccs(acc, + secret + XXH_SECRET_MERGEACCS_START, + (xxh_u64)len * XXH_PRIME64_1); + h128.high64 = XXH3_mergeAccs(acc, + secret + secretSize + - sizeof(acc) - XXH_SECRET_MERGEACCS_START, + ~((xxh_u64)len * XXH_PRIME64_2)); + return h128; + } +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + */ +XXH_NO_INLINE XXH128_hash_t +XXH3_hashLong_128b_default(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, + const void* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; (void)secret; (void)secretLen; + return XXH3_hashLong_128b_internal(input, len, XXH3_kSecret, sizeof(XXH3_kSecret), + XXH3_accumulate_512, XXH3_scrambleAcc); +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + */ +XXH_NO_INLINE XXH128_hash_t +XXH3_hashLong_128b_withSecret(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, + const void* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; + return XXH3_hashLong_128b_internal(input, len, (const xxh_u8*)secret, secretLen, + XXH3_accumulate_512, XXH3_scrambleAcc); +} + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_hashLong_128b_withSeed_internal(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, + XXH3_f_accumulate_512 f_acc512, + XXH3_f_scrambleAcc f_scramble, + XXH3_f_initCustomSecret f_initSec) +{ + if (seed64 == 0) + return XXH3_hashLong_128b_internal(input, len, + XXH3_kSecret, sizeof(XXH3_kSecret), + f_acc512, f_scramble); + { XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE]; + f_initSec(secret, seed64); + return XXH3_hashLong_128b_internal(input, len, (const xxh_u8*)secret, sizeof(secret), + f_acc512, f_scramble); + } +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + */ +XXH_NO_INLINE XXH128_hash_t +XXH3_hashLong_128b_withSeed(const void* input, size_t len, + XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen) +{ + (void)secret; (void)secretLen; + return XXH3_hashLong_128b_withSeed_internal(input, len, seed64, + XXH3_accumulate_512, XXH3_scrambleAcc, XXH3_initCustomSecret); +} + +typedef XXH128_hash_t (*XXH3_hashLong128_f)(const void* XXH_RESTRICT, size_t, + XXH64_hash_t, const void* XXH_RESTRICT, size_t); + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_128bits_internal(const void* input, size_t len, + XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen, + XXH3_hashLong128_f f_hl128) +{ + XXH_ASSERT(secretLen >= XXH3_SECRET_SIZE_MIN); + /* + * If an action is to be taken if `secret` conditions are not respected, + * it should be done here. + * For now, it's a contract pre-condition. + * Adding a check and a branch here would cost performance at every hash. + */ + if (len <= 16) + return XXH3_len_0to16_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, seed64); + if (len <= 128) + return XXH3_len_17to128_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + if (len <= XXH3_MIDSIZE_MAX) + return XXH3_len_129to240_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + return f_hl128(input, len, seed64, secret, secretLen); +} + + +/* === Public XXH128 API === */ + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits(const void* input, size_t len) +{ + return XXH3_128bits_internal(input, len, 0, + XXH3_kSecret, sizeof(XXH3_kSecret), + XXH3_hashLong_128b_default); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH3_128bits_withSecret(const void* input, size_t len, const void* secret, size_t secretSize) +{ + return XXH3_128bits_internal(input, len, 0, + (const xxh_u8*)secret, secretSize, + XXH3_hashLong_128b_withSecret); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH3_128bits_withSeed(const void* input, size_t len, XXH64_hash_t seed) +{ + return XXH3_128bits_internal(input, len, seed, + XXH3_kSecret, sizeof(XXH3_kSecret), + XXH3_hashLong_128b_withSeed); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH128(const void* input, size_t len, XXH64_hash_t seed) +{ + return XXH3_128bits_withSeed(input, len, seed); +} + + +/* === XXH3 128-bit streaming === */ + +/* + * All the functions are actually the same as for 64-bit streaming variant. + * The only difference is the finalizatiom routine. + */ + +static void +XXH3_128bits_reset_internal(XXH3_state_t* statePtr, + XXH64_hash_t seed, + const void* secret, size_t secretSize) +{ + XXH3_64bits_reset_internal(statePtr, seed, secret, secretSize); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset(XXH3_state_t* statePtr) +{ + if (statePtr == NULL) return XXH_ERROR; + XXH3_128bits_reset_internal(statePtr, 0, XXH3_kSecret, XXH_SECRET_DEFAULT_SIZE); + return XXH_OK; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize) +{ + if (statePtr == NULL) return XXH_ERROR; + XXH3_128bits_reset_internal(statePtr, 0, secret, secretSize); + if (secret == NULL) return XXH_ERROR; + if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR; + return XXH_OK; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed) +{ + if (statePtr == NULL) return XXH_ERROR; + if (seed==0) return XXH3_128bits_reset(statePtr); + if (seed != statePtr->seed) XXH3_initCustomSecret(statePtr->customSecret, seed); + XXH3_128bits_reset_internal(statePtr, seed, NULL, XXH_SECRET_DEFAULT_SIZE); + return XXH_OK; +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_update(XXH3_state_t* state, const void* input, size_t len) +{ + return XXH3_update(state, (const xxh_u8*)input, len, + XXH3_accumulate_512, XXH3_scrambleAcc); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_digest (const XXH3_state_t* state) +{ + const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret; + if (state->totalLen > XXH3_MIDSIZE_MAX) { + XXH_ALIGN(XXH_ACC_ALIGN) XXH64_hash_t acc[XXH_ACC_NB]; + XXH3_digest_long(acc, state, secret); + XXH_ASSERT(state->secretLimit + XXH_STRIPE_LEN >= sizeof(acc) + XXH_SECRET_MERGEACCS_START); + { XXH128_hash_t h128; + h128.low64 = XXH3_mergeAccs(acc, + secret + XXH_SECRET_MERGEACCS_START, + (xxh_u64)state->totalLen * XXH_PRIME64_1); + h128.high64 = XXH3_mergeAccs(acc, + secret + state->secretLimit + XXH_STRIPE_LEN + - sizeof(acc) - XXH_SECRET_MERGEACCS_START, + ~((xxh_u64)state->totalLen * XXH_PRIME64_2)); + return h128; + } + } + /* len <= XXH3_MIDSIZE_MAX : short code */ + if (state->seed) + return XXH3_128bits_withSeed(state->buffer, (size_t)state->totalLen, state->seed); + return XXH3_128bits_withSecret(state->buffer, (size_t)(state->totalLen), + secret, state->secretLimit + XXH_STRIPE_LEN); +} + +/* 128-bit utility functions */ + +#include /* memcmp, memcpy */ + +/* return : 1 is equal, 0 if different */ +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API int XXH128_isEqual(XXH128_hash_t h1, XXH128_hash_t h2) +{ + /* note : XXH128_hash_t is compact, it has no padding byte */ + return !(memcmp(&h1, &h2, sizeof(h1))); +} + +/* This prototype is compatible with stdlib's qsort(). + * return : >0 if *h128_1 > *h128_2 + * <0 if *h128_1 < *h128_2 + * =0 if *h128_1 == *h128_2 */ +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API int XXH128_cmp(const void* h128_1, const void* h128_2) +{ + XXH128_hash_t const h1 = *(const XXH128_hash_t*)h128_1; + XXH128_hash_t const h2 = *(const XXH128_hash_t*)h128_2; + int const hcmp = (h1.high64 > h2.high64) - (h2.high64 > h1.high64); + /* note : bets that, in most cases, hash values are different */ + if (hcmp) return hcmp; + return (h1.low64 > h2.low64) - (h2.low64 > h1.low64); +} + + +/*====== Canonical representation ======*/ +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API void +XXH128_canonicalFromHash(XXH128_canonical_t* dst, XXH128_hash_t hash) +{ + XXH_STATIC_ASSERT(sizeof(XXH128_canonical_t) == sizeof(XXH128_hash_t)); + if (XXH_CPU_LITTLE_ENDIAN) { + hash.high64 = XXH_swap64(hash.high64); + hash.low64 = XXH_swap64(hash.low64); + } + memcpy(dst, &hash.high64, sizeof(hash.high64)); + memcpy((char*)dst + sizeof(hash.high64), &hash.low64, sizeof(hash.low64)); +} + +/*! @ingroup xxh3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH128_hashFromCanonical(const XXH128_canonical_t* src) +{ + XXH128_hash_t h; + h.high64 = XXH_readBE64(src); + h.low64 = XXH_readBE64(src->digest + 8); + return h; +} + +/* Pop our optimization override from above */ +#if XXH_VECTOR == XXH_AVX2 /* AVX2 */ \ + && defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \ + && defined(__OPTIMIZE__) && !defined(__OPTIMIZE_SIZE__) /* respect -O0 and -Os */ +# pragma GCC pop_options +#endif + +#endif /* XXH_NO_LONG_LONG */ + +/*! + * @} + */ +#endif /* XXH_IMPLEMENTATION */ + + +#if defined (__cplusplus) +} +#endif diff --git a/volatile/include/kvdk/volatile/comparator.hpp b/volatile/include/kvdk/volatile/comparator.hpp new file mode 100644 index 00000000..8eb7594a --- /dev/null +++ b/volatile/include/kvdk/volatile/comparator.hpp @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include +#include + +#include "types.hpp" + +namespace KVDK_NAMESPACE { +using StringView = pmem::obj::string_view; +using Comparator = + std::function; + +class ComparatorTable { + public: + // Register a string compare function to the table + // + // Return true on success, return false if comparator_name already existed + bool RegisterComparator(const StringView& comparator_name, + Comparator comp_func) { + std::string name(comparator_name.data(), comparator_name.size()); + if (comparator_table_.find(name) == comparator_table_.end()) { + comparator_table_.emplace(name, comp_func); + return true; + } else { + return false; + } + } + + // Return a registered comparator "comparator_name" on success, return nullptr + // if it's not existing + Comparator GetComparator(const StringView& comparator_name) { + std::string name(comparator_name.data(), comparator_name.size()); + auto iter = comparator_table_.find(name); + if (iter != comparator_table_.end()) { + return iter->second; + } + return nullptr; + }; + + private: + std::unordered_map comparator_table_; +}; +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/include/kvdk/volatile/configs.hpp b/volatile/include/kvdk/volatile/configs.hpp new file mode 100644 index 00000000..9b881105 --- /dev/null +++ b/volatile/include/kvdk/volatile/configs.hpp @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include + +#include "comparator.hpp" +#include "types.hpp" + +namespace KVDK_NAMESPACE { + +enum class LogLevel : uint8_t { + All = 0, + Debug, + Info, + Error, + None, +}; + +// Configs of created sorted collection +// For correctness of encoding, please add new config field in the end of the +// existing fields +struct SortedCollectionConfigs { + std::string comparator_name = "default"; + int index_with_hashtable = 1; +}; + +struct Configs { + // TODO: rename to concurrent internal threads + // + // Max number of concurrent threads read/write the kvdk instance internally. + // Set it to the number of the hyper-threads to get best performance + // + // Notice: you can call KVDK API with any number of threads, but if your + // parallel threads more than max_access_threads, the performance will be + // degraded due to synchronization cost + uint64_t max_access_threads = 64; + + // The number of bucket groups in the hash table. + // + // It should be 2^n and should smaller than 2^32. + // The original hash table size would be (kHashBucketSize(128 by default) * + // hash_bucket_num) + // More buckets means less write contention, faster hash + // search, but more internal memory fragmentation. + uint64_t hash_bucket_num = (1 << 27); + + // The number of buckets per hash slot. + // + // The hash slot is the minimum unit of write lock and hot-spot cache, less + // buckets in a slot means better hot spot read performance, less write + // contentions and more memory consumption + uint32_t num_buckets_per_slot = 1; + + // Time interval to do background work in seconds + // + // In KVDK, a background thread will regularly organize free space, + // frequent execution will lead to better space utilization, but more + // influence to foreground performance + double background_work_interval = 5.0; + + // Time interval that the background thread report memory usage by + // GlobalLogger. report_memory_usage_interval less than + // background_work_interval will be ignored. + double report_memory_usage_interval = 1000000.0; + + // Log information to show + LogLevel log_level = LogLevel::Info; + + // Optional optimization strategy for few large skiplists by multi-thread + // recovery a skiplist. The optimization can get better performance when + // having few large skiplists. Default is to close optimization. + bool opt_large_sorted_collection_recovery = false; + + // If a checkpoint is made in last open, recover the instance to the + // checkpoint version if this true + // + // Notice: If opening a backup instance, this will always be set to true + // during recovery. + bool recover_to_checkpoint = false; + + // If customer compare functions is used in a kvdk engine, these functions + // should be registered to the comparator before open engine + ComparatorTable comparator; + + // Background clean thread numbers. + uint64_t clean_threads = 8; + + // Set the memory nodes where volatile KV memory allocator binds to. + // There is no effect when the configured value is empty or invalid. + // Note: Currently, only the jemalloc allocator support memory binding. + // jemalloc is enabled by cmake option -DWITH_JEMALLOC=ON. + std::string dest_memory_nodes = ""; +}; + +struct WriteOptions { + WriteOptions(TTLType _ttl_time = kPersistTTL, bool _update_ttl = true) + : ttl_time(_ttl_time), update_ttl(_update_ttl) {} + + // expired time in milliseconod, should be kPersistTime for no expiration + // data, or a certain interge which be in the range [INT64_MIN , INT64_MAX]. + TTLType ttl_time; + + // determine whether to update expired time if key already existed + bool update_ttl; +}; + +} // namespace KVDK_NAMESPACE diff --git a/volatile/include/kvdk/volatile/engine.h b/volatile/include/kvdk/volatile/engine.h new file mode 100644 index 00000000..fd831a41 --- /dev/null +++ b/volatile/include/kvdk/volatile/engine.h @@ -0,0 +1,308 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +#include "types.h" + +typedef struct KVDKEngine KVDKEngine; +typedef struct KVDKConfigs KVDKConfigs; +typedef struct KVDKWriteOptions KVDKWriteOptions; +typedef struct KVDKWriteBatch KVDKWriteBatch; +typedef struct KVDKSortedIterator KVDKSortedIterator; +typedef struct KVDKListIterator KVDKListIterator; +typedef struct KVDKHashIterator KVDKHashIterator; +typedef struct KVDKSnapshot KVDKSnapshot; +typedef struct KVDKSortedCollectionConfigs KVDKSortedCollectionConfigs; +typedef struct KVDKRegex KVDKRegex; + +extern KVDKRegex* KVDKRegexCreate(char const* data, size_t len); +extern void KVDKRegexDestroy(KVDKRegex* re); + +extern KVDKConfigs* KVDKCreateConfigs(void); +extern void KVDKSetConfigs(KVDKConfigs* kv_config, uint64_t max_access_threads, + uint64_t hash_bucket_num, + uint32_t num_buckets_per_slot); +extern void KVDKConfigRegisterCompFunc( + KVDKConfigs* kv_config, const char* compara_name, size_t compara_len, + int (*compare)(const char* src, size_t src_len, const char* target, + size_t target_len)); +extern void KVDKDestroyConfigs(KVDKConfigs* kv_config); + +extern KVDKWriteOptions* KVDKCreateWriteOptions(void); +extern void KVDKDestroyWriteOptions(KVDKWriteOptions*); +extern void KVDKWriteOptionsSetTTLTime(KVDKWriteOptions*, int64_t); +extern void KVDKWriteOptionsSetUpdateTTL(KVDKWriteOptions* kv_options, + int update_ttl); + +extern KVDKSortedCollectionConfigs* KVDKCreateSortedCollectionConfigs(); +extern void KVDKSetSortedCollectionConfigs(KVDKSortedCollectionConfigs* configs, + const char* comp_func_name, + size_t comp_func_len, + int index_with_hashtable); +extern void KVDKDestroySortedCollectionConfigs( + KVDKSortedCollectionConfigs* configs); + +extern KVDKStatus KVDKOpen(const char* name, const KVDKConfigs* config, + FILE* log_file, KVDKEngine** engine); +extern KVDKStatus KVDKBackup(KVDKEngine* engine, const char* backup_path, + size_t backup_path_len, KVDKSnapshot* snapshot); +extern KVDKStatus KVDKRestore(const char* name, const char* backup_log, + const KVDKConfigs* config, FILE* log_file, + KVDKEngine** engine); +extern void KVDKCloseEngine(KVDKEngine* engine); +extern KVDKSnapshot* KVDKGetSnapshot(KVDKEngine* engine, int make_checkpoint); +extern void KVDKReleaseSnapshot(KVDKEngine* engine, KVDKSnapshot* snapshot); + +extern int KVDKRegisterCompFunc(KVDKEngine* engine, const char* compara_name, + size_t compara_len, + int (*compare)(const char* src, size_t src_len, + const char* target, + size_t target_len)); + +// For BatchWrite +extern KVDKWriteBatch* KVDKWriteBatchCreate(KVDKEngine* engine); +extern void KVDKWriteBatchDestory(KVDKWriteBatch* batch); +extern void KVDKWRiteBatchClear(KVDKWriteBatch* batch); +extern void KVDKWriteBatchStringPut(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* val_data, + size_t val_len); +extern void KVDKWriteBatchStringDelete(KVDKWriteBatch* batch, + char const* key_data, size_t key_len); +extern void KVDKWriteBatchSortedPut(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* field_data, + size_t field_len, char const* val_data, + size_t val_len); +extern void KVDKWriteBatchSortedDelete(KVDKWriteBatch* batch, + char const* key_data, size_t key_len, + char const* field_data, + size_t field_len); +extern void KVDKWriteBatchHashPut(KVDKWriteBatch* batch, char const* key_data, + size_t key_len, char const* field_data, + size_t field_len, char const* val_data, + size_t val_len); +extern void KVDKWriteBatchHashDelete(KVDKWriteBatch* batch, + char const* key_data, size_t key_len, + char const* field_data, size_t field_len); +extern KVDKStatus KVDKBatchWrite(KVDKEngine* engine, + KVDKWriteBatch const* batch); + +// For Anonymous Global Collection +extern KVDKStatus KVDKGet(KVDKEngine* engine, const char* key, size_t key_len, + size_t* val_len, char** val); +extern KVDKStatus KVDKPut(KVDKEngine* engine, const char* key, size_t key_len, + const char* val, size_t val_len, + const KVDKWriteOptions* write_option); +extern KVDKStatus KVDKDelete(KVDKEngine* engine, const char* key, + size_t key_len); + +// Modify value of existing key in the engine +// +// * modify_func: customized function to modify existing value of key. See +// definition of KVDKModifyFunc (types.h) for more details. +// * modify_args: customized arguments of modify_func. +// * free_func: function to free allocated space for new value in +// modify_func, pall NULL if not need to free +// +// Return Ok if modify success. +// Return Abort if modify function abort modifying. +// Return other non-Ok status on any error. +extern KVDKStatus KVDKModify(KVDKEngine* engine, const char* key, + size_t key_len, KVDKModifyFunc modify_func, + void* modify_args, KVDKFreeFunc free_func, + const KVDKWriteOptions* write_option); + +/// Sorted +/// ////////////////////////////////////////////////////////////////////// +extern KVDKStatus KVDKSortedCreate(KVDKEngine* engine, + const char* collection_name, + size_t collection_len, + KVDKSortedCollectionConfigs* configs); +extern KVDKStatus KVDKSortedDestroy(KVDKEngine* engine, + const char* collection_name, + size_t collection_len); +extern KVDKStatus KVDKSortedSize(KVDKEngine* engine, const char* collection, + size_t collection_len, size_t* size); +extern KVDKStatus KVDKSortedPut(KVDKEngine* engine, const char* collection, + size_t collection_len, const char* key, + size_t key_len, const char* val, + size_t val_len); +extern KVDKStatus KVDKSortedDelete(KVDKEngine* engine, const char* collection, + size_t collection_len, const char* key, + size_t key_len); +extern KVDKStatus KVDKSortedGet(KVDKEngine* engine, const char* collection, + size_t collection_len, const char* key, + size_t key_len, size_t* val_len, char** val); +extern KVDKSortedIterator* KVDKSortedIteratorCreate(KVDKEngine* engine, + const char* collection, + size_t collection_len, + KVDKSnapshot* snapshot, + KVDKStatus* s); +extern void KVDKSortedIteratorDestroy(KVDKEngine* engine, + KVDKSortedIterator* iterator); +extern void KVDKSortedIteratorSeekToFirst(KVDKSortedIterator* iter); +extern void KVDKSortedIteratorSeekToLast(KVDKSortedIterator* iter); +extern void KVDKSortedIteratorSeek(KVDKSortedIterator* iter, const char* str, + size_t str_len); +extern void KVDKSortedIteratorNext(KVDKSortedIterator* iter); +extern void KVDKSortedIteratorPrev(KVDKSortedIterator* iter); +extern unsigned char KVDKSortedIteratorValid(KVDKSortedIterator* iter); +extern void KVDKSortedIteratorKey(KVDKSortedIterator* iter, char** key, + size_t* key_len); +extern void KVDKSortedIteratorValue(KVDKSortedIterator* iter, char** value, + size_t* val_len); + +/// Hash ////////////////////////////////////////////////////////////////////// +extern KVDKStatus KVDKHashCreate(KVDKEngine* engine, char const* key_data, + size_t key_len); +extern KVDKStatus KVDKHashDestroy(KVDKEngine* engine, char const* key_data, + size_t key_len); +extern KVDKStatus KVDKHashLength(KVDKEngine* engine, char const* key_data, + size_t key_len, size_t* len); +extern KVDKStatus KVDKHashGet(KVDKEngine* engine, const char* key_data, + size_t key_len, const char* field_data, + size_t field_len, char** val_data, + size_t* val_len); +extern KVDKStatus KVDKHashPut(KVDKEngine* engine, const char* key_data, + size_t key_len, const char* field_data, + size_t field_len, const char* val_data, + size_t val_len); +extern KVDKStatus KVDKHashDelete(KVDKEngine* engine, const char* key_data, + size_t key_len, const char* field_data, + size_t field_len); +extern KVDKStatus KVDKHashModify(KVDKEngine* engine, const char* key_data, + size_t key_len, const char* field_data, + size_t field_len, KVDKModifyFunc modify_func, + void* args, KVDKFreeFunc free_func); + +/// HashIterator ////////////////////////////////////////////////////////////// +extern KVDKHashIterator* KVDKHashIteratorCreate(KVDKEngine* engine, + char const* key_data, + size_t key_len, + KVDKSnapshot* snapshot, + KVDKStatus* s); +extern void KVDKHashIteratorDestroy(KVDKEngine* engine, KVDKHashIterator* iter); +extern void KVDKHashIteratorPrev(KVDKHashIterator* iter); +extern void KVDKHashIteratorNext(KVDKHashIterator* iter); +extern void KVDKHashIteratorSeekToFirst(KVDKHashIterator* iter); +extern void KVDKHashIteratorSeekToLast(KVDKHashIterator* iter); +extern int KVDKHashIteratorIsValid(KVDKHashIterator* iter); +extern void KVDKHashIteratorGetKey(KVDKHashIterator* iter, char** field_data, + size_t* field_len); +extern void KVDKHashIteratorGetValue(KVDKHashIterator* iter, char** value_data, + size_t* value_len); +extern int KVDKHashIteratorMatchKey(KVDKHashIterator* iter, + KVDKRegex const* re); + +/// List ////////////////////////////////////////////////////////////////////// +extern KVDKStatus KVDKListCreate(KVDKEngine* engine, char const* key_data, + size_t key_len); +extern KVDKStatus KVDKListDestroy(KVDKEngine* engine, char const* key_data, + size_t key_len); +extern KVDKStatus KVDKListSize(KVDKEngine* engine, char const* key_data, + size_t key_len, size_t* len); +extern KVDKStatus KVDKListPushFront(KVDKEngine* engine, char const* key_data, + size_t key_len, char const* elem_data, + size_t elem_len); +extern KVDKStatus KVDKListPushBack(KVDKEngine* engine, char const* key_data, + size_t key_len, char const* elem_data, + size_t elem_len); +extern KVDKStatus KVDKListPopFront(KVDKEngine* engine, char const* key_data, + size_t key_len, char** elem_data, + size_t* elem_len); +extern KVDKStatus KVDKListPopBack(KVDKEngine* engine, char const* key_data, + size_t key_len, char** elem_data, + size_t* elem_len); +extern KVDKStatus KVDKListBatchPushFront(KVDKEngine* engine, + char const* key_data, size_t key_len, + char const* const* elems_data, + size_t const* elems_len, + size_t elems_cnt); +extern KVDKStatus KVDKListBatchPushBack(KVDKEngine* engine, + char const* key_data, size_t key_len, + char const* const* elems_data, + size_t const* elems_len, + size_t elems_cnt); +extern KVDKStatus KVDKListBatchPopFront( + KVDKEngine* engine, char const* key_data, size_t key_len, size_t n, + void (*cb)(char const* elem_data, size_t elem_len, void* args), void* args); +extern KVDKStatus KVDKListBatchPopBack( + KVDKEngine* engine, char const* key_data, size_t key_len, size_t n, + void (*cb)(char const* elem_data, size_t elem_len, void* args), void* args); +extern KVDKStatus KVDKListMove(KVDKEngine* engine, char const* src_data, + size_t src_len, int src_pos, + char const* dst_data, size_t dst_len, + int dst_pos, char** elem_data, size_t* elem_len); +extern KVDKStatus KVDKListInsertAt(KVDKEngine* engine, char const* list_name, + size_t list_name_len, char const* elem_data, + size_t elem_len, long index); +extern KVDKStatus KVDKListInsertBefore(KVDKEngine* engine, + char const* list_name, + size_t list_name_len, + char const* elem_data, size_t elem_len, + char const* pos_elem, + size_t pos_elem_len); +extern KVDKStatus KVDKListInsertAfter(KVDKEngine* engine, char const* list_name, + size_t list_name_len, + char const* elem_data, size_t elem_len, + char const* pos_elem, + size_t pos_elem_len); +extern KVDKStatus KVDKListErase(KVDKEngine* engine, char const* list_name, + size_t list_len, long index, char** elem_data, + size_t* elem_len); +extern KVDKStatus KVDKListReplace(KVDKEngine* engine, char const* list_name, + size_t list_name_len, long index, + char const* elem, size_t elem_len); +/// ListIterator ////////////////////////////////////////////////////////////// +extern KVDKListIterator* KVDKListIteratorCreate(KVDKEngine* engine, + char const* key_data, + size_t key_len, KVDKStatus* s); +extern void KVDKListIteratorDestroy(KVDKEngine* engine, KVDKListIterator* iter); +extern void KVDKListIteratorPrev(KVDKListIterator* iter); +extern void KVDKListIteratorNext(KVDKListIterator* iter); +extern void KVDKListIteratorPrevElem(KVDKListIterator* iter, + char const* elem_data, size_t elem_len); +extern void KVDKListIteratorNextElem(KVDKListIterator* iter, + char const* elem_data, size_t elem_len); +extern void KVDKListIteratorSeekToFirst(KVDKListIterator* iter); +extern void KVDKListIteratorSeekToLast(KVDKListIterator* iter); +extern void KVDKListIteratorSeekToFirstElem(KVDKListIterator* iter, + char const* elem_data, + size_t elem_len); +extern void KVDKListIteratorSeekToLastElem(KVDKListIterator* iter, + char const* elem_data, + size_t elem_len); +extern void KVDKListIteratorSeekPos(KVDKListIterator* iter, long pos); +extern int KVDKListIteratorIsValid(KVDKListIterator* iter); +extern void KVDKListIteratorGetValue(KVDKListIterator* iter, char** elem_data, + size_t* elem_len); + +/* ttl_time is negetive or positive number, If ttl_time == INT64_MAX, + * the key is persistent; If ttl_time <=0, the key is expired immediately. + */ +extern KVDKStatus KVDKExpire(KVDKEngine* engine, const char* str, + size_t str_len, int64_t ttl_time); +/* ttl_time is INT64_MAX and return Status::Ok if the key is persist + * ttl_time is 0 and return Status::NotFound if the key is expired or doesnot + * exist. + * ttl_time is certain positive number and return Status::Ok if the key hasn't + * expired and exist. + */ +extern KVDKStatus KVDKGetTTL(KVDKEngine* engine, const char* str, + size_t str_len, int64_t* ttl_time); + +extern KVDKStatus KVDKTypeOf(KVDKEngine* engine, char const* key_data, + size_t key_len, KVDKValueType* type); + +#ifdef __cplusplus +} /* end extern "C" */ +#endif // extern "C" diff --git a/volatile/include/kvdk/volatile/engine.hpp b/volatile/include/kvdk/volatile/engine.hpp new file mode 100644 index 00000000..3fb01b08 --- /dev/null +++ b/volatile/include/kvdk/volatile/engine.hpp @@ -0,0 +1,514 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include +#include + +#include "comparator.hpp" +#include "configs.hpp" +#include "iterator.hpp" +#include "snapshot.hpp" +#include "types.hpp" +#include "write_batch.hpp" + +namespace kvdk = KVDK_NAMESPACE; + +namespace KVDK_NAMESPACE { +// This is the abstraction of a persistent KVDK instance +class Engine { + public: + // Open a new KVDK instance or restore a existing KVDK instance + // + // Args: + // * engine_path: indicates the dir path that persist the instance + // * engine_ptr: store the pointer to restored instance + // * configs: engine configs4 + // * log_file: file to print out runtime logs + // + // Return: + // Return Status::Ok on sucess, return other status for any error + // + // To close the instance, just delete *engine_ptr. + static Status Open(const StringView engine_path, Engine** engine_ptr, + const Configs& configs, FILE* log_file = stdout); + + // Restore a KVDK instance from a backup log file. + // + // Args: + // * engine_path: indicates the dir path that persist the instance + // * backup_log: the backup log file restored from + // * engine_ptr: store the pointer to restored instance + // * configs: engine configs + // * log_file: file to print out runtime logs + // + // Return: + // Return Status::Ok on sucess, return other status for any error + // + // Notice: + // "engine_path" should be an empty dir + static Status Restore(const StringView engine_path, + const StringView backup_log, Engine** engine_ptr, + const Configs& configs, FILE* log_file = stdout); + + // Get type of key, it can be String, SortedCollection, HashCollection or List + // + // Return: + // Status::Ok and store type to "*type" on success + // Status::NotFound if key does not exist + virtual Status TypeOf(StringView key, ValueType* type) = 0; + + // Insert a STRING-type KV to set "key" to hold "value". + // + // Args: + // *options: customized write options + // + // Return: + // Status Ok on success . + // Status::WrongType if key exists but is a collection. + // Status::MemoryOverflow if DRAM exhausted. + virtual Status Put(const StringView key, const StringView value, + const WriteOptions& options = WriteOptions()) = 0; + + // Search the STRING-type KV of "key" in the kvdk instance. + // + // Return: + // Return Status::Ok and store the corresponding value to *value on success. + // Return Status::NotFound if the "key" does not exist. + virtual Status Get(const StringView key, std::string* value) = 0; + + // Remove STRING-type KV of "key". + // + // Return: + // Status::Ok on success or the "key" did not exist + // Status::WrongType if key exists but is a collection type + // Status::MemoryOverflow if DRAM exhausted. + virtual Status Delete(const StringView key) = 0; + + // Modify value of existing key in the engine + // + // Args: + // * modify_func: customized function to modify existing value of key. See + // definition of ModifyFunc (types.hpp) for more details. + // * modify_args: customized arguments of modify_func. + // + // Return: + // Return Status::Ok if modify success. + // Return Status::Abort if modify function abort modifying. + // Return other non-Ok status on any error. + virtual Status Modify(const StringView key, ModifyFunc modify_func, + void* modify_args, + const WriteOptions& options = WriteOptions()) = 0; + + // Atomically do a batch of operations (Put or Delete) to the instance, these + // operations either all succeed, or all fail. The data will be rollbacked if + // the instance crash during a batch write + // + // Return: + // Status::Ok on success + // Status::NotFound if a collection operated by the batch does not exist + // Status::MemoryOverflow if DRAM exhausted. + // + // Notice: + // BatchWrite has no isolation guaranteed, if you need it, you should use + // Transaction API + virtual Status BatchWrite(std::unique_ptr const& batch) = 0; + + // Create a write batch for BatchWrite operation + virtual std::unique_ptr WriteBatchCreate() = 0; + + // time to *expired_time on success. + // + // Args: + // * key: STRING-type key or collection name to search. + // * ttl_time: store TTL result. + // + // Return: + // Status::Ok and store ttl time to *ttl_time on success + // Status::NotFound if key is expired or does not exist + virtual Status GetTTL(const StringView key, int64_t* ttl_time) = 0; + + // Set ttl_time for STRING-type or Collection type data + // + // Args: + // * key: STRING-type key or collection name to set ttl_time. + // * ttl_time: ttl time to set. if ttl_time == kPersistTTL, the name will not + // be expired. If ttl_time <=0, the name is expired immediately. + // + // Return: + // Status::Ok on success. + // Status::NotFound if key does not exist. + virtual Status Expire(const StringView key, int64_t ttl_time) = 0; + + // Create a empty sorted collection with configs. You should always create + // collection before you do any operations on it + // + // Args: + // * configs: customized config of creating collection + // + // Return: + // Status::Ok on success + // Status::Existed if sorted collection already existed + // Status::WrongType if collection existed but not a sorted collection + // Status::MemoryOverflow if DRAM exhausted. + virtual Status SortedCreate( + const StringView collection, + const SortedCollectionConfigs& configs = SortedCollectionConfigs()) = 0; + + // Destroy a sorted collection + // Return: + // Status::Ok on success + // Status::WrongType if collection existed but not a sorted collection + // Status::MemoryOverflow if runtime/KV memory exhausted + virtual Status SortedDestroy(const StringView collection) = 0; + + // Get number of elements in a sorted collection + // + // Return: + // Status::Ok on success + // Status::NotFound if collection not exist + virtual Status SortedSize(const StringView collection, size_t* size) = 0; + + // Insert a KV to set "key" in sorted collection "collection" + // to hold "value" + // Return: + // Status::Ok on success. + // Status::NotFound if collection not exist. + // Status::WrongType if collection exists but is not a sorted collection. + // Status::MemoryOverflow if DRAM exhausted. + virtual Status SortedPut(const StringView collection, const StringView key, + const StringView value) = 0; + + // Search the KV of "key" in sorted collection "collection" + // + // Return: + // Status::Ok and store the corresponding value to *value on success. + // Status::NotFound If the "collection" or "key" does not exist. + virtual Status SortedGet(const StringView collection, const StringView key, + std::string* value) = 0; + + // Remove KV of "key" in the sorted collection "collection". + // + // Return: + // Status::Ok on success or key not existed in collection + // Status::NotFound if collection not exist + // Status::WrongType if collection exists but is not a sorted collection. + // Status::MemoryOverflow if DRAM exhausted. + virtual Status SortedDelete(const StringView collection, + const StringView key) = 0; + + // Create a KV iterator on sorted collection "collection", which is able to + // sequentially iterate all KVs in the "collection". + // + // Args: + // * snapshot: iterator will iterate all elems a t "snapshot" version, if + // snapshot is nullptr, then a internal snapshot will be created at current + // version and the iterator will be created on it + // * status: store operation status if not null + // + // Return: + // Return A pointer to iterator on success. + // Return nullptr if collection not exist or any other errors, and store error + // status to "status" + // + // Notice: + // 1. Iterator will be invalid after the passed snapshot is released + // 2. Please release the iterator as soon as it is not needed, as the holding + // snapshot will forbid newer data being freed + virtual SortedIterator* SortedIteratorCreate(const StringView collection, + Snapshot* snapshot = nullptr, + Status* s = nullptr) = 0; + + // Release a sorted iterator and its holding resouces + virtual void SortedIteratorRelease(SortedIterator*) = 0; + + /// List APIs /////////////////////////////////////////////////////////////// + + // Create an empty List. + // Return: + // Status::WrongType if list name existed but is not a List. + // Status::Existed if a List named list already exists. + // Status::MemoryOverflow if DRAM exhausted. + // Status::Ok if successfully created the List. + virtual Status ListCreate(StringView list) = 0; + + // Destroy a List associated with key + // Return: + // Status::WrongType if list name is not a List. + // Status::Ok if successfully destroyed the List or the List not existed + // Status::MemoryOverflow if DRAM exhausted + virtual Status ListDestroy(StringView list) = 0; + + // Total elements in List. + // Return: + // Status::InvalidDataSize if list name is too long + // Status::WrongType if list name is not a List. + // Status::NotFound if list does not exist or has expired. + // Status::Ok and length of List if List exists. + virtual Status ListSize(StringView list, size_t* sz) = 0; + + // Push element as first element of List + // Return: + // Status::InvalidDataSize if list name or elem is too long + // Status::WrongType if list name is not a List. + // Status::MemoryOverflow if DRAM exhausted. + // Status::Ok if operation succeeded. + virtual Status ListPushFront(StringView list, StringView elem) = 0; + + // Push element as last element of List + // Return: + // Status::InvalidDataSize if list name or elem is too long + // Status::WrongType if list name in the instance is not a List. + // Status::MemoryOverflow if DRAM exhausted. + // Status::Ok if operation succeeded. + virtual Status ListPushBack(StringView list, StringView elem) = 0; + + // Pop first element of list + // Return: + // Status::InvalidDataSize if list name is too long + // Status::WrongType if list is not a List. + // Status::NotFound if list does not exist or has expired. + // Status::Ok and element if operation succeeded. + virtual Status ListPopFront(StringView list, std::string* elem) = 0; + + // Pop last element of List + // Return: + // Status::InvalidDataSize if list name is too long + // Status::WrongType if list is not a List. + // Status::NotFound if list does not exist or has expired. + // Status::Ok and element if operation succeeded. + virtual Status ListPopBack(StringView list, std::string* elem) = 0; + + // Push multiple elements to the front of List + // Return: + // Status::InvalidDataSize if list name or elem is too long + // Status::WrongType if list is not a List. + // Status::MemoryOverflow if DRAM exhausted. + // Status::Ok if operation succeeded. + virtual Status ListBatchPushFront(StringView list, + std::vector const& elems) = 0; + virtual Status ListBatchPushFront(StringView list, + std::vector const& elems) = 0; + + // Push multiple elements to the back of List + // Return: + // Status::InvalidDataSize if list or elem is too long + // Status::WrongType if list name is not a List. + // Status::MemoryOverflow if DRAM exhausted. + // Status::Ok if operation succeeded. + virtual Status ListBatchPushBack(StringView list, + std::vector const& elems) = 0; + virtual Status ListBatchPushBack(StringView list, + std::vector const& elems) = 0; + + // Pop first N element of List + // Return: + // Status::InvalidDataSize if list is too long + // Status::WrongType if list is not a List. + // Status::NotFound if list does not exist or has expired. + // Status::Ok and element if operation succeeded. + virtual Status ListBatchPopFront(StringView list, size_t n, + std::vector* elems) = 0; + + // Pop last N element of List + // Return: + // Status::InvalidDataSize if list is too long + // Status::WrongType if list is not a List. + // Status::NotFound if list does not exist or has expired. + // Status::Ok and element if operation succeeded. + virtual Status ListBatchPopBack(StringView list, size_t n, + std::vector* elems) = 0; + + // Move element in src List at src_pos to dst List at dst_pos + // src_pos and dst_pos can only be 0, indicating List front, + // or -1, indicating List back. + // Return: + // Status::WrongType if src or dst is not a List. + // Status::NotFound if list or element not exist + // Status::Ok and moved element if operation succeeded. + virtual Status ListMove(StringView src_list, ListPos src_pos, + StringView dst_list, ListPos dst_pos, + std::string* elem) = 0; + + // Insert a element to a list at index, the index can be positive or + // negative + // Return: + // Status::InvalidDataSize if list name is too large + // Status::WrongType if collection is not a list + // Status::NotFound if collection not found or index is beyond list size + // Status::Ok if operation succeeded + virtual Status ListInsertAt(StringView list, StringView elem, long index) = 0; + + // Insert an element before element "pos" in list "collection" + // Return: + // Status::InvalidDataSize if elem is too long. + // Status::MemoryOverflow if DRAM exhausted. + // Status::NotFound if List of the ListIterator has expired or been + // deleted, or "pos" not exist in the list + // Status::Ok if operation succeeded. + virtual Status ListInsertBefore(StringView list, StringView elem, + StringView pos) = 0; + + // Insert an element after element "pos" in list "collection" + // Return: + // Status::InvalidDataSize if elem is too long. + // Status::MemoryOverflow if DRAM exhausted. + // Status::NotFound if List of the ListIterator has expired or been + // deleted, or "pos" not exist in the list + // Status::Ok if operation succeeded. + virtual Status ListInsertAfter(StringView list, StringView elem, + StringView pos) = 0; + + // Remove the element at index + // Return: + // Status::NotFound if the index beyond list size. + // Status::Ok if operation succeeded, and store removed elem in "elem" + virtual Status ListErase(StringView list, long index, std::string* elem) = 0; + + // Replace the element at index + // Return: + // Status::InvalidDataSize if elem is too long + // Status::NotFound if if the index beyond list size. + // Status::Ok if operation succeeded. + virtual Status ListReplace(StringView list, long index, StringView elem) = 0; + + // Create a KV iterator on list "list", which is able to iterate all elems in + // the list + // + // Args: + // * snapshot: iterator will iterate all elems a t "snapshot" version, if + // snapshot is nullptr, then a internal snapshot will be created at current + // version and the iterator will be created on it + // * status: store operation status if not null + // + // Return: + // Return A pointer to iterator on success. + // Return nullptr if list not exist or any other errors, and store error + // status to "status" + // + // Notice: + // 1. Iterator will be invalid after the passed snapshot is released + // 2. Please release the iterator as soon as it is not needed, as the holding + // snapshot will forbid newer data being freed + virtual ListIterator* ListIteratorCreate(StringView list, + Snapshot* snapshot = nullptr, + Status* status = nullptr) = 0; + // Release a ListIterator and its holding resources + virtual void ListIteratorRelease(ListIterator*) = 0; + + /// Hash APIs /////////////////////////////////////////////////////////////// + + // Create a empty hash collection. You should always create collection before + // you do any operations on it + // + // Return: + // Status::Ok on success + // Status::Existed if hash collection already existed + // Status::WrongType if collection existed but not a hash collection + // Status::MemoryOverflow if DRAM exhausted + virtual Status HashCreate(StringView collection) = 0; + + // Destroy a hash collection + // Return: + // Status::Ok on success + // Status::WrongType if collection existed but not a hash collection + // Status::MemoryOverflow if DRAM exhausted + virtual Status HashDestroy(StringView collection) = 0; + + // Get number of elements in a hash collection + // + // Return: + // Status::Ok on success + // Status::NotFound if collection not exist + virtual Status HashSize(StringView collection, size_t* len) = 0; + + // Search the KV of "key" in hash collection "collection" + // + // Return: + // Status::Ok and store the corresponding value to *value on success. + // Status::NotFound If the "collection" or "key" does not exist. + virtual Status HashGet(StringView collection, StringView key, + std::string* value) = 0; + + // Insert a KV to set "key" in hash collection "collection" + // to hold "value" + // Return: + // Status::Ok on success. + // Status::NotFound if collection not exist. + // Status::WrongType if collection exists but is not a hash collection. + // Status::MemoryOverflow if DRAM exhausted + virtual Status HashPut(StringView collection, StringView key, + StringView value) = 0; + + // Remove KV of "key" in the hash collection "collection". + // + // Return: + // Status::Ok on success or key not existed in collection + // Status::NotFound if collection not exist + // Status::WrongType if collection exists but is not a hash collection. + // Status::MemoryOverflow if DRAM exhausted. + virtual Status HashDelete(StringView collection, StringView key) = 0; + + // Modify value of a existing key in a hash collection + // + // Args: + // * modify_func: customized function to modify existing value of key. See + // definition of ModifyFunc (types.hpp) for more details. + // * modify_args: customized arguments of modify_func. + // + // Return: + // Return Status::Ok if modify success. + // Return Status::Abort if modify function abort modifying. + // Return other non-Ok status on any error. + virtual Status HashModify(StringView collection, StringView key, + ModifyFunc modify_func, void* cb_args) = 0; + + // Create a KV iterator on hash collection "collection", which is able to + // iterate all elems in the collection at "snapshot" version, if snapshot is + // nullptr, then a internal snapshot will be created at current version and + // the iterator will be created on it + // + // Notice: + // 1. Iterator will be invalid after the passed snapshot is released + // 2. Please release the iterator as soon as it is not needed, as the holding + // snapshot will forbid newer data being freed + virtual HashIterator* HashIteratorCreate(StringView collection, + Snapshot* snapshot = nullptr, + Status* s = nullptr) = 0; + virtual void HashIteratorRelease(HashIterator*) = 0; + + /// Other /////////////////////////////////////////////////////////////////// + + // Get a snapshot of the instance at this moment. + // If set make_checkpoint to true, a persistent checkpoint will be made until + // this snapshot is released. You can recover KVDK instance to the checkpoint + // version during recovery, then the checkpoint will be removed. + // + // Notice: + // 1. You can maintain multiple snapshot but only the last checkpoint. + // 2. Please release the snapshot as soon as it is not needed, as it will + // forbid newer data being freed + virtual Snapshot* GetSnapshot(bool make_checkpoint) = 0; + + // Make a backup on "snapshot" to "backup_log" + virtual Status Backup(const pmem::obj::string_view backup_log, + const Snapshot* snapshot) = 0; + + // Release a snapshot of the instance + virtual void ReleaseSnapshot(const Snapshot*) = 0; + + // Register a customized comparator to the engine on runtime + // + // Return: + // Return true on success + // Return false if a comparator of comparator_name already existed + virtual bool registerComparator(const StringView& comparator_name, + Comparator) = 0; + + // Close the instance on exit. + virtual ~Engine() = 0; +}; + +} // namespace KVDK_NAMESPACE diff --git a/volatile/include/kvdk/volatile/iterator.hpp b/volatile/include/kvdk/volatile/iterator.hpp new file mode 100644 index 00000000..10e7e981 --- /dev/null +++ b/volatile/include/kvdk/volatile/iterator.hpp @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +#include + +#include "types.hpp" + +namespace KVDK_NAMESPACE { + +class SortedIterator { + public: + virtual void Seek(const std::string& key) = 0; + + virtual void SeekToFirst() = 0; + + virtual void SeekToLast() = 0; + + virtual bool Valid() = 0; + + virtual void Next() = 0; + + virtual void Prev() = 0; + + virtual std::string Key() = 0; + + virtual std::string Value() = 0; + + virtual ~SortedIterator() = default; +}; + +class ListIterator { + public: + virtual void Seek(long index) = 0; + + virtual void SeekToFirst() = 0; + + virtual void SeekToFirst(StringView elem) = 0; + + virtual void SeekToLast() = 0; + + virtual void SeekToLast(StringView elem) = 0; + + virtual void Next() = 0; + + virtual void Next(StringView elem) = 0; + + virtual void Prev() = 0; + + virtual void Prev(StringView elem) = 0; + + virtual bool Valid() const = 0; + + virtual std::string Value() const = 0; + + virtual ~ListIterator() = default; +}; + +class HashIterator { + public: + virtual void SeekToFirst() = 0; + + virtual void SeekToLast() = 0; + + virtual bool Valid() const = 0; + + virtual void Next() = 0; + + virtual void Prev() = 0; + + virtual std::string Key() const = 0; + + virtual std::string Value() const = 0; + + virtual bool MatchKey(std::regex const& re) = 0; + + virtual ~HashIterator() = default; +}; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/include/kvdk/volatile/snapshot.hpp b/volatile/include/kvdk/volatile/snapshot.hpp new file mode 100644 index 00000000..91b29622 --- /dev/null +++ b/volatile/include/kvdk/volatile/snapshot.hpp @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#pragma once + +namespace KVDK_NAMESPACE { + +// A snapshot indicates a immutable view of a KVDK engine at a certain time +struct Snapshot {}; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/include/kvdk/volatile/types.h b/volatile/include/kvdk/volatile/types.h new file mode 100644 index 00000000..ab0e1f9e --- /dev/null +++ b/volatile/include/kvdk/volatile/types.h @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ +#pragma once + +#include + +#define KVDK_MODIFY_WRITE 0 +#define KVDK_MODIFY_DELETE 1 +#define KVDK_MODIFY_ABORT 2 +#define KVDK_MODIFY_NOOP 3 + +#define KVDK_LIST_FRONT 0 +#define KVDK_LIST_BACK 1 + +// Customized modify function used in KVDKModify, indicate how to modify +// existing value +// +// Below is args of the function, "input" is passed by KVDK engine, and the +// function is responsible to fill outputs in "output" args. +// +// *(input) old_val: existing value of key, or nullptr if key not exist +// *(input) old_val_len: length of "old_val" +// *(output) new_val: store new value after modifying. It's responsible of +// KVDKModifyFunc to allocate space for *new_val and fill modify result here if +// the function returns KVDK_MODIFY_WRITE. +// *(output) new_val_len: length of "new_val", It's responsible of +// KVDKModifyFunc to fill it +// * args: customer args +// +// return KVDK_MODIFY_WRITE indicates to update existing value to +// "new_value" +// return KVDK_MODIFY_DELETE indicates to delete the kv from engine +// return KVDK_MODIFY_ABORT indicates the existing kv should not be +// modified and abort the operation +typedef int (*KVDKModifyFunc)(const char* old_val, size_t old_val_len, + char** new_val, size_t* new_val_len, void* args); +// Used in KVDKModify, indicate how to free allocated space in KVDKModifyFunc +typedef void (*KVDKFreeFunc)(void*); + +#define GENERATE_ENUM(ENUM) ENUM, +#define GENERATE_STRING(STRING) #STRING, + +#define KVDK_TYPES(GEN) \ + GEN(String) \ + GEN(SortedCollection) \ + GEN(HashCollection) \ + GEN(List) + +typedef enum { KVDK_TYPES(GENERATE_ENUM) } KVDKValueType; + +__attribute__((unused)) static char const* KVDKValueTypeString[] = { + KVDK_TYPES(GENERATE_STRING)}; + +#define KVDK_STATUS(GEN) \ + GEN(Ok) \ + GEN(NotFound) \ + GEN(Outdated) \ + GEN(WrongType) \ + GEN(Existed) \ + GEN(OperationFail) \ + GEN(OutOfRange) \ + GEN(MemoryOverflow) \ + GEN(NotSupported) \ + GEN(InvalidBatchSize) \ + GEN(InvalidDataSize) \ + GEN(InvalidArgument) \ + GEN(IOError) \ + GEN(InvalidConfiguration) \ + GEN(Fail) \ + GEN(Abort) + +typedef enum { KVDK_STATUS(GENERATE_ENUM) } KVDKStatus; + +__attribute__((unused)) static char const* KVDKStatusStrings[] = { + KVDK_STATUS(GENERATE_STRING)}; diff --git a/volatile/include/kvdk/volatile/types.hpp b/volatile/include/kvdk/volatile/types.hpp new file mode 100644 index 00000000..1d99c922 --- /dev/null +++ b/volatile/include/kvdk/volatile/types.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +#include "libpmemobj++/string_view.hpp" +#include "types.h" + +namespace KVDK_NAMESPACE { +using StringView = pmem::obj::string_view; +using CollectionIDType = std::uint64_t; +using IndexType = std::int64_t; + +using UnixTimeType = std::int64_t; +using ExpireTimeType = UnixTimeType; +using TTLType = std::int64_t; + +using Status = KVDKStatus; +using ValueType = KVDKValueType; + +enum class ListPos : int { + Front = KVDK_LIST_FRONT, + Back = KVDK_LIST_BACK, +}; + +enum class ModifyOperation : int { + Write = KVDK_MODIFY_WRITE, + Delete = KVDK_MODIFY_DELETE, + Abort = KVDK_MODIFY_ABORT, + Noop = KVDK_MODIFY_NOOP +}; + +// Customized modify function used in Engine::Modify, indicate how to modify +// existing value +// +// Below is args of the function, "input" is passed by KVDK engine, and the +// function is responsible to fill outputs in "output" args. +// +// *(input) key: associated key of modify operation +// *(input) old_value: existing value of key, or nullptr if key not exist +// *(output) new_value: store new value after modifying "existing_value". Caller +// is responsible to fill the modify result here if the function returns +// ModifyOperation::Write. +// * args: customer args +// +// return ModifyOperation::Write indicates to update existing value to +// "new_value" +// return ModifyOperation::Delete indicates to delete the kv from engine +// return ModifyOperation::Abort indicates the existing kv should not be +// modified and abort the operation +using ModifyFunc = std::function; + +constexpr ExpireTimeType kPersistTime = INT64_MAX; +constexpr TTLType kPersistTTL = INT64_MAX; +constexpr TTLType kInvalidTTL = 0; +constexpr uint32_t kMinMemoryBlockSize = 32; +} // namespace KVDK_NAMESPACE + +#if !__cpp_lib_string_view +#include + +#include + +template +std::basic_ostream& operator<<( + std::basic_ostream& out, + pmem::obj::basic_string_view sv) { + return std::__ostream_insert(out, sv.data(), sv.size()); +} + +namespace std { +template <> +struct hash { + size_t operator()(pmem::obj::string_view const& sv) const noexcept { + return _Hash_impl::hash(sv.data(), sv.size()); + } +}; +} // namespace std + +#endif // !__cpp_lib_string_view diff --git a/volatile/include/kvdk/volatile/write_batch.hpp b/volatile/include/kvdk/volatile/write_batch.hpp new file mode 100644 index 00000000..b156dff1 --- /dev/null +++ b/volatile/include/kvdk/volatile/write_batch.hpp @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ +#pragma once + +#include +#include + +#include "types.hpp" + +namespace KVDK_NAMESPACE { + +class WriteBatch { + public: + virtual void StringPut(const StringView key, const StringView value) = 0; + virtual void StringDelete(const StringView key) = 0; + + virtual void SortedPut(const StringView collection, const StringView key, + const StringView value) = 0; + virtual void SortedDelete(const StringView collection, + const StringView key) = 0; + + virtual void HashPut(const StringView collection, const StringView key, + const StringView value) = 0; + virtual void HashDelete(const StringView collection, + const StringView key) = 0; + + virtual void Clear() = 0; + + virtual size_t Size() const = 0; + + virtual ~WriteBatch() = default; +}; + +} // namespace KVDK_NAMESPACE \ No newline at end of file diff --git a/volatile/java/CMakeLists.txt b/volatile/java/CMakeLists.txt new file mode 100644 index 00000000..045864dd --- /dev/null +++ b/volatile/java/CMakeLists.txt @@ -0,0 +1,194 @@ +cmake_minimum_required(VERSION 3.4) + +if(${CMAKE_VERSION} VERSION_LESS "3.11.4") + message("Please consider switching to CMake 3.11.4 or newer") +endif() + +set(CMAKE_JAVA_COMPILE_FLAGS -source 7) + +set(KVDKJNI_VERSION 1.0.0-SNAPSHOT) +set(KVDKJNI_JAR_FILE kvdkjni-${KVDKJNI_VERSION}.jar) + +set(JNI_NATIVE_SOURCES + kvdkjni/configs.cc + kvdkjni/engine.cc + kvdkjni/iterator.cc + kvdkjni/native_bytes_handle.cc + kvdkjni/write_batch.cc +) + +set(JAVA_MAIN_SOURCES + src/main/java/io/pmem/kvdk/AbstractNativeReference.java + src/main/java/io/pmem/kvdk/Configs.java + src/main/java/io/pmem/kvdk/Engine.java + src/main/java/io/pmem/kvdk/Iterator.java + src/main/java/io/pmem/kvdk/Status.java + src/main/java/io/pmem/kvdk/KVDKException.java + src/main/java/io/pmem/kvdk/KVDKObject.java + src/main/java/io/pmem/kvdk/NativeBytesHandle.java + src/main/java/io/pmem/kvdk/NativeLibraryLoader.java + src/main/java/io/pmem/kvdk/WriteBatch.java + src/main/java/io/pmem/kvdk/WriteOptions.java +) + +set(JAVA_TEST_SOURCES + src/test/java/io/pmem/kvdk/ConfigsTest.java + src/test/java/io/pmem/kvdk/EngineTest.java + src/test/java/io/pmem/kvdk/EngineTestBase.java + src/test/java/io/pmem/kvdk/IteratorTest.java + src/test/java/io/pmem/kvdk/WriteBatchTest.java +) + +include(FindJava) +include(UseJava) +find_package(JNI) +include(ExternalProject) + +include_directories(${JNI_INCLUDE_DIRS}) +include_directories(${PROJECT_SOURCE_DIR}/java) + +set(MVN_LOCAL ~/.m2/repository) +set(MVN_CENTRAL https://repo1.maven.org/maven2) + +set(JAVA_TEST_LIBDIR ${PROJECT_SOURCE_DIR}/java/test-libs) +set(JAVA_TMP_JAR ${JAVA_TEST_LIBDIR}/tmp.jar) + +set(JAVA_JUNIT_JAR_SOURCE /junit/junit/4.12/junit-4.12.jar) +set(JAVA_JUNIT_JAR ${JAVA_TEST_LIBDIR}/junit-4.12.jar) +set(JAVA_JUNIT_JAR_CHECKSUM_256 59721f0805e223d84b90677887d9ff567dc534d7c502ca903c0c2b17f05c116a) + +set(JAVA_TESTCLASSPATH ${JAVA_JUNIT_JAR}) + +set(JNI_OUTPUT_DIR ${PROJECT_SOURCE_DIR}/java/include) +file(MAKE_DIRECTORY ${JNI_OUTPUT_DIR}) + +if(${Java_VERSION_MINOR} VERSION_LESS_EQUAL "7" AND ${Java_VERSION_MAJOR} STREQUAL "1") + message(FATAL_ERROR "Detected Java 7 or older (${Java_VERSION_STRING}), minimum required version is Java 8") +endif() + +# Download dependencies for Java unit tests +if(NOT EXISTS ${JAVA_TEST_LIBDIR}) + file(MAKE_DIRECTORY ${JAVA_TEST_LIBDIR}) +endif() + +if(NOT EXISTS ${JAVA_JUNIT_JAR}) + if(EXISTS ${MVN_LOCAL}${JAVA_JUNIT_JAR_SOURCE}) + message("Copying from ${MVN_LOCAL}${JAVA_JUNIT_JAR_SOURCE} to ${JAVA_JUNIT_JAR}") + file(COPY ${MVN_LOCAL}${JAVA_JUNIT_JAR_SOURCE} DESTINATION ${JAVA_TEST_LIBDIR}) + else() + message("Downloading ${MVN_CENTRAL}${JAVA_JUNIT_JAR_SOURCE} to ${JAVA_TMP_JAR}") + execute_process(COMMAND curl --fail --insecure --location ${MVN_CENTRAL}${JAVA_JUNIT_JAR_SOURCE} + RESULT_VARIABLE error_code + OUTPUT_VARIABLE error_message + OUTPUT_FILE ${JAVA_TMP_JAR}) + + if(NOT error_code EQUAL 0) + message(FATAL_ERROR "Failed downloading ${MVN_CENTRAL}${JAVA_JUNIT_JAR_SOURCE} to ${JAVA_JUNIT_JAR}: ${error_message}") + endif() + file(RENAME ${JAVA_TMP_JAR} ${JAVA_JUNIT_JAR}) + endif() +endif() + +# Verify checksum of downloaded files +file(SHA256 ${JAVA_JUNIT_JAR} TEMP_FILE_CHECKSUM) +if (NOT ${JAVA_JUNIT_JAR_CHECKSUM_256} STREQUAL ${TEMP_FILE_CHECKSUM}) + message(FATAL_ERROR "Checksum mismatch, file: ${JAVA_JUNIT_JAR}, expected: ${JAVA_JUNIT_JAR_CHECKSUM_256}, is: ${TEMP_FILE_CHECKSUM}") +endif() + +if(${Java_VERSION_MAJOR} VERSION_GREATER_EQUAL "10" AND ${CMAKE_VERSION} VERSION_LESS "3.11.4") + # Java 10 and newer don't have javah, but the alternative GENERATE_NATIVE_HEADERS requires CMake 3.11.4 or newer + message(FATAL_ERROR "Detected Java 10 or newer (${Java_VERSION_STRING}), to build with CMake please upgrade CMake to 3.11.4 or newer") + +elseif(${CMAKE_VERSION} VERSION_LESS "3.11.4") + # Old CMake + message("Using an old CMAKE (${CMAKE_VERSION}) - JNI headers generated in separate step") + add_jar( + kvdkjni_classes + SOURCES + ${JAVA_MAIN_SOURCES} + ${JAVA_TEST_SOURCES} + INCLUDE_JARS ${JAVA_TESTCLASSPATH} + ) + +else () + # Java 1.8 or newer prepare the JAR... + message("Preparing Jar for JDK ${Java_VERSION_STRING}") + add_jar( + kvdkjni_classes + SOURCES + ${JAVA_MAIN_SOURCES} + ${JAVA_TEST_SOURCES} + INCLUDE_JARS ${JAVA_TESTCLASSPATH} + GENERATE_NATIVE_HEADERS kvdkjni_headers DESTINATION ${JNI_OUTPUT_DIR} + ) + +endif() + +if(${CMAKE_VERSION} VERSION_LESS "3.11.4") + # Old CMake ONLY generate JNI headers, otherwise JNI is handled in add_jar step above + message("Preparing JNI headers for old CMake (${CMAKE_VERSION})") + set(NATIVE_JAVA_CLASSES + io.pmem.kvdk.Configs + ) + + create_javah( + TARGET kvdkjni_headers + CLASSES ${NATIVE_JAVA_CLASSES} + CLASSPATH kvdkjni_classes ${JAVA_TESTCLASSPATH} + OUTPUT_DIR ${JNI_OUTPUT_DIR} + ) +endif() + +set(KVDKJNI_SHARED_LIB kvdkjni${ARTIFACT_SUFFIX}) +add_library(${KVDKJNI_SHARED_LIB} SHARED ${JNI_NATIVE_SOURCES}) +add_dependencies(${KVDKJNI_SHARED_LIB} kvdkjni_headers) +target_link_libraries(${KVDKJNI_SHARED_LIB} ${KVDK_STATIC_LIB}) + +# find needed shared libraries. +# libatomic.so.1 +find_library(ATOMIC_SHARED_LIB NAMES "${CMAKE_SHARED_LIBRARY_PREFIX}atomic${CMAKE_SHARED_LIBRARY_SUFFIX}.1") +find_package_handle_standard_args(atomic REQUIRED_VARS ATOMIC_SHARED_LIB) +if(NOT atomic_FOUND) + message(FATAL_ERROR "libatomic.so.1 not found") +endif() + +# libstdc++.so.6 +find_library(STD_CPP_SHARED_LIB NAMES "${CMAKE_SHARED_LIBRARY_PREFIX}stdc++${CMAKE_SHARED_LIBRARY_SUFFIX}.6") +find_package_handle_standard_args(std_cpp REQUIRED_VARS STD_CPP_SHARED_LIB) +if(NOT std_cpp_FOUND) + message(FATAL_ERROR "libstdc++.so.6 not found") +endif() + +# copy shared libraries to java dir for unit tests +SET(JAVA_RESOURCES_DIR ${PROJECT_SOURCE_DIR}/java/src/main/resources/) +if(NOT EXISTS ${JAVA_RESOURCES_DIR}) + file(MAKE_DIRECTORY ${JAVA_RESOURCES_DIR}) +endif() + +set(ATOMIC_SHARED_LIB_FILE ${CMAKE_SHARED_LIBRARY_PREFIX}atomic${CMAKE_SHARED_LIBRARY_SUFFIX}.1) +set(STD_CPP_SHARED_LIB_FILE ${CMAKE_SHARED_LIBRARY_PREFIX}stdc++${CMAKE_SHARED_LIBRARY_SUFFIX}.6) +set(KVDKJNI_SHARED_LIB_FILE ${CMAKE_SHARED_LIBRARY_PREFIX}${KVDKJNI_SHARED_LIB}${CMAKE_SHARED_LIBRARY_SUFFIX}) + +add_custom_command(TARGET ${KVDKJNI_SHARED_LIB} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${ATOMIC_SHARED_LIB} + ${JAVA_RESOURCES_DIR} + COMMAND ${CMAKE_COMMAND} -E copy + ${STD_CPP_SHARED_LIB} + ${JAVA_RESOURCES_DIR} + COMMAND ${CMAKE_COMMAND} -E copy + $ + ${JAVA_RESOURCES_DIR} + COMMENT "Copying JNI library file to: ${JAVA_RESOURCES_DIR}" +) + +# build jar +add_custom_command(TARGET ${KVDKJNI_SHARED_LIB} POST_BUILD + COMMAND cp kvdkjni_classes.jar ${KVDKJNI_JAR_FILE} + COMMAND cp ${ATOMIC_SHARED_LIB} ${ATOMIC_SHARED_LIB_FILE} + COMMAND jar -uf ${KVDKJNI_JAR_FILE} ${ATOMIC_SHARED_LIB_FILE} + COMMAND cp ${STD_CPP_SHARED_LIB} ${STD_CPP_SHARED_LIB_FILE} + COMMAND jar -uf ${KVDKJNI_JAR_FILE} ${STD_CPP_SHARED_LIB_FILE} + COMMAND jar -uf ${KVDKJNI_JAR_FILE} ${KVDKJNI_SHARED_LIB_FILE} + COMMENT "Building jar file: ${CMAKE_CURRENT_BINARY_DIR}/${KVDKJNI_JAR_FILE}" +) diff --git a/volatile/java/README.md b/volatile/java/README.md new file mode 100644 index 00000000..7fcd9f15 --- /dev/null +++ b/volatile/java/README.md @@ -0,0 +1,89 @@ +## Build + +### System requirements +* JDK >= 1.8 +* Apache Maven >= 3.1.0 + +### Build KVDK JNI library +Add `-DWITH_JNI=ON` to cmake command options. For example: + +```bash +cd kvdk +mkdir -p build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DCHECK_CPP_STYLE=ON -DWITH_JNI=ON && make -j +``` + +### Unit Test with Apache Maven +```bash +cd kvdk/java +mvn clean test +``` + +## Release +### Install to local Maven repository +```bash +cd kvdk/java +mvn clean install +``` + +### Release to Maven Central +1. Sign and deploy jar [to OSSRH](https://central.sonatype.org/publish/publish-manual/) +2. Release deployment [from OSSRH to Maven Central](https://central.sonatype.org/publish/release/) + +## Run examples +```bash +cd kvdk/java/examples +mvn clean package + +export PMEM_IS_PMEM_FORCE=1 +mkdir -p /tmp/kvdk-test-dir + +java -cp target/kvdkjni-examples-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.examples.KVDKExamples +``` + +## Run benchmark +```bash +cd kvdk/java/benchmark +mvn clean package + +mkdir -p /mnt/pmem0/kvdk/bench-dir + +export type=string +# or export type=sorted + +# fill example +numactl --cpunodebind=0 --membind=0 java -cp target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark -fill=true -latency=true -path=/mnt/pmem0/kvdk/bench-dir -space=412316860416 -value_size=120 -num_kv=536870912 -num_operations=536870912 -type=$type -threads=32 + +# random batch insert example +numactl --cpunodebind=0 --membind=0 java -cp target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark -fill=false -latency=true -path=/mnt/pmem0/kvdk/bench-dir -space=412316860416 -value_size=120 -num_kv=536870912 -num_operations=536870912 -type=$type -threads=32 -timeout=30 -read_ratio=0 -existing_keys_ratio=0 -batch_size=100 + +# random insert example +numactl --cpunodebind=0 --membind=0 java -cp target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark -fill=false -latency=true -path=/mnt/pmem0/kvdk/bench-dir -space=412316860416 -value_size=120 -num_kv=536870912 -num_operations=536870912 -type=$type -threads=32 -timeout=30 -read_ratio=0 -existing_keys_ratio=0 + +# range scan example +numactl --cpunodebind=0 --membind=0 java -cp target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark -fill=false -latency=true -path=/mnt/pmem0/kvdk/bench-dir -space=412316860416 -value_size=120 -num_kv=536870912 -num_operations=10737418240 -type=$type -threads=32 -timeout=30 -read_ratio=1 -scan=1 + +# random read example +numactl --cpunodebind=0 --membind=0 java -cp target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark -fill=false -latency=true -path=/mnt/pmem0/kvdk/bench-dir -space=412316860416 -value_size=120 -num_kv=536870912 -num_operations=10737418240 -type=$type -threads=32 -timeout=30 -read_ratio=1 + +# random read write example (9R:1W) +numactl --cpunodebind=0 --membind=0 java -cp target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark -fill=false -latency=true -path=/mnt/pmem0/kvdk/bench-dir -space=412316860416 -value_size=120 -num_kv=536870912 -num_operations=536870912 -type=$type -threads=32 -timeout=30 -read_ratio=0.9 + +# random update example +numactl --cpunodebind=0 --membind=0 java -cp target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark -fill=false -latency=true -path=/mnt/pmem0/kvdk/bench-dir -space=412316860416 -value_size=120 -num_kv=536870912 -num_operations=10737418240 -type=$type -threads=32 -timeout=30 -read_ratio=0 +``` + +## Cross Platform + +The KVDK Java library contains the needed shared libaries (`.so` files), which will be loaded when they are not present in system library paths. + +A Java application relying on KVDK Java library can be run on **64-bit** servers without building the KVDK C++ code or installing its dependencies (e.g. ndctl, pmdk). + +The only requirement is the `libc` version `GLIBC_2.18 or higher` can be found. + +We tested without installing KVDK C++ library or its dependencies on below platforms, in which the `libc` is new enough: +* Fedora >= 20 +* Ubuntu >= 14.04 +* Centos >= 8 + +For older platforms, you may need to upgrade `libc` in system path or where `LD_LIBRARY_PATH` points to. diff --git a/volatile/java/benchmark/pom.xml b/volatile/java/benchmark/pom.xml new file mode 100644 index 00000000..a2b66e69 --- /dev/null +++ b/volatile/java/benchmark/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + io.pmem + kvdkjni-benchmark + 1.0.0-SNAPSHOT + + KVDK JNI Benchmark + KVDK JNI Benchmark. + https://github.com/pmem/kvdk + 2021 + + + + BSD-3-Clause + hhttps://github.com/pmem/kvdk/blob/main/LICENSE + repo + + + + + scm:git:https://github.com/pmem/kvdk.git + scm:git:https://github.com/pmem/kvdk.git + scm:git:https://github.com/pmem/kvdk.git + + + + Intel + https://www.intel.com + + + + 1.0.0-SNAPSHOT + 1.8 + 1.8 + UTF-8 + 2.7.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + ${project.build.source} + ${project.build.target} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + target + -Djava.library.path=${project.build.directory} + false + false + + ${project.build.directory}/* + + + 1 + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless.version} + + + + 1.7 + + + + + + + spotless-check + compile + + check + + + + + + + + + + io.pmem + kvdkjni + ${kvdkjni.version} + + + diff --git a/volatile/java/benchmark/scripts/benchmark_all.sh b/volatile/java/benchmark/scripts/benchmark_all.sh new file mode 100755 index 00000000..70de46c6 --- /dev/null +++ b/volatile/java/benchmark/scripts/benchmark_all.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +BASEDIR=$(dirname "$0") + +# Note: The directory $pmem_path will be removed from file system automatically before benchmarking +pmem_path=/mnt/pmem0/kvdk/bench-dir +space=412316860416 +num_kv=536870912 + +# languages +for lang in java native; do + echo "#################################### $lang ####################################" + + bin="java -cp $BASEDIR/../target/kvdkjni-benchmark-1.0.0-SNAPSHOT.jar:$BASEDIR/../../target/kvdkjni-1.0.0-SNAPSHOT.jar io.pmem.kvdk.benchmark.KVDKBenchmark" + if [ "$lang" == "native" ]; then + bin=$BASEDIR/../../../build/bench + fi + + # value_sizes + for value_size in 120 4096; do + echo "#################################### value_size: $value_size ####################################" + if [[ "$value_size" == "4096" ]]; then + # calculation logic for num_kv: + # header_size = type == string ? 24 : 48 + # key_size = 8 + # num_kv = space // 4 // (header_size + key_size + average_value_size) + + num_kv=24778657 + fi + + # benchmarks + for type in string sorted blackhole; do + echo "#################################### $type ####################################" + rm -rf $pmem_path + + num_ops1=$num_kv + num_ops2=10737418240 + + if [ "$type" == "blackhole" ]; then + num_ops1=$num_ops2 + fi + + # fill + echo "#################################### fill" + numactl -N 0 -m 0 $bin -populate=true -latency=true -path=$pmem_path -space=$space -value_size=$value_size -num_kv=$num_kv -num_operations=$num_ops1 -max_access_threads=64 -threads=64 -type=$type -num_collection=16 -fill=true + + # batch insert random + echo "#################################### batch insert random" + numactl -N 0 -m 0 $bin -populate=true -latency=true -path=$pmem_path -space=$space -value_size=$value_size -num_kv=$num_kv -num_operations=$num_ops1 -max_access_threads=64 -threads=64 -type=$type -num_collection=16 -fill=false -timeout=30 -read_ratio=0 -existing_keys_ratio=0 -batch_size=100 + + # insert random + echo "#################################### insert random" + numactl -N 0 -m 0 $bin -populate=true -latency=true -path=$pmem_path -space=$space -value_size=$value_size -num_kv=$num_kv -num_operations=$num_ops1 -max_access_threads=64 -threads=64 -type=$type -num_collection=16 -fill=false -timeout=30 -read_ratio=0 -existing_keys_ratio=0 + + # range scan + echo "#################################### range scan" + numactl -N 0 -m 0 $bin -populate=true -latency=true -path=$pmem_path -space=$space -value_size=$value_size -num_kv=$num_kv -num_operations=$num_ops2 -max_access_threads=64 -threads=64 -type=$type -num_collection=16 -fill=false -timeout=30 -read_ratio=1 -scan=1 + + # read random + echo "#################################### read random" + numactl -N 0 -m 0 $bin -populate=true -latency=true -path=$pmem_path -space=$space -value_size=$value_size -num_kv=$num_kv -num_operations=$num_ops2 -max_access_threads=64 -threads=64 -type=$type -num_collection=16 -fill=false -timeout=30 -read_ratio=1 + + # read write random (9R:1W) + echo "#################################### read write random 9R:1W" + numactl -N 0 -m 0 $bin -populate=true -latency=true -path=$pmem_path -space=$space -value_size=$value_size -num_kv=$num_kv -num_operations=$num_ops1 -max_access_threads=64 -threads=64 -type=$type -num_collection=16 -fill=false -timeout=30 -read_ratio=0.9 + + # update random + echo "#################################### update random" + numactl -N 0 -m 0 $bin -populate=true -latency=true -path=$pmem_path -space=$space -value_size=$value_size -num_kv=$num_kv -num_operations=$num_ops2 -max_access_threads=64 -threads=64 -type=$type -num_collection=16 -fill=false -timeout=30 -read_ratio=0 + done + done +done diff --git a/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java new file mode 100644 index 00000000..afdee6ad --- /dev/null +++ b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/KVDKBenchmark.java @@ -0,0 +1,1033 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk.benchmark; + +import io.pmem.kvdk.Configs; +import io.pmem.kvdk.Engine; +import io.pmem.kvdk.Iterator; +import io.pmem.kvdk.KVDKException; +import io.pmem.kvdk.NativeBytesHandle; +import io.pmem.kvdk.WriteBatch; +import io.pmem.kvdk.benchmark.util.ConstantLongGenerator; +import io.pmem.kvdk.benchmark.util.LongGenerator; +import io.pmem.kvdk.benchmark.util.RandomLongGenerator; +import io.pmem.kvdk.benchmark.util.RangeLongGenerator; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +public class KVDKBenchmark { + private static final long MAX_VALUE_SIZE = 102400; + private static final long VALUE_RANDOM_SEED = 42; + private static final int WARMUP_SECONDS = 2; + private static final int MAX_LATENCY = 10000000; // Unit: 100 ns + + String path; + long numKv; + long numOperations; + boolean fill; + long timeout; + long valueSize; + String valueSizeDistributionStr; + int threads; + double readRatio; + double existingKeysRatio; + boolean latency; + String type; + boolean scan; + int numCollection; + long batchSize; + String keyDistributionStr; + boolean populate; + int maxAccessThreads; + long space; + boolean optLargeSortedCollectionRestore; + boolean useDevdaxMode; + long hashBucketNum; + + Map flags; + + List collectionNames; + List closeableObjects; + long maxRandomKey; + int writeThreads; + int readThreads; + ExecutorService executor; + + List readOpsLog; + List writeOpsLog; + List readNotFoundCountLog; + int lastValidLogIndex; + + DataType dataType; + KeyDistribution keyDistribution; + ValueSizeDistribution valueSizeDistribution; + + AtomicLong finishedThreads; + AtomicBoolean isTimeout; + AtomicLong readOps; + AtomicLong writeOps; + AtomicLong readNotFoundCount; + + Engine kvdkEngine; + byte[] valuePool; + long numOperationsPerThread; + NativeBytesHandle[] collectionNameHandles; + LongGenerator[] randomKeyGenerators; + LongGenerator[] rangeKeyGenerators; + LongGenerator[] randomValueSizeGenerators; + LongGenerator[] constantValueSizeGenerators; + long[][] latencies; + + static { + Engine.loadLibrary(); + } + + public KVDKBenchmark(Map flags) { + path = (String) flags.get(Flag.path); + numKv = (Long) flags.get(Flag.num_kv); + numOperations = (Long) flags.get(Flag.num_operations); + fill = (Boolean) flags.get(Flag.fill); + timeout = (Long) flags.get(Flag.timeout); + valueSize = (Long) flags.get(Flag.value_size); + valueSizeDistributionStr = (String) flags.get(Flag.value_size_distribution); + threads = ((Long) flags.get(Flag.threads)).intValue(); + readRatio = (Double) flags.get(Flag.read_ratio); + existingKeysRatio = (Double) flags.get(Flag.existing_keys_ratio); + latency = (Boolean) flags.get(Flag.latency); + type = (String) flags.get(Flag.type); + scan = (Boolean) flags.get(Flag.scan); + numCollection = ((Long) flags.get(Flag.num_collection)).intValue(); + batchSize = (Long) flags.get(Flag.batch_size); + keyDistributionStr = (String) flags.get(Flag.key_distribution); + populate = (Boolean) flags.get(Flag.populate); + maxAccessThreads = ((Long) flags.get(Flag.max_access_threads)).intValue(); + space = (Long) flags.get(Flag.space); + optLargeSortedCollectionRestore = + (Boolean) flags.get(Flag.opt_large_sorted_collection_restore); + useDevdaxMode = (Boolean) flags.get(Flag.use_devdax_mode); + hashBucketNum = (Long) flags.get(Flag.hash_bucket_num); + + this.flags = flags; + closeableObjects = new ArrayList<>(); + finishedThreads = new AtomicLong(); + isTimeout = new AtomicBoolean(false); + readOps = new AtomicLong(); + writeOps = new AtomicLong(); + readNotFoundCount = new AtomicLong(); + readOpsLog = new ArrayList<>(); + writeOpsLog = new ArrayList<>(); + readNotFoundCountLog = new ArrayList<>(); + + printFlags(); + processBenchmarkConfigs(); + generateValuePool(); + } + + private void printFlags() { + System.out.println("flags:"); + for (Flag flag : Flag.values()) { + System.out.format(" -%s=%s%n", flag.name(), flags.get(flag).toString()); + } + } + + private void processBenchmarkConfigs() { + if (type.equals("sorted")) { + dataType = DataType.Sorted; + } else if (type.equals("string")) { + dataType = DataType.String; + } else if (type.equals("blackhole")) { + dataType = DataType.Blackhole; + } else { + throw new IllegalArgumentException("Unsupported -type: " + type); + } + + if (dataType.equals(DataType.Sorted)) { + collectionNames = new ArrayList<>(); + collectionNameHandles = new NativeBytesHandle[numCollection]; + for (int i = 0; i < numCollection; i++) { + collectionNames.add("Collection_" + i); + } + } + + if (scan && dataType.equals(DataType.String)) { + throw new IllegalArgumentException("Unsupported to scan for -type: " + type); + } + + if (valueSize > MAX_VALUE_SIZE) { + throw new IllegalArgumentException( + "-value_size is too large: " + + valueSize + + ", should be less than: " + + MAX_VALUE_SIZE); + } + + maxRandomKey = existingKeysRatio == 0 ? Long.MAX_VALUE : (long) (numKv / existingKeysRatio); + + if (fill) { + assert (readRatio == 0); + System.out.println("Fill mode, -num_operations is ignored, will use -num_kv"); + + if (threads > maxAccessThreads) { + threads = maxAccessThreads; // overwrite + System.out.println( + "Fill mode, -threads is reset to -max_access_threads: " + maxAccessThreads); + } + System.out.println("Fill mode, -threads: " + threads); + System.out.println("Fill mode, -key_distribution is reset to: range"); + + keyDistribution = KeyDistribution.Range; + numOperationsPerThread = numKv / threads + 1; + rangeKeyGenerators = new LongGenerator[threads]; + for (int i = 0; i < threads; i++) { + rangeKeyGenerators[i] = + new RangeLongGenerator( + i * numOperationsPerThread, (i + 1) * numOperationsPerThread, 1); + } + } else { + numOperationsPerThread = numOperations / threads + 1; + + if (keyDistributionStr.equals("random")) { + keyDistribution = KeyDistribution.Uniform; + } else { + throw new IllegalArgumentException( + "Invalid -key_distribution: " + keyDistributionStr); + } + + randomKeyGenerators = new LongGenerator[threads]; + for (int i = 0; i < threads; i++) { + randomKeyGenerators[i] = new RandomLongGenerator(0, maxRandomKey); + } + } + System.out.println("Num of operations per thread: " + numOperationsPerThread); + + if (valueSizeDistributionStr.equals("constant")) { + valueSizeDistribution = ValueSizeDistribution.Constant; + constantValueSizeGenerators = new LongGenerator[threads]; + for (int i = 0; i < threads; i++) { + constantValueSizeGenerators[i] = new ConstantLongGenerator(valueSize); + } + } else if (valueSizeDistributionStr.equals("random")) { + valueSizeDistribution = ValueSizeDistribution.Uniform; + randomValueSizeGenerators = new LongGenerator[threads]; + for (int i = 0; i < threads; i++) { + randomValueSizeGenerators[i] = new RandomLongGenerator(1, valueSize + 1); + } + } else { + throw new IllegalArgumentException( + "Invalid -value_size_distribution: " + valueSizeDistributionStr); + } + + writeThreads = fill ? threads : (int) (threads - (readRatio * 100 * threads / 100)); + readThreads = threads - writeThreads; + + if (latency) { + System.out.println("Latency stat is enabled."); + latencies = new long[threads][MAX_LATENCY]; + } + } + + private void initKVDKEngine() throws KVDKException { + Configs configs = new Configs(); + + configs.setMaxAccessThreads(maxAccessThreads); + configs.setOptLargeSortedCollectionRecovery(optLargeSortedCollectionRestore); + configs.setUseDevDaxMode(useDevdaxMode); + configs.setHashBucketNum(hashBucketNum); + + kvdkEngine = Engine.open(path, configs); + + configs.close(); + closeableObjects.add(kvdkEngine); + } + + private void generateValuePool() { + StringBuilder sb = new StringBuilder(); + Random random = new Random(VALUE_RANDOM_SEED); + int low = 'a'; + for (int i = 0; i < valueSize; i++) { + sb.append((char) (low + random.nextInt(26))); + } + + valuePool = sb.toString().getBytes(); + } + + private void createSortedCollections() throws KVDKException { + System.out.format("Creating %d Sorted Collections", numCollection); + for (int i = 0; i < collectionNames.size(); i++) { + String collectionName = collectionNames.get(i); + NativeBytesHandle nameHandle = new NativeBytesHandle(collectionName.getBytes()); + collectionNameHandles[i] = nameHandle; + closeableObjects.add(nameHandle); + kvdkEngine.sortedCreate(nameHandle); + } + } + + private void startTasks() { + System.out.println("Init " + readThreads + " readers and " + writeThreads + " writers."); + executor = Executors.newCachedThreadPool(); + for (int i = 0; i < writeThreads; i++) { + executor.submit(new WriteTask(i)); + } + for (int i = writeThreads; i < threads; i++) { + Callable task = scan ? new ScanTask(i) : new ReadTask(i); + executor.submit(task); + } + } + + private void shutdownTasks() throws InterruptedException { + System.out.println("Shutdown task executor."); + executor.shutdown(); + boolean finished = executor.awaitTermination(10, TimeUnit.SECONDS); + if (!finished) { + System.out.println("Executor was not finished after 10 seconds. Force shutdown."); + executor.shutdownNow(); + } + System.out.println("Executor was shut down."); + } + + private void printProgress() throws InterruptedException { + System.out.println("----------------------------------------------------------"); + System.out.format( + "%-15s%-15s%-15s%-15s%-15s%-15s\n", + "Time(ms)", "Read Ops", "Write Ops", "Not Found", "Total Read", "Total Write"); + long start = System.currentTimeMillis(); + + readOpsLog.add(Long.valueOf(0)); + writeOpsLog.add(Long.valueOf(0)); + readNotFoundCountLog.add(Long.valueOf(0)); + lastValidLogIndex = 0; + while (true) { + Thread.sleep(1000); + long duration = System.currentTimeMillis() - start; + + readOpsLog.add(readOps.get()); + writeOpsLog.add(writeOps.get()); + readNotFoundCountLog.add(readNotFoundCount.get()); + + int idx = readOpsLog.size() - 1; + System.out.format( + "%-15d%-15d%-15d%-15d%-15d%-15d\n", + duration, + readOpsLog.get(idx) - readOpsLog.get(idx - 1), + writeOpsLog.get(idx) - writeOpsLog.get(idx - 1), + readNotFoundCountLog.get(idx) - readNotFoundCountLog.get(idx - 1), + readOpsLog.get(idx), + writeOpsLog.get(idx)); + + if (finishedThreads.get() == 0 || idx <= WARMUP_SECONDS) { + lastValidLogIndex = idx; + } + + if (finishedThreads.get() == threads) { + break; + } + + if (!fill && duration >= timeout * 1000) { + // Signal time out + System.out.println("Benchmark timeout (second): " + timeout); + isTimeout.set(true); + break; + } + } + + long duration = System.currentTimeMillis() - start; + System.out.println("Benchmark finished, duration (ms): " + duration); + } + + private void printLatency() { + long ro = readOps.get(); + if (ro > 0 && readThreads > 0 && !scan) { + double avg = 0; + double l50 = 0; + double l99 = 0; + double l995 = 0; + double l999 = 0; + double l9999 = 0; + + long accumulatedOps = 0; + long accumulatedLatency = 0; + for (int i = 1; i < MAX_LATENCY; i++) { + for (int j = 0; j < readThreads; j++) { + accumulatedOps += latencies[writeThreads + j][i]; + accumulatedLatency += latencies[writeThreads + j][i] * i; + + if (l50 == 0 && (double) accumulatedOps / ro > 0.5) { + l50 = (double) i / 10; + } else if (l99 == 0 && (double) accumulatedOps / ro > 0.99) { + l99 = (double) i / 10; + } else if (l995 == 0 && (double) accumulatedOps / ro > 0.995) { + l995 = (double) i / 10; + } else if (l999 == 0 && (double) accumulatedOps / ro > 0.999) { + l999 = (double) i / 10; + } else if (l9999 == 0 && (double) accumulatedOps / ro > 0.9999) { + l9999 = (double) i / 10; + } + } + } + avg = accumulatedLatency / ro / 10; + + System.out.format( + "read lantencies (us): Avg: %.2f, P50: %.2f, P99: %.2f, " + + "P99.5: %.2f, P99.9: %.2f, P99.99: %.2f\n", + avg, l50, l99, l995, l999, l9999); + } + + long wo = writeOps.get(); + if (wo > 0 && writeThreads > 0) { + double avg = 0; + double l50 = 0; + double l99 = 0; + double l995 = 0; + double l999 = 0; + double l9999 = 0; + + long accumulatedOps = 0; + long accumulatedLatency = 0; + for (int i = 1; i < MAX_LATENCY; i++) { + for (int j = 0; j < writeThreads; j++) { + accumulatedOps += latencies[j][i]; + accumulatedLatency += latencies[j][i] * i; + + if (l50 == 0 && (double) accumulatedOps / wo > 0.5) { + l50 = (double) i / 10; + } else if (l99 == 0 && (double) accumulatedOps / wo > 0.99) { + l99 = (double) i / 10; + } else if (l995 == 0 && (double) accumulatedOps / wo > 0.995) { + l995 = (double) i / 10; + } else if (l999 == 0 && (double) accumulatedOps / wo > 0.999) { + l999 = (double) i / 10; + } else if (l9999 == 0 && (double) accumulatedOps / wo > 0.9999) { + l9999 = (double) i / 10; + } + } + } + avg = accumulatedLatency / wo / 10; + + System.out.format( + "write lantencies (us): Avg: %.2f, P50: %.2f, P99: %.2f, " + + "P99.5: %.2f, P99.9: %.2f, P99.99: %.2f\n", + avg, l50, l99, l995, l999, l9999); + } + } + + private void printFinalStats() { + int timeElapsedInSeconds = 0; + long totalValidRead = 0; + long totalValidWrite = 0; + + if (lastValidLogIndex <= WARMUP_SECONDS) { + timeElapsedInSeconds = lastValidLogIndex; + totalValidRead = readOpsLog.get(lastValidLogIndex); + totalValidWrite = writeOpsLog.get(lastValidLogIndex); + } else { + timeElapsedInSeconds = lastValidLogIndex - WARMUP_SECONDS; + totalValidRead = readOpsLog.get(lastValidLogIndex) - readOpsLog.get(WARMUP_SECONDS); + totalValidWrite = writeOpsLog.get(lastValidLogIndex) - writeOpsLog.get(WARMUP_SECONDS); + } + + if (timeElapsedInSeconds == 0) { + return; + } + + System.out.println("----------------------------------------------------------"); + System.out.println( + "Average Read Ops:\t" + + totalValidRead / timeElapsedInSeconds + + ". Average Write Ops:\t" + + totalValidWrite / timeElapsedInSeconds); + + if (latency) { + printLatency(); + } + } + + private void closeObjects() throws Exception { + Collections.reverse(closeableObjects); + for (AutoCloseable c : closeableObjects) { + c.close(); + } + } + + public void run() throws Exception { + if (!dataType.equals(DataType.Blackhole)) { + initKVDKEngine(); + } + + if (dataType.equals(DataType.Sorted)) { + createSortedCollections(); + } + + startTasks(); + + try { + printProgress(); + } finally { + shutdownTasks(); + + printFinalStats(); + + closeObjects(); + } + } + + private enum DataType { + String, + Sorted, + Blackhole; + } + + private enum KeyDistribution { + Range, + Uniform + } + + private enum ValueSizeDistribution { + Constant, + Uniform + } + + private enum Flag { + path("/mnt/pmem0/kvdk", "Instance path") { + @Override + protected Object parseValue(String value) { + return value; + } + }, + num_kv((1L << 30), "Number of KVs to place") { + @Override + protected Object parseValue(String value) { + return parseLongPositive("num_kv", value); + } + }, + num_operations( + (1L << 30), + "Number of total operations. Asserted to be equal to num_kv if \n" + + "\t(fill == true).") { + @Override + protected Object parseValue(String value) { + return parseLongPositive("num_operations", value); + } + }, + fill(false, "Fill num_kv uniform kv pairs to a new instance") { + @Override + protected Object parseValue(String value) { + return parseBoolean(value); + } + }, + timeout(30L, "Time (seconds) to benchmark, this is valid only if fill=false") { + @Override + protected Object parseValue(String value) { + return parseLongPositive("timeout", value); + } + }, + value_size(120L, "Value size of KV") { + @Override + protected Object parseValue(String value) { + return parseLongPositive("value_size", value); + } + }, + value_size_distribution( + "constant", + "Distribution of value size to write, can be constant/random,\n" + + "\tdefault is constant. If set to random, the max value size\n" + + "\twill be -value_size.") { + @Override + protected Object parseValue(String value) { + return value; + } + }, + threads(10L, "Number of concurrent threads to run benchmark") { + @Override + protected Object parseValue(String value) { + return parseLongPositive("threads", value); + } + }, + read_ratio(0.0, "Read threads = threads * read_ratio") { + @Override + protected Object parseValue(String value) { + return parseDoubleRatio("read_ratio", value); + } + }, + existing_keys_ratio( + 1.0, + "Ratio of keys to read / write that existed in the filled instance, for\n" + + "\texample, if set to\n" + + "\t1, all writes will be updates, and all read keys will be existed") { + @Override + protected Object parseValue(String value) { + return parseDoubleRatio("existing_keys_ratio", value); + } + }, + latency(false, "Stat operation latencies") { + @Override + protected Object parseValue(String value) { + return parseBoolean(value); + } + }, + type("string", "Storage engine to benchmark, can be string, sorted or blackhole") { + @Override + protected Object parseValue(String value) { + return value; + } + }, + scan( + false, + "If set true, read threads will do scan operations, this is valid\n" + + "\tonly if we benchmark sorted engine") { + @Override + protected Object parseValue(String value) { + return parseBoolean(value); + } + }, + num_collection(1L, "Number of collections in the instance to benchmark") { + @Override + protected Object parseValue(String value) { + return parseLongNotNegtive("num_collection", value); + } + }, + batch_size( + 0L, + "Size of write batch. If batch>0, write string type kv with atomic batch\n" + + "\twrite, this is valid only if we benchmark string engine") { + @Override + protected Object parseValue(String value) { + return parseLongNotNegtive("batch_size", value); + } + }, + key_distribution( + "random", + "Distribution of benchmark keys, if fill is true, this param will\n" + + "\tbe ignored and only uniform distribution will be used") { + @Override + protected Object parseValue(String value) { + return value; + } + }, + populate( + false, + "Populate pmem space while creating a new instance. This can improve write\n" + + "\tperformance in runtime, but will take long time to init the instance") { + @Override + protected Object parseValue(String value) { + return parseBoolean(value); + } + }, + max_access_threads(32L, "Max access threads of the instance") { + @Override + protected Object parseValue(String value) { + return parseLongPositive("max_access_threads", value); + } + }, + space((256L << 30), "Max usable PMem space of the instance") { + @Override + protected Object parseValue(String value) { + return parseLongPositive("space", value); + } + }, + opt_large_sorted_collection_restore( + true, + "Optional optimization strategy which Multi-thread recovery a\n" + + "\tskiplist. When having few large skiplists, the optimization can\n" + + "\tget better performance") { + @Override + protected Object parseValue(String value) { + return parseBoolean(value); + } + }, + use_devdax_mode(false, "Use devdax device for kvdk") { + @Override + protected Object parseValue(String value) { + return parseBoolean(value); + } + }, + hash_bucket_num( + (1L << 27), + "The number of bucket groups in the hash table.\n" + + "\tIt should be 2^n and should smaller than 2^32.") { + @Override + protected Object parseValue(String value) { + Long ret = parseLongPositive("hash_bucket_num", value); + if (!isPowerOfTwo(ret) || (int) (Math.ceil((Math.log(ret) / Math.log(2)))) >= 32) { + throw new IllegalArgumentException( + "Invalid value for -hash_bucket_num: " + ret); + } + return ret; + } + }; + + private Flag(Object defaultValue, String description) { + this.defaultValue = defaultValue; + this.description = description; + } + + public Object getDefaultValue() { + return defaultValue; + } + + public String getDescription() { + return description; + } + + public boolean parseBoolean(String value) { + if (value.equals("1")) { + return true; + } else if (value.equals("0")) { + return false; + } + return Boolean.parseBoolean(value); + } + + public Long parseLongNotNegtive(String name, String value) { + Long ret = Long.parseLong(value); + if (ret < 0) { + throw new IllegalArgumentException("Invalid value for -" + name + ": " + ret); + } + return ret; + } + + public Long parseLongPositive(String name, String value) { + Long ret = Long.parseLong(value); + if (ret <= 0) { + throw new IllegalArgumentException("Invalid value for -" + name + ": " + ret); + } + return ret; + } + + public Double parseDoubleRatio(String name, String value) { + Double ret = Double.parseDouble(value); + if (ret < 0 || ret > 1) { + throw new IllegalArgumentException("Invalid value for -" + name + ": " + ret); + } + return ret; + } + + static boolean isPowerOfTwo(long n) { + return (int) (Math.ceil((Math.log(n) / Math.log(2)))) + == (int) (Math.floor(((Math.log(n) / Math.log(2))))); + } + + protected abstract Object parseValue(String value); + + private final Object defaultValue; + private final String description; + } + + abstract class BenchmarkTask implements Callable { + protected int tid; + + public BenchmarkTask(int tid) { + this.tid = tid; + } + + @Override + public Void call() throws Exception { + run(); + return null; + } + + protected abstract void run() throws KVDKException; + + protected long generateKey() { + switch (keyDistribution) { + case Range: + return rangeKeyGenerators[tid].gen(); + case Uniform: + return randomKeyGenerators[tid].gen(); + default: + throw new UnsupportedOperationException(); + } + } + + protected long generateValueSize() { + switch (valueSizeDistribution) { + case Constant: + return constantValueSizeGenerators[tid].gen(); + case Uniform: + return randomValueSizeGenerators[tid].gen(); + default: + throw new UnsupportedOperationException(); + } + } + + public byte[] longToBytes(long x) { + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + buffer.putLong(x); + return buffer.array(); + } + } + + private class WriteTask extends BenchmarkTask { + public WriteTask(int tid) { + super(tid); + } + + @Override + public void run() throws KVDKException { + WriteBatch batch = null; + if (kvdkEngine != null) { + batch = kvdkEngine.writeBatchCreate(); + } + + for (long operations = 0; operations < numOperationsPerThread; operations++) { + if (isTimeout.get()) { + break; + } + + long num = generateKey(); + byte[] key = longToBytes(num); + int generatedValueSize = (int) generateValueSize(); + + long start = 0; + if (latency) { + start = System.nanoTime(); + } + + switch (dataType) { + case String: + if (batchSize == 0) { + kvdkEngine.put(key, 0, key.length, valuePool, 0, generatedValueSize); + } else { + batch.stringPut(key, 0, key.length, valuePool, 0, generatedValueSize); + if ((operations + 1) % batchSize == 0) { + kvdkEngine.batchWrite(batch); + batch.clear(); + } + } + break; + case Sorted: + int cid = (int) (num % numCollection); + if (batchSize == 0) { + kvdkEngine.sortedPut( + collectionNameHandles[cid], + key, + 0, + key.length, + valuePool, + 0, + generatedValueSize); + } else { + batch.sortedPut( + collectionNameHandles[cid], + key, + 0, + key.length, + valuePool, + 0, + generatedValueSize); + if ((operations + 1) % batchSize == 0) { + kvdkEngine.batchWrite(batch); + batch.clear(); + } + } + break; + case Blackhole: + break; + default: + throw new UnsupportedOperationException(); + } + + if (latency) { + long lat = System.nanoTime() - start; + if (lat / 100 >= MAX_LATENCY) { + throw new RuntimeException("Write latency overflow"); + } + latencies[tid][(int) (lat / 100)]++; + } + + if ((operations + 1) % 1000 == 0) { + writeOps.addAndGet(1000); + } + } + + if (batch != null) { + batch.close(); + } + + finishedThreads.incrementAndGet(); + } + } + + private class ReadTask extends BenchmarkTask { + public ReadTask(int tid) { + super(tid); + } + + @Override + public void run() throws KVDKException { + long readNotFound = 0; + for (long operations = 0; operations < numOperationsPerThread; operations++) { + if (isTimeout.get()) { + break; + } + + long num = generateKey(); + byte[] key = longToBytes(num); + byte[] value = null; + + long start = 0; + if (latency) { + start = System.nanoTime(); + } + + switch (dataType) { + case String: + value = kvdkEngine.get(key); + break; + case Sorted: + int cid = (int) (num % numCollection); + value = kvdkEngine.sortedGet(collectionNameHandles[cid], key); + break; + case Blackhole: + break; + default: + throw new UnsupportedOperationException(); + } + + if (latency) { + long lat = System.nanoTime() - start; + if (lat / 100 >= MAX_LATENCY) { + throw new RuntimeException("Read latency overflow"); + } + latencies[tid][(int) (lat / 100)]++; + } + + if (value == null && !dataType.equals(DataType.Blackhole)) { + // Not found + if (++readNotFound % 1000 == 0) { + readNotFoundCount.addAndGet(1000); + } + } + + if ((operations + 1) % 1000 == 0) { + readOps.addAndGet(1000); + } + } + + finishedThreads.incrementAndGet(); + } + } + + private class ScanTask extends BenchmarkTask { + public ScanTask(int tid) { + super(tid); + } + + @Override + public void run() throws KVDKException { + long readNotFound = 0; + for (long operations = 0, operations_counted = 0; + operations < numOperationsPerThread; ) { + if (isTimeout.get()) { + break; + } + + long num = generateKey(); + byte[] key = longToBytes(num); + byte[] value = null; + + final int scanLength = 100; + switch (dataType) { + case Sorted: + int cid = (int) (num % numCollection); + Iterator iter = kvdkEngine.sortedIteratorCreate(collectionNameHandles[cid]); + iter.seek(key); + for (int i = 0; i < scanLength && iter.isValid(); i++, iter.next()) { + key = iter.key(); + value = iter.value(); + + operations++; + if (operations >= operations_counted + 1000) { + readOps.addAndGet(operations - operations_counted); + operations_counted = operations; + } + } + kvdkEngine.sortedIteratorRelease(iter); + + break; + case Blackhole: + operations += 1024; + readOps.addAndGet(1024); + break; + case String: + default: + throw new UnsupportedOperationException(); + } + + if ((operations + 1) % 1000 == 0) { + writeOps.addAndGet(1000); + } + } + + finishedThreads.incrementAndGet(); + } + } + + public static void printHelp() { + System.out.println("usage:"); + for (Flag flag : Flag.values()) { + System.out.format(" -%s%n\t%s%n", flag.name(), flag.getDescription()); + if (flag.getDefaultValue() != null) { + System.out.format("\tDefault: %s%n", flag.getDefaultValue().toString()); + } + } + } + + private static Map parseArgs(String[] args) { + Map flags = new EnumMap(Flag.class); + for (Flag flag : Flag.values()) { + if (flag.getDefaultValue() != null) { + flags.put(flag, flag.getDefaultValue()); + } + } + + for (String arg : args) { + boolean valid = false; + if (arg.equals("-h") || arg.equals("--help") || arg.equals("-?") || arg.equals("?")) { + printHelp(); + System.exit(0); + } + if (arg.startsWith("-")) { + try { + String[] parts = arg.substring(1).split("="); + if (parts.length >= 1) { + Flag key = Flag.valueOf(parts[0]); + if (key != null) { + Object value = null; + if (parts.length >= 2) { + value = key.parseValue(parts[1]); + } + flags.put(key, value); + valid = true; + } + } + } catch (Exception e) { + } + } + if (!valid) { + System.err.println("Invalid argument: " + arg); + System.err.println("Run with -h to show help information"); + System.exit(1); + } + } + + return flags; + } + + public static void main(String[] args) throws Exception { + Map flags = parseArgs(args); + new KVDKBenchmark(flags).run(); + } +} diff --git a/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java new file mode 100644 index 00000000..17669d51 --- /dev/null +++ b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/ConstantLongGenerator.java @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk.benchmark.util; + +public class ConstantLongGenerator implements LongGenerator { + private long value; + + public ConstantLongGenerator(long value) { + this.value = value; + } + + @Override + public long gen() { + return value; + } +} diff --git a/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java new file mode 100644 index 00000000..fd3f9165 --- /dev/null +++ b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/LongGenerator.java @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk.benchmark.util; + +public interface LongGenerator { + long gen(); +} diff --git a/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java new file mode 100644 index 00000000..76b6260f --- /dev/null +++ b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RandomLongGenerator.java @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk.benchmark.util; + +import java.util.SplittableRandom; + +public class RandomLongGenerator implements LongGenerator { + private SplittableRandom random; + private long low; + private long high; + + public RandomLongGenerator(long low, long high) { + this.low = low; + this.high = high; + this.random = new SplittableRandom(); + } + + @Override + public long gen() { + return random.nextLong(low, high); + } +} diff --git a/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java new file mode 100644 index 00000000..ccf54180 --- /dev/null +++ b/volatile/java/benchmark/src/main/java/io/pmem/kvdk/benchmark/util/RangeLongGenerator.java @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk.benchmark.util; + +public class RangeLongGenerator implements LongGenerator { + private long low; + private long high; + private long step; + private long curr; + + public RangeLongGenerator(long low, long high, long step) { + this.low = low; + this.high = high; + this.step = step; + this.curr = this.low; + } + + @Override + public long gen() { + long old = curr; + curr += step; + + assert (old < high); + return old; + } +} diff --git a/volatile/java/examples/pom.xml b/volatile/java/examples/pom.xml new file mode 100644 index 00000000..18b3b831 --- /dev/null +++ b/volatile/java/examples/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + io.pmem + kvdkjni-examples + 1.0.0-SNAPSHOT + + KVDK JNI Examples + KVDK JNI Examples. + https://github.com/pmem/kvdk + 2021 + + + + BSD-3-Clause + hhttps://github.com/pmem/kvdk/blob/main/LICENSE + repo + + + + + scm:git:https://github.com/pmem/kvdk.git + scm:git:https://github.com/pmem/kvdk.git + scm:git:https://github.com/pmem/kvdk.git + + + + Intel + https://www.intel.com + + + + 1.0.0-SNAPSHOT + 1.8 + 1.8 + UTF-8 + 2.7.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + ${project.build.source} + ${project.build.target} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + target + -Djava.library.path=${project.build.directory} + false + false + + ${project.build.directory}/* + + + 1 + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless.version} + + + + 1.7 + + + + + + + spotless-check + compile + + check + + + + + + + + + + io.pmem + kvdkjni + ${kvdkjni.version} + + + diff --git a/volatile/java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java b/volatile/java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java new file mode 100644 index 00000000..e075d00d --- /dev/null +++ b/volatile/java/examples/src/main/java/io/pmem/kvdk/exmaples/KVDKExamples.java @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk.examples; + +import io.pmem.kvdk.Configs; +import io.pmem.kvdk.Engine; +import io.pmem.kvdk.Iterator; +import io.pmem.kvdk.KVDKException; +import io.pmem.kvdk.NativeBytesHandle; +import io.pmem.kvdk.WriteBatch; + +public class KVDKExamples { + protected Configs engineConfigs; + protected Engine kvdkEngine; + + public void prepare() throws KVDKException { + engineConfigs = new Configs(); + engineConfigs.setHashBucketNum(1L << 10); + engineConfigs.setMaxAccessThreads(4); + + kvdkEngine = Engine.open("/tmp/kvdk-test-dir", engineConfigs); + engineConfigs.close(); + } + + public void runAnonymousCollection() throws KVDKException { + System.out.println(); + System.out.println("Output of runAnonymousCollection: "); + + // put + String key = "sssss"; + String value = "22222"; + kvdkEngine.put(key.getBytes(), value.getBytes()); + + // expire + kvdkEngine.expire(key.getBytes(), 1000); + + // get + System.out.println("value: " + new String(kvdkEngine.get(key.getBytes()))); + + // delete + kvdkEngine.delete(key.getBytes()); + } + + public void runSortedCollection() throws KVDKException { + System.out.println(); + System.out.println("Output of runSortedCollection: "); + + String name = "collection\u0000\nname"; + NativeBytesHandle nameHandle = new NativeBytesHandle(name.getBytes()); + + // create + kvdkEngine.sortedCreate(nameHandle); + + String key1 = "key\u00001"; + String key2 = "key\u00002"; + String key3 = "key\u00003"; + + String value1 = "value\u00003"; + String value2 = "value\u00002"; + String value3 = "value\u00001"; + + // disordered put + kvdkEngine.sortedPut(nameHandle, key3.getBytes(), value3.getBytes()); + kvdkEngine.sortedPut(nameHandle, key1.getBytes(), value1.getBytes()); + kvdkEngine.sortedPut(nameHandle, key2.getBytes(), value2.getBytes()); + + // print sorted result + System.out.println("Sorted by key:"); + Iterator iter = kvdkEngine.sortedIteratorCreate(nameHandle); + for (iter.seekToFirst(); iter.isValid(); iter.next()) { + System.out.println( + new String(iter.key()).replace("\0", "[\\0]") + + ": " + + new String(iter.value()).replace("\0", "[\\0]")); + } + + // close iterator + iter.close(); + + // destroy sorted collection + kvdkEngine.sortedDestroy(nameHandle); + + // close name handle + nameHandle.close(); + } + + public void runWriteBatch() throws KVDKException { + System.out.println(); + System.out.println("Output of runWriteBatch: "); + + String key = "key1"; + String value1 = "value1"; + String value2 = "value2"; + + // batch + WriteBatch batch = kvdkEngine.writeBatchCreate(); + batch.stringPut(key.getBytes(), value1.getBytes()); + batch.stringPut(key.getBytes(), value2.getBytes()); + + // write + kvdkEngine.batchWrite(batch); + + // get + kvdkEngine.get(key.getBytes()); + + // print + System.out.println("value: " + new String(kvdkEngine.get(key.getBytes()))); + + // delete + kvdkEngine.delete(key.getBytes()); + } + + public void close() { + kvdkEngine.close(); + } + + public static void main(String[] args) { + try { + KVDKExamples examples = new KVDKExamples(); + examples.prepare(); + examples.runAnonymousCollection(); + examples.runSortedCollection(); + examples.runWriteBatch(); + examples.close(); + } catch (KVDKException ex) { + ex.printStackTrace(); + } + } +} diff --git a/volatile/java/kvdkjni/configs.cc b/volatile/java/kvdkjni/configs.cc new file mode 100644 index 00000000..d8e3c1ff --- /dev/null +++ b/volatile/java/kvdkjni/configs.cc @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include + +#include "include/io_pmem_kvdk_Configs.h" +#include "kvdkjni/kvdkjni.h" + +/* + * Class: io_pmem_kvdk_Configs + * Method: newConfigs + * Signature: ()J + */ +jlong Java_io_pmem_kvdk_Configs_newConfigs(JNIEnv*, jclass) { + auto* cfg = new KVDK_NAMESPACE::Configs(); + return GET_CPLUSPLUS_POINTER(cfg); +} + +/* + * Class: io_pmem_kvdk_Configs + * Method: setMaxAccessThreads + * Signature: (JJ)V + */ +void Java_io_pmem_kvdk_Configs_setMaxAccessThreads(JNIEnv*, jobject, + jlong handle, jlong num) { + reinterpret_cast(handle)->max_access_threads = num; +} + +/* + * Class: io_pmem_kvdk_Configs + * Method: setHashBucketNum + * Signature: (JJ)V + */ +void Java_io_pmem_kvdk_Configs_setHashBucketNum(JNIEnv*, jobject, jlong handle, + jlong num) { + reinterpret_cast(handle)->hash_bucket_num = num; +} + +/* + * Class: io_pmem_kvdk_Configs + * Method: setDestMemoryNodes + * Signature: (JLjava/lang/String;)V + */ +void Java_io_pmem_kvdk_Configs_setDestMemoryNodes(JNIEnv* env, jobject, + jlong handle, + jstring jnodes) { + const char* nodes_chars = env->GetStringUTFChars(jnodes, nullptr); + if (nodes_chars == nullptr) { + // exception thrown: OutOfMemoryError + return; + } + std::string nodes_str = nodes_chars; + reinterpret_cast(handle)->dest_memory_nodes = + nodes_str; + env->ReleaseStringUTFChars(jnodes, nodes_chars); +} + +/* + * Class: io_pmem_kvdk_Configs + * Method: setOptLargeSortedCollectionRecovery + * Signature: (JZ)V + */ +void Java_io_pmem_kvdk_Configs_setOptLargeSortedCollectionRecovery( + JNIEnv*, jobject, jlong handle, jboolean opt) { + reinterpret_cast(handle) + ->opt_large_sorted_collection_recovery = opt; +} + +/* + * Class: io_pmem_kvdk_Configs + * Method: setCleanThreads + * Signature: (JJ)V + */ +void Java_io_pmem_kvdk_Configs_setCleanThreads(JNIEnv*, jobject, jlong handle, + jlong num_threds) { + reinterpret_cast(handle)->clean_threads = + num_threds; +} + +/* + * Class: io_pmem_kvdk_Configs + * Method: closeInternal + * Signature: (J)V + */ +void Java_io_pmem_kvdk_Configs_closeInternal(JNIEnv*, jobject, jlong handle) { + auto* cfg = reinterpret_cast(handle); + assert(cfg != nullptr); + delete cfg; +} diff --git a/volatile/java/kvdkjni/cplusplus_to_java_convert.h b/volatile/java/kvdkjni/cplusplus_to_java_convert.h new file mode 100644 index 00000000..705e797b --- /dev/null +++ b/volatile/java/kvdkjni/cplusplus_to_java_convert.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#pragma once + +#define GET_CPLUSPLUS_POINTER(_pointer) \ + static_cast(reinterpret_cast(_pointer)) diff --git a/volatile/java/kvdkjni/engine.cc b/volatile/java/kvdkjni/engine.cc new file mode 100644 index 00000000..324319bc --- /dev/null +++ b/volatile/java/kvdkjni/engine.cc @@ -0,0 +1,438 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include + +#include "include/io_pmem_kvdk_Engine.h" +#include "kvdkjni/kvdkjni.h" + +/* + * Class: io_pmem_kvdk_Engine + * Method: open + * Signature: (Ljava/lang/String;J)V + */ +jlong Java_io_pmem_kvdk_Engine_open(JNIEnv* env, jclass, jstring jengine_path, + jlong jcfg_handle) { + const char* engine_path_chars = env->GetStringUTFChars(jengine_path, nullptr); + if (engine_path_chars == nullptr) { + // exception thrown: OutOfMemoryError + return 0; + } + std::string engine_path_str = engine_path_chars; + + auto* cfg = reinterpret_cast(jcfg_handle); + KVDK_NAMESPACE::Engine* engine = nullptr; + KVDK_NAMESPACE::Status s = + KVDK_NAMESPACE::Engine::Open(engine_path_str, &engine, *cfg, stdout); + + env->ReleaseStringUTFChars(jengine_path, engine_path_chars); + + if (s == KVDK_NAMESPACE::Status::Ok) { + return GET_CPLUSPLUS_POINTER(engine); + } else { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + return 0; + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: put + * Signature: (J[BII[BIIJZ)V + */ +void Java_io_pmem_kvdk_Engine_put(JNIEnv* env, jobject, jlong handle, + jbyteArray key, jint key_off, jint key_len, + jbyteArray value, jint value_off, + jint value_len, jlong ttl_in_millis, + jboolean update_ttl_if_existed) { + auto* engine = reinterpret_cast(handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + jbyte* value_bytes = new jbyte[value_len]; + env->GetByteArrayRegion(value, value_off, value_len, value_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + delete[] value_bytes; + return; + } + + KVDK_NAMESPACE::WriteOptions write_options; + write_options.ttl_time = ttl_in_millis; + write_options.update_ttl = update_ttl_if_existed; + + auto s = + engine->Put(std::string(reinterpret_cast(key_bytes), key_len), + std::string(reinterpret_cast(value_bytes), value_len), + write_options); + + delete[] key_bytes; + delete[] value_bytes; + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: expire + * Signature: (J[BIIJ)V + */ +void Java_io_pmem_kvdk_Engine_expire__J_3BIIJ(JNIEnv* env, jobject, + jlong handle, jbyteArray key, + jint key_off, jint key_len, + jlong ttl_in_millis) { + auto* engine = reinterpret_cast(handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + auto s = engine->Expire( + std::string(reinterpret_cast(key_bytes), key_len), ttl_in_millis); + + delete[] key_bytes; + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: expire + * Signature: (JJIJ)V + */ +void Java_io_pmem_kvdk_Engine_expire__JJIJ(JNIEnv* env, jobject, + jlong engine_handle, + jlong name_handle, jint name_len, + jlong ttl_in_millis) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + auto s = engine->Expire(std::string(name_chars, name_len), ttl_in_millis); + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: get + * Signature: (J[BII)[B + */ +jbyteArray Java_io_pmem_kvdk_Engine_get(JNIEnv* env, jobject, jlong handle, + jbyteArray key, jint key_off, + jint key_len) { + auto* engine = reinterpret_cast(handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return nullptr; + } + + std::string value; + auto s = engine->Get(std::string(reinterpret_cast(key_bytes), key_len), + &value); + + delete[] key_bytes; + + if (s == KVDK_NAMESPACE::Status::NotFound) { + return nullptr; + } + + if (s == KVDK_NAMESPACE::Status::Ok) { + jbyteArray ret = KVDK_NAMESPACE::JniUtil::createJavaByteArray( + env, value.c_str(), value.size()); + return ret; + } + + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + return nullptr; +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: delete + * Signature: (J[BII)V + */ +void Java_io_pmem_kvdk_Engine_delete(JNIEnv* env, jobject, jlong handle, + jbyteArray key, jint key_off, + jint key_len) { + auto* engine = reinterpret_cast(handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + auto s = + engine->Delete(std::string(reinterpret_cast(key_bytes), key_len)); + + delete[] key_bytes; + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: sortedCreate + * Signature: (JJI)V + */ +void Java_io_pmem_kvdk_Engine_sortedCreate(JNIEnv* env, jobject, + jlong engine_handle, + jlong name_handle, jint name_len) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + auto s = engine->SortedCreate(std::string(name_chars, name_len)); + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: sortedDestroy + * Signature: (JJI)V + */ +void Java_io_pmem_kvdk_Engine_sortedDestroy(JNIEnv* env, jobject, + jlong engine_handle, + jlong name_handle, jint name_len) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + auto s = engine->SortedDestroy(std::string(name_chars, name_len)); + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: sortedSize + * Signature: (JJI)J + */ +jlong Java_io_pmem_kvdk_Engine_sortedSize(JNIEnv* env, jobject, + jlong engine_handle, + jlong name_handle, jint name_len) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + size_t size = 0; + auto s = engine->SortedSize(std::string(name_chars, name_len), &size); + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } + + return size; +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: sortedPut + * Signature: (JJI[BII[BII)V + */ +void Java_io_pmem_kvdk_Engine_sortedPut(JNIEnv* env, jobject, + jlong engine_handle, jlong name_handle, + jint name_len, jbyteArray key, + jint key_off, jint key_len, + jbyteArray value, jint value_off, + jint value_len) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + jbyte* value_bytes = new jbyte[value_len]; + env->GetByteArrayRegion(value, value_off, value_len, value_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + delete[] value_bytes; + return; + } + + auto s = engine->SortedPut( + std::string(name_chars, name_len), + std::string(reinterpret_cast(key_bytes), key_len), + std::string(reinterpret_cast(value_bytes), value_len)); + + delete[] key_bytes; + delete[] value_bytes; + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: sortedGet + * Signature: (JJI[BII)[B + */ +jbyteArray Java_io_pmem_kvdk_Engine_sortedGet(JNIEnv* env, jobject, + jlong engine_handle, + jlong name_handle, jint name_len, + jbyteArray key, jint key_off, + jint key_len) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return nullptr; + } + + std::string value; + auto s = engine->SortedGet( + std::string(name_chars, name_len), + std::string(reinterpret_cast(key_bytes), key_len), &value); + + delete[] key_bytes; + + if (s == KVDK_NAMESPACE::Status::NotFound) { + return nullptr; + } + + if (s == KVDK_NAMESPACE::Status::Ok) { + jbyteArray ret = KVDK_NAMESPACE::JniUtil::createJavaByteArray( + env, value.c_str(), value.size()); + return ret; + } + + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + return nullptr; +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: sortedDelete + * Signature: (JJI[BII)V + */ +void Java_io_pmem_kvdk_Engine_sortedDelete(JNIEnv* env, jobject, + jlong engine_handle, + jlong name_handle, jint name_len, + jbyteArray key, jint key_off, + jint key_len) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + auto s = engine->SortedDelete( + std::string(name_chars, name_len), + std::string(reinterpret_cast(key_bytes), key_len)); + + delete[] key_bytes; + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: sortedIteratorCreate + * Signature: (JJI)J + */ +jlong Java_io_pmem_kvdk_Engine_sortedIteratorCreate(JNIEnv* env, jobject, + jlong engine_handle, + jlong name_handle, + jint name_len) { + auto* engine = reinterpret_cast(engine_handle); + auto* name_chars = reinterpret_cast(name_handle); + + KVDK_NAMESPACE::Status s; + KVDK_NAMESPACE::SortedIterator* iter = engine->SortedIteratorCreate( + std::string(name_chars, name_len), nullptr, &s); + + if (s == KVDK_NAMESPACE::Status::Ok) { + return GET_CPLUSPLUS_POINTER(iter); + } else { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + return 0; + } +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: closeInternal + * Signature: (J)V + */ +void Java_io_pmem_kvdk_Engine_closeInternal(JNIEnv*, jobject, jlong handle) { + auto* engine = reinterpret_cast(handle); + assert(engine != nullptr); + delete engine; +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: writeBatchCreate + * Signature: (J)J + */ +jlong Java_io_pmem_kvdk_Engine_writeBatchCreate(JNIEnv*, jobject, + jlong handle) { + auto* engine = reinterpret_cast(handle); + std::unique_ptr ptr = engine->WriteBatchCreate(); + return GET_CPLUSPLUS_POINTER(ptr.release()); +} + +/* + * Class: io_pmem_kvdk_Engine + * Method: batchWrite + * Signature: (JJ)V + */ +void Java_io_pmem_kvdk_Engine_batchWrite(JNIEnv* env, jobject, + jlong engine_handle, + jlong batch_handle) { + auto* engine = reinterpret_cast(engine_handle); + auto* batch = reinterpret_cast(batch_handle); + std::unique_ptr ptr(batch); + auto s = engine->BatchWrite(ptr); + ptr.release(); + + if (s != KVDK_NAMESPACE::Status::Ok) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew(env, s); + } +} diff --git a/volatile/java/kvdkjni/iterator.cc b/volatile/java/kvdkjni/iterator.cc new file mode 100644 index 00000000..e7631ec7 --- /dev/null +++ b/volatile/java/kvdkjni/iterator.cc @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include + +#include "include/io_pmem_kvdk_Iterator.h" +#include "kvdkjni/kvdkjni.h" + +/* + * Class: io_pmem_kvdk_Iterator + * Method: closeInternal + * Signature: (J)V + */ +void Java_io_pmem_kvdk_Iterator_closeInternal(JNIEnv*, jobject, + jlong iterator_handle, + jlong engine_handle) { + auto* iterator = + reinterpret_cast(iterator_handle); + auto* engine = reinterpret_cast(engine_handle); + assert(iterator != nullptr); + assert(engine != nullptr); + + engine->SortedIteratorRelease(iterator); +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: seek + * Signature: (J[BII)V + */ +void Java_io_pmem_kvdk_Iterator_seek(JNIEnv* env, jobject, jlong handle, + jbyteArray key, jint key_off, + jint key_len) { + auto* iterator = reinterpret_cast(handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + iterator->Seek(std::string(reinterpret_cast(key_bytes), key_len)); + + delete[] key_bytes; +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: seekToFirst + * Signature: (J)V + */ +void Java_io_pmem_kvdk_Iterator_seekToFirst(JNIEnv*, jobject, jlong handle) { + auto* iterator = reinterpret_cast(handle); + iterator->SeekToFirst(); +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: seekToLast + * Signature: (J)V + */ +void Java_io_pmem_kvdk_Iterator_seekToLast(JNIEnv*, jobject, jlong handle) { + auto* iterator = reinterpret_cast(handle); + iterator->SeekToLast(); +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: isValid + * Signature: (J)Z + */ +jboolean Java_io_pmem_kvdk_Iterator_isValid(JNIEnv*, jobject, jlong handle) { + auto* iterator = reinterpret_cast(handle); + return iterator->Valid(); +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: next + * Signature: (J)V + */ +void Java_io_pmem_kvdk_Iterator_next(JNIEnv*, jobject, jlong handle) { + auto* iterator = reinterpret_cast(handle); + iterator->Next(); +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: prev + * Signature: (J)V + */ +void Java_io_pmem_kvdk_Iterator_prev(JNIEnv*, jobject, jlong handle) { + auto* iterator = reinterpret_cast(handle); + iterator->Prev(); +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: key + * Signature: (J)[B + */ +jbyteArray Java_io_pmem_kvdk_Iterator_key(JNIEnv* env, jobject, jlong handle) { + auto* iterator = reinterpret_cast(handle); + + std::string key = iterator->Key(); + jbyteArray ret = KVDK_NAMESPACE::JniUtil::createJavaByteArray( + env, key.c_str(), key.size()); + return ret; +} + +/* + * Class: io_pmem_kvdk_Iterator + * Method: value + * Signature: (J)[B + */ +jbyteArray Java_io_pmem_kvdk_Iterator_value(JNIEnv* env, jobject, + jlong handle) { + auto* iterator = reinterpret_cast(handle); + + std::string value = iterator->Value(); + jbyteArray ret = KVDK_NAMESPACE::JniUtil::createJavaByteArray( + env, value.c_str(), value.size()); + return ret; +} diff --git a/volatile/java/kvdkjni/kvdkjni.h b/volatile/java/kvdkjni/kvdkjni.h new file mode 100644 index 00000000..f87696c6 --- /dev/null +++ b/volatile/java/kvdkjni/kvdkjni.h @@ -0,0 +1,322 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#ifndef JAVA_KVDKJNI_KVDKJNI_H_ +#define JAVA_KVDKJNI_KVDKJNI_H_ + +#include +#include + +#include + +#include "kvdk/volatile/engine.hpp" +#include "kvdkjni/cplusplus_to_java_convert.h" + +namespace KVDK_NAMESPACE { + +// Helper class to get Java class by name from C++. +class JavaClass { + public: + /* + * Gets and initializes a Java Class + * + * @param env A pointer to the Java environment + * @param jclazz_name The fully qualified JNI name of the Java Class + * e.g. "java/lang/String" + * + * @return The Java Class or nullptr if one of the + * ClassFormatError, ClassCircularityError, NoClassDefFoundError, + * OutOfMemoryError or ExceptionInInitializerError exceptions is thrown + */ + static jclass getJClass(JNIEnv* env, const char* jclazz_name) { + jclass jclazz = env->FindClass(jclazz_name); + assert(jclazz != nullptr); + return jclazz; + } +}; + +// The portal class for io.pmem.kvdk.Status +class StatusJni : public JavaClass { + public: + /* + * Get the Java Class io.pmem.kvdk.Status + * + * @param env A pointer to the Java environment + * + * @return The Java Class or nullptr if one of the + * ClassFormatError, ClassCircularityError, NoClassDefFoundError, + * OutOfMemoryError or ExceptionInInitializerError exceptions is thrown + */ + static jclass getJClass(JNIEnv* env) { + return JavaClass::getJClass(env, "io/pmem/kvdk/Status"); + } + + /* + * Create a new Java io.pmem.kvdk.Status object with the same properties as + * the provided C++ KVDK_NAMESPACE::Status object + * + * @param env A pointer to the Java environment + * @param status The KVDK_NAMESPACE::Status object + * + * @return A reference to a Java io.pmem.kvdk.Status object, or nullptr + * if an an exception occurs + */ + static jobject construct(JNIEnv* env, const Status& status) { + jclass jclazz = getJClass(env); + if (jclazz == nullptr) { + // exception occurred accessing class + return nullptr; + } + + jmethodID mid = env->GetMethodID(jclazz, "", "(B)V"); + if (mid == nullptr) { + // exception thrown: NoSuchMethodException or OutOfMemoryError + return nullptr; + } + + jobject jstatus = env->NewObject(jclazz, mid, toJavaStatusCode(status)); + if (env->ExceptionCheck()) { + // exception occurred + return nullptr; + } + + return jstatus; + } + + static jobject construct(JNIEnv* env, const Status* status) { + return construct(env, *status); + } + + // Returns the equivalent io.pmem.kvdk.Status.Code for the provided + // C++ KVDK_NAMESPACE::Status enum + static jbyte toJavaStatusCode(const KVDK_NAMESPACE::Status& status) { + switch (status) { + case KVDK_NAMESPACE::Status::Ok: + return 0x0; + case KVDK_NAMESPACE::Status::NotFound: + return 0x1; + case KVDK_NAMESPACE::Status::Outdated: + return 0x2; + case KVDK_NAMESPACE::Status::WrongType: + return 0x3; + case KVDK_NAMESPACE::Status::Existed: + return 0x4; + case KVDK_NAMESPACE::Status::OperationFail: + return 0x5; + case KVDK_NAMESPACE::Status::OutOfRange: + return 0x6; + case KVDK_NAMESPACE::Status::MemoryOverflow: + return 0x7; + case KVDK_NAMESPACE::Status::NotSupported: + return 0x8; + case KVDK_NAMESPACE::Status::InvalidBatchSize: + return 0x9; + case KVDK_NAMESPACE::Status::InvalidDataSize: + return 0xA; + case KVDK_NAMESPACE::Status::InvalidArgument: + return 0xB; + case KVDK_NAMESPACE::Status::IOError: + return 0xC; + case KVDK_NAMESPACE::Status::InvalidConfiguration: + return 0xD; + case KVDK_NAMESPACE::Status::Fail: + return 0xE; + case KVDK_NAMESPACE::Status::Abort: + return 0xF; + default: + return 0x7F; // undefined + } + } +}; + +// Java Exception template +template +class JavaException : public JavaClass { + public: + /* + * Create and throw a java exception with the provided message + * + * @param env A pointer to the Java environment + * @param msg The message for the exception + * + * @return true if an exception was thrown, false otherwise + */ + static bool ThrowNew(JNIEnv* env, const std::string& msg) { + jclass jclazz = DERIVED::getJClass(env); + if (jclazz == nullptr) { + // exception occurred accessing class + std::cerr << "JavaException::ThrowNew - Error: unexpected exception!" + << std::endl; + return env->ExceptionCheck(); + } + + const jint rs = env->ThrowNew(jclazz, msg.c_str()); + if (rs != JNI_OK) { + // exception could not be thrown + std::cerr << "JavaException::ThrowNew - Fatal: could not throw exception!" + << std::endl; + return env->ExceptionCheck(); + } + + return true; + } +}; + +// The portal class for io.pmem.kvdk.KVDKException +class KVDKExceptionJni : public JavaException { + public: + /* + * Get the Java Class io.pmem.kvdk.KVDKException + * + * @param env A pointer to the Java environment + * + * @return The Java Class or nullptr if one of the + * ClassFormatError, ClassCircularityError, NoClassDefFoundError, + * OutOfMemoryError or ExceptionInInitializerError exceptions is thrown + */ + static jclass getJClass(JNIEnv* env) { + return JavaException::getJClass(env, "io/pmem/kvdk/KVDKException"); + } + + /* + * Create and throw a Java KVDKException with the provided message + * + * @param env A pointer to the Java environment + * @param msg The message for the exception + * + * @return true if an exception was thrown, false otherwise + */ + static bool ThrowNew(JNIEnv* env, const std::string& msg) { + return JavaException::ThrowNew(env, msg); + } + + /* + * Create and throw a Java KVDKException with the provided status + * + * If s == Status::Ok, then this function will not throw any exception. + * + * @param env A pointer to the Java environment + * @param s The status for the exception + * + * @return true if an exception was thrown, false otherwise + */ + static bool ThrowNew(JNIEnv* env, const Status& s) { + if (s == Status::Ok) { + return false; + } + + // get the KVDKException class + jclass jclazz = getJClass(env); + if (jclazz == nullptr) { + // exception occurred accessing class + std::cerr + << "KVDKExceptionJni::ThrowNew/class - Error: unexpected exception!" + << std::endl; + return env->ExceptionCheck(); + } + + // get the constructor of io.pmem.kvdk.KVDKException + jmethodID mid = + env->GetMethodID(jclazz, "", "(Lio/pmem/kvdk/Status;)V"); + if (mid == nullptr) { + // exception thrown: NoSuchMethodException or OutOfMemoryError + std::cerr + << "KVDKExceptionJni::ThrowNew/cstr - Error: unexpected exception!" + << std::endl; + return env->ExceptionCheck(); + } + + // get the Java status object + jobject jstatus = StatusJni::construct(env, s); + if (jstatus == nullptr) { + // exception occcurred + std::cerr << "KVDKExceptionJni::ThrowNew/StatusJni - Error: unexpected " + "exception!" + << std::endl; + return env->ExceptionCheck(); + } + + // construct the KVDKException + jthrowable kvdk_exception = + reinterpret_cast(env->NewObject(jclazz, mid, jstatus)); + if (env->ExceptionCheck()) { + if (jstatus != nullptr) { + env->DeleteLocalRef(jstatus); + } + if (kvdk_exception != nullptr) { + env->DeleteLocalRef(kvdk_exception); + } + std::cerr << "KVDKExceptionJni::ThrowNew/NewObject - Error: unexpected " + "exception!" + << std::endl; + return true; + } + + // throw the KVDKException + const jint rs = env->Throw(kvdk_exception); + if (rs != JNI_OK) { + // exception could not be thrown + std::cerr + << "KVDKExceptionJni::ThrowNew - Fatal: could not throw exception!" + << std::endl; + if (jstatus != nullptr) { + env->DeleteLocalRef(jstatus); + } + if (kvdk_exception != nullptr) { + env->DeleteLocalRef(kvdk_exception); + } + return env->ExceptionCheck(); + } + + if (jstatus != nullptr) { + env->DeleteLocalRef(jstatus); + } + if (kvdk_exception != nullptr) { + env->DeleteLocalRef(kvdk_exception); + } + + return true; + } +}; + +class JniUtil { + public: + static jbyteArray createJavaByteArray(JNIEnv* env, const char* bytes, + const size_t size) { + // Limitation for java array size is vm specific + // In general it cannot exceed Integer.MAX_VALUE (2^31 - 1) + // Current HotSpot VM limitation for array size is Integer.MAX_VALUE - 5 + // (2^31 - 1 - 5) It means that the next call to env->NewByteArray can still + // end with OutOfMemoryError("Requested array size exceeds VM limit") coming + // from VM + static const size_t MAX_JARRAY_SIZE = (static_cast(1)) << 31; + if (size > MAX_JARRAY_SIZE) { + KVDK_NAMESPACE::KVDKExceptionJni::ThrowNew( + env, "Requested array size exceeds VM limit"); + return nullptr; + } + + const jsize jlen = static_cast(size); + jbyteArray jbytes = env->NewByteArray(jlen); + if (jbytes == nullptr) { + // exception thrown: OutOfMemoryError + return nullptr; + } + + env->SetByteArrayRegion( + jbytes, 0, jlen, + const_cast(reinterpret_cast(bytes))); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + env->DeleteLocalRef(jbytes); + return nullptr; + } + + return jbytes; + } +}; + +} // namespace KVDK_NAMESPACE + +#endif // JAVA_KVDKJNI_KVDKJNI_H_ diff --git a/volatile/java/kvdkjni/native_bytes_handle.cc b/volatile/java/kvdkjni/native_bytes_handle.cc new file mode 100644 index 00000000..ae877c7b --- /dev/null +++ b/volatile/java/kvdkjni/native_bytes_handle.cc @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include + +#include "include/io_pmem_kvdk_NativeBytesHandle.h" +#include "kvdkjni/kvdkjni.h" + +/* + * Class: io_pmem_kvdk_NativeBytesHandle + * Method: newNativeBytes + * Signature: ([B)J + */ +jlong Java_io_pmem_kvdk_NativeBytesHandle_newNativeBytes(JNIEnv* env, jclass, + jbyteArray bytes) { + int len = env->GetArrayLength(bytes); + jbyte* b = new jbyte[len]; + env->GetByteArrayRegion(bytes, 0, len, b); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] b; + return 0; + } + + return GET_CPLUSPLUS_POINTER(b); +} + +/* + * Class: io_pmem_kvdk_NativeBytesHandle + * Method: closeInternal + * Signature: (J)V + */ +void Java_io_pmem_kvdk_NativeBytesHandle_closeInternal(JNIEnv*, jobject, + jlong handle) { + auto* b = reinterpret_cast(handle); + assert(b != nullptr); + delete[] b; +} diff --git a/volatile/java/kvdkjni/write_batch.cc b/volatile/java/kvdkjni/write_batch.cc new file mode 100644 index 00000000..6e0c9008 --- /dev/null +++ b/volatile/java/kvdkjni/write_batch.cc @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include + +#include "include/io_pmem_kvdk_WriteBatch.h" +#include "kvdkjni/kvdkjni.h" + +/* + * Class: io_pmem_kvdk_WriteBatch + * Method: closeInternal + * Signature: (J)V + */ +void Java_io_pmem_kvdk_WriteBatch_closeInternal(JNIEnv*, jobject, + jlong batch_handle) { + auto* batch = reinterpret_cast(batch_handle); + assert(batch != nullptr); + delete batch; +} + +/* + * Class: io_pmem_kvdk_WriteBatch + * Method: stringPut + * Signature: (J[BII[BII)V + */ +void Java_io_pmem_kvdk_WriteBatch_stringPut(JNIEnv* env, jobject, + jlong batch_handle, jbyteArray key, + jint key_off, jint key_len, + jbyteArray value, jint value_off, + jint value_len) { + auto* batch = reinterpret_cast(batch_handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + jbyte* value_bytes = new jbyte[value_len]; + env->GetByteArrayRegion(value, value_off, value_len, value_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + delete[] value_bytes; + return; + } + + batch->StringPut( + std::string(reinterpret_cast(key_bytes), key_len), + std::string(reinterpret_cast(value_bytes), value_len)); + + delete[] key_bytes; + delete[] value_bytes; +} + +/* + * Class: io_pmem_kvdk_WriteBatch + * Method: stringDelete + * Signature: (J[BII)V + */ +void Java_io_pmem_kvdk_WriteBatch_stringDelete(JNIEnv* env, jobject, + jlong batch_handle, + jbyteArray key, jint key_off, + jint key_len) { + auto* batch = reinterpret_cast(batch_handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + batch->StringDelete(std::string(reinterpret_cast(key_bytes), key_len)); + + delete[] key_bytes; +} + +/* + * Class: io_pmem_kvdk_WriteBatch + * Method: sortedPut + * Signature: (JJI[BII[BII)V + */ +void Java_io_pmem_kvdk_WriteBatch_sortedPut(JNIEnv* env, jobject, + jlong batch_handle, + jlong name_handle, jint name_len, + jbyteArray key, jint key_off, + jint key_len, jbyteArray value, + jint value_off, jint value_len) { + auto* batch = reinterpret_cast(batch_handle); + auto* name_chars = reinterpret_cast(name_handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + jbyte* value_bytes = new jbyte[value_len]; + env->GetByteArrayRegion(value, value_off, value_len, value_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + delete[] value_bytes; + return; + } + + batch->SortedPut( + std::string(name_chars, name_len), + std::string(reinterpret_cast(key_bytes), key_len), + std::string(reinterpret_cast(value_bytes), value_len)); + + delete[] key_bytes; + delete[] value_bytes; +} + +/* + * Class: io_pmem_kvdk_WriteBatch + * Method: sortedDelete + * Signature: (JJI[BII)V + */ +void Java_io_pmem_kvdk_WriteBatch_sortedDelete(JNIEnv* env, jobject, + jlong batch_handle, + jlong name_handle, jint name_len, + jbyteArray key, jint key_off, + jint key_len) { + auto* batch = reinterpret_cast(batch_handle); + auto* name_chars = reinterpret_cast(name_handle); + + jbyte* key_bytes = new jbyte[key_len]; + env->GetByteArrayRegion(key, key_off, key_len, key_bytes); + if (env->ExceptionCheck()) { + // exception thrown: ArrayIndexOutOfBoundsException + delete[] key_bytes; + return; + } + + batch->SortedDelete(std::string(name_chars, name_len), + std::string(reinterpret_cast(key_bytes), key_len)); + + delete[] key_bytes; +} + +/* + * Class: io_pmem_kvdk_WriteBatch + * Method: clear + * Signature: (J)V + */ +void Java_io_pmem_kvdk_WriteBatch_clear(JNIEnv*, jobject, jlong batch_handle) { + auto* batch = reinterpret_cast(batch_handle); + batch->Clear(); +} + +/* + * Class: io_pmem_kvdk_WriteBatch + * Method: size + * Signature: (J)J + */ +jlong Java_io_pmem_kvdk_WriteBatch_size(JNIEnv*, jobject, jlong batch_handle) { + auto* batch = reinterpret_cast(batch_handle); + return batch->Size(); +} \ No newline at end of file diff --git a/volatile/java/pom.xml b/volatile/java/pom.xml new file mode 100644 index 00000000..807360d2 --- /dev/null +++ b/volatile/java/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + io.pmem + kvdkjni + 1.0.0-SNAPSHOT + + KVDK JNI + KVDK fat jar that contains .so file. + + https://github.com/pmem/kvdk + 2021 + + + + BSD-3-Clause + hhttps://github.com/pmem/kvdk/blob/main/LICENSE + repo + + + + + scm:git:https://github.com/pmem/kvdk.git + scm:git:https://github.com/pmem/kvdk.git + scm:git:https://github.com/pmem/kvdk.git + + + + Intel + https://www.intel.com + + + + 1.8 + 1.8 + UTF-8 + 2.7.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + ${project.build.source} + ${project.build.target} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + target + -Djava.library.path=${project.build.directory} + false + false + + ${project.build.directory}/* + + + 1 + libasan.so.4 + detect_leaks=0:handle_segv=0 + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless.version} + + + + 1.7 + + + + + + + spotless-check + compile + + check + + + + + + + + + + junit + junit + 4.13.1 + test + + + diff --git a/volatile/java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java b/volatile/java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java new file mode 100644 index 00000000..2ec4cfe3 --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/AbstractNativeReference.java @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** Immutable reference to native C++ object. */ +public abstract class AbstractNativeReference implements AutoCloseable { + /** Indicating whether this reference is responsible to free the native C++ object. */ + protected final AtomicBoolean owningHandle_; + + /** + * Constrctor. + * + * @param owningHandle + */ + protected AbstractNativeReference(final boolean owningHandle) { + this.owningHandle_ = new AtomicBoolean(owningHandle); + } + + public boolean isOwningHandle() { + return owningHandle_.get(); + } + + /** Release the responsibility of freeing native C++ object. */ + protected final void disOwnNativeHandle() { + owningHandle_.set(false); + } + + @Override + public void close() { + if (owningHandle_.compareAndSet(true, false)) { + closeInternal(); + } + } + + protected abstract void closeInternal(); +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/Configs.java b/volatile/java/src/main/java/io/pmem/kvdk/Configs.java new file mode 100644 index 00000000..aee992de --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/Configs.java @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +/** KVDK engine configs. */ +public class Configs extends KVDKObject { + static { + Engine.loadLibrary(); + } + + public Configs() { + super(newConfigs()); + } + + public Configs setMaxAccessThreads(final long num_threads) { + setMaxAccessThreads(nativeHandle_, num_threads); + return this; + } + + public Configs setHashBucketNum(final long num) { + setHashBucketNum(nativeHandle_, num); + return this; + } + + public Configs setDestMemoryNodes(final String nodes) { + setDestMemoryNodes(nativeHandle_, nodes); + return this; + } + + public Configs setOptLargeSortedCollectionRecovery(final boolean opt) { + setOptLargeSortedCollectionRecovery(nativeHandle_, opt); + return this; + } + + public Configs setUseDevDaxMode(final boolean use) { + setUseDevDaxMode(nativeHandle_, use); + return this; + } + + public Configs setCleanThreads(final long num_threads) { + setCleanThreads(nativeHandle_, num_threads); + return this; + } + + // Native methods + private static native long newConfigs(); + + private native void setMaxAccessThreads(long handle, long num); + + private native void setHashBucketNum(long handle, long num); + + private native void setDestMemoryNodes(long handle, String nodes); + + private native void setOptLargeSortedCollectionRecovery(long handle, boolean opt); + + private native void setUseDevDaxMode(long handle, boolean use); + + private native void setCleanThreads(long handle, long num); + + @Override + protected final native void closeInternal(long handle); +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/Engine.java b/volatile/java/src/main/java/io/pmem/kvdk/Engine.java new file mode 100644 index 00000000..ec952aa9 --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/Engine.java @@ -0,0 +1,426 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** The KVDK engine, providing the APIs for Key-Value operations. */ +public class Engine extends KVDKObject { + private static final String atomicLibraryFileName = "libatomic.so.1"; + private static final String stdLibraryFileName = "libstdc++.so.6"; + private static final String jniLibraryFileName = System.mapLibraryName("kvdkjni"); + + /** + * Current state of loading native library. This must be defined before calling loading + * loadLibrary(). + */ + private static final AtomicReference libraryLoaded = + new AtomicReference<>(LibraryState.NOT_LOADED); + + static { + Engine.loadLibrary(); + } + + protected Engine(final long nativeHandle) { + super(nativeHandle); + } + + /** + * Open a KVDK engine instance. + * + * @param path A directory to PMem + * @param configs KVDK engine configs + * @return + * @throws KVDKException + */ + public static Engine open(final String path, final Configs configs) throws KVDKException { + final Engine engine = new Engine(open(path, configs.getNativeHandle())); + return engine; + } + + public void put(final byte[] key, final byte[] value) throws KVDKException { + WriteOptions defaulWriteOptions = new WriteOptions(); + put( + nativeHandle_, + key, + 0, + key.length, + value, + 0, + value.length, + defaulWriteOptions.getTtlInMillis(), + defaulWriteOptions.isUpdateTtlIfExisted()); + } + + public void put( + final byte[] key, + int keyOffset, + int keyLength, + final byte[] value, + int valueOffset, + int valueLength) + throws KVDKException { + WriteOptions defaulWriteOptions = new WriteOptions(); + put( + nativeHandle_, + key, + keyOffset, + keyLength, + value, + valueOffset, + valueLength, + defaulWriteOptions.getTtlInMillis(), + defaulWriteOptions.isUpdateTtlIfExisted()); + } + + public void put(final byte[] key, final byte[] value, final WriteOptions wOptions) + throws KVDKException { + put( + nativeHandle_, + key, + 0, + key.length, + value, + 0, + value.length, + wOptions.getTtlInMillis(), + wOptions.isUpdateTtlIfExisted()); + } + + public void put( + final byte[] key, + int keyOffset, + int keyLength, + final byte[] value, + int valueOffset, + int valueLength, + final WriteOptions wOptions) + throws KVDKException { + put( + nativeHandle_, + key, + keyOffset, + keyLength, + value, + valueOffset, + valueLength, + wOptions.getTtlInMillis(), + wOptions.isUpdateTtlIfExisted()); + } + + public void expire(final byte[] key, final long ttlInMillis) throws KVDKException { + expire(nativeHandle_, key, 0, key.length, ttlInMillis); + } + + public void expire(final byte[] key, int keyOffset, int keyLength, final long ttlInMillis) + throws KVDKException { + expire(nativeHandle_, key, keyOffset, keyLength, ttlInMillis); + } + + public void expire(final NativeBytesHandle nameHandle, final long ttlInMillis) + throws KVDKException { + expire(nativeHandle_, nameHandle.getNativeHandle(), nameHandle.getLength(), ttlInMillis); + } + + /** + * @param key + * @return Value as byte array, null if the specified key doesn't exist. + * @throws KVDKException + */ + public byte[] get(final byte[] key) throws KVDKException { + return get(nativeHandle_, key, 0, key.length); + } + + /** + * @param key + * @return Value as byte array, null if the specified key doesn't exist. + * @throws KVDKException + */ + public byte[] get(final byte[] key, int keyOffset, int keyLength) throws KVDKException { + return get(nativeHandle_, key, keyOffset, keyLength); + } + + public void delete(final byte[] key) throws KVDKException { + delete(nativeHandle_, key, 0, key.length); + } + + public void delete(final byte[] key, int keyOffset, int keyLength) throws KVDKException { + delete(nativeHandle_, key, keyOffset, keyLength); + } + + public void sortedCreate(final NativeBytesHandle nameHandle) throws KVDKException { + sortedCreate(nativeHandle_, nameHandle.getNativeHandle(), nameHandle.getLength()); + } + + public void sortedDestroy(final NativeBytesHandle nameHandle) throws KVDKException { + sortedDestroy(nativeHandle_, nameHandle.getNativeHandle(), nameHandle.getLength()); + } + + public long sortedSize(final NativeBytesHandle nameHandle) throws KVDKException { + return sortedSize(nativeHandle_, nameHandle.getNativeHandle(), nameHandle.getLength()); + } + + public void sortedPut(final NativeBytesHandle nameHandle, final byte[] key, final byte[] value) + throws KVDKException { + sortedPut( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + 0, + key.length, + value, + 0, + value.length); + } + + public void sortedPut( + final NativeBytesHandle nameHandle, + final byte[] key, + int keyOffset, + int keyLength, + final byte[] value, + int valueOffset, + int valueLength) + throws KVDKException { + sortedPut( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + keyOffset, + keyLength, + value, + valueOffset, + valueLength); + } + + /** + * @param nameHandle + * @param key + * @return Value as byte array, null if the specified key doesn't exist. + * @throws KVDKException + */ + public byte[] sortedGet(final NativeBytesHandle nameHandle, final byte[] key) + throws KVDKException { + return sortedGet( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + 0, + key.length); + } + + /** + * @param nameHandle + * @param key + * @return Value as byte array, null if the specified key doesn't exist. + * @throws KVDKException + */ + public byte[] sortedGet( + final NativeBytesHandle nameHandle, final byte[] key, int keyOffset, int keyLength) + throws KVDKException { + return sortedGet( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + keyOffset, + keyLength); + } + + public void sortedDelete(final NativeBytesHandle nameHandle, final byte[] key) + throws KVDKException { + sortedDelete( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + 0, + key.length); + } + + public void sortedDelete( + final NativeBytesHandle nameHandle, final byte[] key, int keyOffset, int keyLength) + throws KVDKException { + sortedDelete( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + keyOffset, + keyLength); + } + + public Iterator sortedIteratorCreate(final NativeBytesHandle nameHandle) throws KVDKException { + long iteratorHandle = + sortedIteratorCreate( + nativeHandle_, nameHandle.getNativeHandle(), nameHandle.getLength()); + + return new Iterator(iteratorHandle, nativeHandle_); + } + + public void sortedIteratorRelease(final Iterator iterator) throws KVDKException { + iterator.close(); + } + + public WriteBatch writeBatchCreate() { + long batchHandle = writeBatchCreate(nativeHandle_); + return new WriteBatch(batchHandle); + } + + public void batchWrite(WriteBatch batch) throws KVDKException { + batchWrite(nativeHandle_, batch.getNativeHandle()); + } + + // Native methods + @Override + protected final native void closeInternal(long handle); + + private static native long open(final String path, final long cfg_handle) throws KVDKException; + + private native void put( + long handle, + byte[] key, + int keyOffset, + int keyLength, + byte[] value, + int valueOffset, + int valueLength, + long ttl, + boolean updateTtlIfExisted); + + private native void expire( + long handle, byte[] key, int keyOffset, int keyLength, long ttlInMillis); + + private native void expire(long handle, long nameHandle, int nameLenth, long ttlInMillis); + + private native byte[] get(long handle, byte[] key, int keyOffset, int keyLength); + + private native void delete(long handle, byte[] key, int keyOffset, int keyLength); + + private native void sortedCreate(long engineHandle, long nameHandle, int nameLenth); + + private native void sortedDestroy(long engineHandle, long nameHandle, int nameLenth); + + private native long sortedSize(long engineHandle, long nameHandle, int nameLenth); + + private native void sortedPut( + long engineHandle, + long nameHandle, + int nameLenth, + byte[] key, + int keyOffset, + int keyLength, + byte[] value, + int valueOffset, + int valueLength); + + private native byte[] sortedGet( + long engineHandle, + long nameHandle, + int nameLenth, + byte[] key, + int keyOffset, + int keyLength); + + private native void sortedDelete( + long engineHandle, + long nameHandle, + int nameLenth, + byte[] key, + int keyOffset, + int keyLength); + + private native long sortedIteratorCreate(long engineHandle, long nameHandle, int nameLenth); + + private native long writeBatchCreate(long handle); + + private native void batchWrite(long engineHandle, long batchHandle); + + private enum LibraryState { + NOT_LOADED, + LOADING, + LOADED + } + + /** + * Loads the necessary library files. Calling this method twice will have no effect. By default + * the method extracts the shared library for loading at java.io.tmpdir, however, you can + * override this temporary location by setting the environment variable KVDK_SHARED_LIB_DIR. + */ + public static void loadLibrary() { + if (libraryLoaded.get() == LibraryState.LOADED) { + return; + } + + if (libraryLoaded.compareAndSet(LibraryState.NOT_LOADED, LibraryState.LOADING)) { + final String tmpDir = System.getenv("KVDK_SHARED_LIB_DIR"); + + try { + NativeLibraryLoader.getInstance().loadLibrary(tmpDir); + } catch (final IOException e) { + libraryLoaded.set(LibraryState.NOT_LOADED); + throw new RuntimeException("Unable to load the KVDK shared library", e); + } + + libraryLoaded.set(LibraryState.LOADED); + return; + } + + while (libraryLoaded.get() == LibraryState.LOADING) { + try { + Thread.sleep(10); + } catch (final InterruptedException e) { + // ignore + } + } + } + + /** + * Tries to load the necessary library files from the given list of directories. + * + * @param paths a list of strings where each describes a directory of a library. + */ + public static void loadLibrary(final List paths) { + if (libraryLoaded.get() == LibraryState.LOADED) { + return; + } + + if (libraryLoaded.compareAndSet(LibraryState.NOT_LOADED, LibraryState.LOADING)) { + boolean success = false; + UnsatisfiedLinkError err = null; + for (final String path : paths) { + try { + System.load(path + "/" + atomicLibraryFileName); + System.load(path + "/" + stdLibraryFileName); + System.load(path + "/" + jniLibraryFileName); + success = true; + break; + } catch (final UnsatisfiedLinkError e) { + err = e; + } + } + if (!success) { + libraryLoaded.set(LibraryState.NOT_LOADED); + throw err; + } + + libraryLoaded.set(LibraryState.LOADED); + return; + } + + while (libraryLoaded.get() == LibraryState.LOADING) { + try { + Thread.sleep(10); + } catch (final InterruptedException e) { + // ignore + } + } + } +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/Iterator.java b/volatile/java/src/main/java/io/pmem/kvdk/Iterator.java new file mode 100644 index 00000000..b0896cf3 --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/Iterator.java @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +/** Iterator to Key-Values in KVDK. */ +public class Iterator extends KVDKObject { + static { + Engine.loadLibrary(); + } + + private final long engineHandle; + + protected Iterator(final long iteratorHandle, final long engineHandle) { + super(iteratorHandle); + this.engineHandle = engineHandle; + } + + @Override + protected void closeInternal(long handle) { + closeInternal(handle, engineHandle); + } + + public void seek(final byte[] key) { + seek(nativeHandle_, key, 0, key.length); + } + + public void seek(final byte[] key, final int keyOffset, int keyLength) { + seek(nativeHandle_, key, keyOffset, keyLength); + } + + public void seekToFirst() { + seekToFirst(nativeHandle_); + } + + public void seekToLast() { + seekToLast(nativeHandle_); + } + + public boolean isValid() { + return isValid(nativeHandle_); + } + + public void next() { + next(nativeHandle_); + } + + public void prev() { + prev(nativeHandle_); + } + + public byte[] key() { + return key(nativeHandle_); + } + + public byte[] value() { + return value(nativeHandle_); + } + + // Native methods + protected native void closeInternal(long iteratorHandle, long engineHandle); + + private native void seek(long handle, byte[] key, int keyOffset, int keyLength); + + private native void seekToFirst(long handle); + + private native void seekToLast(long handle); + + private native boolean isValid(long handle); + + private native void next(long handle); + + private native void prev(long handle); + + private native byte[] key(long handle); + + private native byte[] value(long handle); +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/KVDKException.java b/volatile/java/src/main/java/io/pmem/kvdk/KVDKException.java new file mode 100644 index 00000000..a9b9e03e --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/KVDKException.java @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +/** Exception class defined for KVDK. */ +public class KVDKException extends Exception { + private final Status status; + + public KVDKException(final String msg) { + this(msg, null); + } + + public KVDKException(final String msg, final Status status) { + super(msg); + this.status = status; + } + + public KVDKException(final Status status) { + super(status.getMessage() != null ? status.getMessage() : status.getCode().name()); + this.status = status; + } + + public Status getStatus() { + return status; + } +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/KVDKObject.java b/volatile/java/src/main/java/io/pmem/kvdk/KVDKObject.java new file mode 100644 index 00000000..032536fd --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/KVDKObject.java @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +/** Base calss for KVDK object that hold a reference to native C++ object. */ +public abstract class KVDKObject extends AbstractNativeReference { + /** An immutable reference to the value of the C++ pointer. */ + protected final long nativeHandle_; + + protected KVDKObject(final long nativeHandle) { + super(true); + this.nativeHandle_ = nativeHandle; + } + + @Override + protected void closeInternal() { + closeInternal(nativeHandle_); + } + + protected abstract void closeInternal(final long handle); + + public long getNativeHandle() { + return nativeHandle_; + } +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java b/volatile/java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java new file mode 100644 index 00000000..7f1d0200 --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/NativeBytesHandle.java @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +/** + * This class is used to handle reference to native string view. Holding native string view avoids + * repetitive coversion of bytes from Java to C++. + */ +public class NativeBytesHandle extends KVDKObject { + static { + Engine.loadLibrary(); + } + + private final int length; + private final byte[] bytes; + + public NativeBytesHandle(final byte[] bytes) { + super(newNativeBytes(bytes)); + this.length = bytes.length; + this.bytes = bytes; + } + + public int getLength() { + return length; + } + + public byte[] getBytes() { + return bytes; + } + + // Native methods + private static native long newNativeBytes(byte[] bytes); + + @Override + protected native void closeInternal(long handle); +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java b/volatile/java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java new file mode 100644 index 00000000..a9318523 --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/NativeLibraryLoader.java @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +/** + * This class is used to load the KVDK shared library from system or jar. When loading from jar, the + * shared library is extracted to a temp folder and loaded from there. + */ +public class NativeLibraryLoader { + private static final NativeLibraryLoader instance = new NativeLibraryLoader(); + private static boolean initialized = false; + + private static final String jniLibraryName = "kvdkjni"; + + private static final String[][] requiredLibraries = { + {"libatomic.so.1", "libatomic", ".so.1"}, + {"libstdc++.so.6", "libstdc++", ".so.6"}, + {"libkvdkjni.so", "libkvdkjni", ".so"} + }; + + public static NativeLibraryLoader getInstance() { + return instance; + } + + public synchronized void loadLibrary(final String tmpDir) throws IOException { + try { + // try system dynamic library + System.loadLibrary(jniLibraryName); + return; + } catch (final UnsatisfiedLinkError ule) { + // ignore - then try from jar + } + + // try jar + loadLibraryFromJar(tmpDir); + } + + void loadLibraryFromJar(final String tmpDir) throws IOException { + if (!initialized) { + for (int i = 0; i < requiredLibraries.length; i++) { + String[] libraryInfo = requiredLibraries[i]; + System.load( + loadLibraryFromJarToTemp( + tmpDir, libraryInfo[0], libraryInfo[1], libraryInfo[2]) + .getAbsolutePath()); + } + + initialized = true; + } + } + + File loadLibraryFromJarToTemp( + final String tmpDir, + String libraryFileName, + String tempFilePrefix, + String tempFileSuffix) + throws IOException { + InputStream is = null; + try { + // attempt to look up the static library in the jar file + is = getClass().getClassLoader().getResourceAsStream(libraryFileName); + + if (is == null) { + throw new RuntimeException(libraryFileName + " was not found inside JAR."); + } + + // create a temporary file to copy the library to + final File temp; + if (tmpDir == null || tmpDir.isEmpty()) { + temp = File.createTempFile(tempFilePrefix, tempFileSuffix); + } else { + final File parentDir = new File(tmpDir); + if (!parentDir.exists()) { + throw new RuntimeException( + "Directory: " + parentDir.getAbsolutePath() + " does not exist!"); + } + temp = new File(parentDir, libraryFileName); + if (temp.exists() && !temp.delete()) { + throw new RuntimeException( + "File: " + + temp.getAbsolutePath() + + " already exists and cannot be removed."); + } + if (!temp.createNewFile()) { + throw new RuntimeException( + "File: " + temp.getAbsolutePath() + " could not be created."); + } + } + if (!temp.exists()) { + throw new RuntimeException("File " + temp.getAbsolutePath() + " does not exist."); + } else { + temp.deleteOnExit(); + } + + // copy the library from the Jar file to the temp destination + Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); + + // return the temporary library file + return temp; + + } finally { + if (is != null) { + is.close(); + } + } + } + + /** Private constructor to disallow instantiation */ + private NativeLibraryLoader() {} +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/Status.java b/volatile/java/src/main/java/io/pmem/kvdk/Status.java new file mode 100644 index 00000000..2836c1ee --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/Status.java @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import java.util.Objects; + +/** Java class to represent the Status enum in C++ KVDK. */ +public class Status { + private final Code code; + private final String message; + + public Status(final Code code, final String message) { + this.code = code; + this.message = message; + } + + protected Status(final byte code) { + this(code, null); + } + + protected Status(final byte code, final String message) { + this.code = Code.getCode(code); + this.message = message; + } + + public Code getCode() { + return code; + } + + public String getMessage() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Status status = (Status) o; + return code == status.code && Objects.equals(message, status.message); + } + + @Override + public int hashCode() { + return Objects.hash(code, message); + } + + // should stay in sync with include/kvdk/types.h:KVDKStatus and + // java/kvdkjni/kvdkjni.h:toJavaStatusCode + public enum Code { + Ok((byte) 0x0), + NotFound((byte) 0x1), + Outdated((byte) 0x2), + WrongType((byte) 0x3), + Existed((byte) 0x4), + OperationFail((byte) 0x5), + OutOfRange((byte) 0x6), + MemoryOverflow((byte) 0x7), + NotSupported((byte) 0x8), + InvalidBatchSize((byte) 0x9), + InvalidDataSize((byte) 0xA), + InvalidArgument((byte) 0xB), + IOError((byte) 0xC), + InvalidConfiguration((byte) 0xD), + Fail((byte) 0xE), + Abort((byte) 0xF), + Undefined((byte) 0x7F); + + private final byte value; + + Code(final byte value) { + this.value = value; + } + + public static Code getCode(final byte value) { + for (final Code code : Code.values()) { + if (code.value == value) { + return code; + } + } + throw new IllegalArgumentException("Illegal value provided for Code (" + value + ")."); + } + + /** + * Returns the byte value of the enumerations value. + * + * @return byte representation + */ + public byte getValue() { + return value; + } + } +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/WriteBatch.java b/volatile/java/src/main/java/io/pmem/kvdk/WriteBatch.java new file mode 100644 index 00000000..58d7a903 --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/WriteBatch.java @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +/** A WriteBatch can be used to prepare multiple operations before a commit. */ +public class WriteBatch extends KVDKObject { + static { + Engine.loadLibrary(); + } + + protected WriteBatch(final long handle) { + super(handle); + } + + public void stringPut(final byte[] key, final byte[] value) { + stringPut(nativeHandle_, key, 0, key.length, value, 0, value.length); + } + + public void stringPut( + final byte[] key, + int keyOffset, + int keyLength, + final byte[] value, + int valueOffset, + int valueLength) { + stringPut(nativeHandle_, key, keyOffset, keyLength, value, valueOffset, valueLength); + } + + public void stringDelete(final byte[] key) { + stringDelete(nativeHandle_, key, 0, key.length); + } + + public void stringDelete(final byte[] key, int keyOffset, int keyLength) { + stringDelete(nativeHandle_, key, keyOffset, keyLength); + } + + public void sortedPut( + final NativeBytesHandle nameHandle, final byte[] key, final byte[] value) { + sortedPut( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + 0, + key.length, + value, + 0, + value.length); + } + + public void sortedPut( + final NativeBytesHandle nameHandle, + final byte[] key, + int keyOffset, + int keyLength, + final byte[] value, + int valueOffset, + int valueLength) { + sortedPut( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + keyOffset, + keyLength, + value, + valueOffset, + valueLength); + } + + public void sortedDelete(final NativeBytesHandle nameHandle, final byte[] key) { + sortedDelete( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + 0, + key.length); + } + + public void sortedDelete( + final NativeBytesHandle nameHandle, final byte[] key, int keyOffset, int keyLength) { + sortedDelete( + nativeHandle_, + nameHandle.getNativeHandle(), + nameHandle.getLength(), + key, + keyOffset, + keyLength); + } + + public void clear() { + clear(nativeHandle_); + } + + public long size() { + return size(nativeHandle_); + } + + // Native methods + @Override + protected final native void closeInternal(long handle); + + private native void stringPut( + long handle, + byte[] key, + int keyOffset, + int keyLength, + byte[] value, + int valueOffset, + int valueLength); + + private native void stringDelete(long handle, byte[] key, int keyOffset, int keyLength); + + private native void sortedPut( + long engineHandle, + long nameHandle, + int nameLenth, + byte[] key, + int keyOffset, + int keyLength, + byte[] value, + int valueOffset, + int valueLength); + + private native void sortedDelete( + long engineHandle, + long nameHandle, + int nameLenth, + byte[] key, + int keyOffset, + int keyLength); + + private native void clear(long handle); + + private native long size(long handle); +} diff --git a/volatile/java/src/main/java/io/pmem/kvdk/WriteOptions.java b/volatile/java/src/main/java/io/pmem/kvdk/WriteOptions.java new file mode 100644 index 00000000..b93f20bd --- /dev/null +++ b/volatile/java/src/main/java/io/pmem/kvdk/WriteOptions.java @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +/** Options of expiration */ +public class WriteOptions { + private long ttlInMillis; + private boolean updateTtlIfExisted; + + public WriteOptions() { + this(Long.MAX_VALUE, true); + } + + public WriteOptions(long ttlInMillis, boolean updateTtlIfExisted) { + this.ttlInMillis = ttlInMillis; + this.updateTtlIfExisted = updateTtlIfExisted; + } + + public long getTtlInMillis() { + return ttlInMillis; + } + + public boolean isUpdateTtlIfExisted() { + return updateTtlIfExisted; + } +} diff --git a/volatile/java/src/test/java/io/pmem/kvdk/ConfigsTest.java b/volatile/java/src/test/java/io/pmem/kvdk/ConfigsTest.java new file mode 100644 index 00000000..9fbf157a --- /dev/null +++ b/volatile/java/src/test/java/io/pmem/kvdk/ConfigsTest.java @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import org.junit.Test; + +public class ConfigsTest { + @Test + public void testCreateConfigs() { + Configs configs = new Configs(); + + configs.setMaxAccessThreads(8); + + configs.close(); + } +} diff --git a/volatile/java/src/test/java/io/pmem/kvdk/EngineTest.java b/volatile/java/src/test/java/io/pmem/kvdk/EngineTest.java new file mode 100644 index 00000000..ea6cab42 --- /dev/null +++ b/volatile/java/src/test/java/io/pmem/kvdk/EngineTest.java @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import static org.junit.Assert.assertEquals; + +import io.pmem.kvdk.Status.Code; +import org.junit.Test; + +public class EngineTest extends EngineTestBase { + @Test + public void testOpenAndCloseKVDKEngine() {} + + @Test + public void testAnonymousCollection() throws KVDKException { + String key = "key1"; + String value1 = "value1"; + + // put + kvdkEngine.put(key.getBytes(), value1.getBytes()); + assertEquals(value1, new String(kvdkEngine.get(key.getBytes()))); + + // rewrite + String value2 = "value2"; + kvdkEngine.put(key.getBytes(), value2.getBytes()); + assertEquals(value2, new String(kvdkEngine.get(key.getBytes()))); + + // delete + kvdkEngine.delete(key.getBytes()); + assertEquals(null, kvdkEngine.get(key.getBytes())); + + // delete nonexistent key: OK + kvdkEngine.delete(key.getBytes()); + + // put special characters + String value3 = "value\0with_zero"; + kvdkEngine.put(key.getBytes(), value3.getBytes()); + assertEquals(value3, new String(kvdkEngine.get(key.getBytes()))); + } + + @Test + public void testSortedCollection() throws KVDKException { + String name = "collection\0\nname"; + NativeBytesHandle nameHandle = new NativeBytesHandle(name.getBytes()); + + // create + kvdkEngine.sortedCreate(nameHandle); + + // duplicate creation: Not OK + try { + kvdkEngine.sortedCreate(nameHandle); + } catch (KVDKException ex) { + // should be Existed + assertEquals(ex.getStatus().getCode(), Code.Existed); + } + + // destroy + kvdkEngine.sortedDestroy(nameHandle); + + // create again: OK + kvdkEngine.sortedCreate(nameHandle); + + String key = "key\01"; + String value = "value\01"; + + // put + kvdkEngine.sortedPut(nameHandle, key.getBytes(), value.getBytes()); + + // size + assertEquals(1, kvdkEngine.sortedSize(nameHandle)); + + // get + assertEquals(value, new String(kvdkEngine.sortedGet(nameHandle, key.getBytes()))); + + // delete + kvdkEngine.sortedDelete(nameHandle, key.getBytes()); + assertEquals(null, kvdkEngine.sortedGet(nameHandle, key.getBytes())); + + // size + assertEquals(0, kvdkEngine.sortedSize(nameHandle)); + + // delete nonexistent key: OK + kvdkEngine.sortedDelete(nameHandle, key.getBytes()); + + // destroy + kvdkEngine.sortedDestroy(nameHandle); + + // destroy destroyed sorted collection: OK + kvdkEngine.sortedDestroy(nameHandle); + + // delete on destroyed sorted collection: OK + kvdkEngine.sortedDelete(nameHandle, key.getBytes()); + + // put on destroyed sorted collection: Not OK + try { + kvdkEngine.sortedPut(nameHandle, key.getBytes(), value.getBytes()); + } catch (KVDKException ex) { + // should be NotFound + assertEquals(ex.getStatus().getCode(), Code.NotFound); + } + + // size on destroyed sorted collection: Not OK + try { + kvdkEngine.sortedSize(nameHandle); + } catch (KVDKException ex) { + // should be NotFound + assertEquals(ex.getStatus().getCode(), Code.NotFound); + } + + // close name handle + nameHandle.close(); + } + + @Test + public void testExpiration() throws KVDKException, InterruptedException { + String key1 = "key1"; + String value1 = "value1"; + + // put + kvdkEngine.put(key1.getBytes(), value1.getBytes()); + assertEquals(value1, new String(kvdkEngine.get(key1.getBytes()))); + + // expire + kvdkEngine.expire(key1.getBytes(), 199); + Thread.sleep(200); + + // check + assertEquals(null, kvdkEngine.get(key1.getBytes())); + } + + @Test + public void testExpirationWithWriteOptions() throws KVDKException, InterruptedException { + String key1 = "key1"; + String value1 = "value1"; + + WriteOptions writeOptions = new WriteOptions(199, true); + + // put + kvdkEngine.put(key1.getBytes(), value1.getBytes(), writeOptions); + assertEquals(value1, new String(kvdkEngine.get(key1.getBytes()))); + + Thread.sleep(200); + + // check + assertEquals(null, kvdkEngine.get(key1.getBytes())); + } +} diff --git a/volatile/java/src/test/java/io/pmem/kvdk/EngineTestBase.java b/volatile/java/src/test/java/io/pmem/kvdk/EngineTestBase.java new file mode 100644 index 00000000..3790d85c --- /dev/null +++ b/volatile/java/src/test/java/io/pmem/kvdk/EngineTestBase.java @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import java.io.File; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +public class EngineTestBase { + protected Engine kvdkEngine; + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Before + public void init() throws KVDKException, IOException { + File tempDir = folder.newFolder(); + String enginePath = tempDir.getAbsolutePath(); + + Configs engineConfigs = new Configs(); + engineConfigs.setHashBucketNum(1L << 10); + engineConfigs.setMaxAccessThreads(4); + + kvdkEngine = Engine.open(enginePath, engineConfigs); + engineConfigs.close(); + } + + @After + public void teardown() { + kvdkEngine.close(); + } +} diff --git a/volatile/java/src/test/java/io/pmem/kvdk/IteratorTest.java b/volatile/java/src/test/java/io/pmem/kvdk/IteratorTest.java new file mode 100644 index 00000000..a5e2a924 --- /dev/null +++ b/volatile/java/src/test/java/io/pmem/kvdk/IteratorTest.java @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class IteratorTest extends EngineTestBase { + + @Test + public void testSortedCollectionIterator() throws KVDKException { + String name = "collection\0\nname"; + NativeBytesHandle nameHandle = new NativeBytesHandle(name.getBytes()); + + // create + kvdkEngine.sortedCreate(nameHandle); + + String key1 = "key\01"; + String key2 = "key\02"; + String key3 = "key\03"; + + String value1 = "value\03"; + String value2 = "value\02"; + String value3 = "value\01"; + + // disordered put + kvdkEngine.sortedPut(nameHandle, key3.getBytes(), value3.getBytes()); + kvdkEngine.sortedPut(nameHandle, key1.getBytes(), value1.getBytes()); + kvdkEngine.sortedPut(nameHandle, key2.getBytes(), value2.getBytes()); + + // create iterator + Iterator iter = kvdkEngine.sortedIteratorCreate(nameHandle); + assertFalse(iter.isValid()); + + // seekToFirst + iter.seekToFirst(); + assertTrue(iter.isValid()); + assertEquals(key1, new String(iter.key())); + assertEquals(value1, new String(iter.value())); + + // next + iter.next(); + assertTrue(iter.isValid()); + assertEquals(key2, new String(iter.key())); + assertEquals(value2, new String(iter.value())); + + iter.next(); + assertTrue(iter.isValid()); + assertEquals(key3, new String(iter.key())); + assertEquals(value3, new String(iter.value())); + + iter.next(); + assertFalse(iter.isValid()); + + // seekToLast + iter.seekToLast(); + assertTrue(iter.isValid()); + assertEquals(key3, new String(iter.key())); + assertEquals(value3, new String(iter.value())); + + // prev + iter.prev(); + assertTrue(iter.isValid()); + assertEquals(key2, new String(iter.key())); + assertEquals(value2, new String(iter.value())); + + iter.prev(); + assertTrue(iter.isValid()); + assertEquals(key1, new String(iter.key())); + assertEquals(value1, new String(iter.value())); + + iter.prev(); + assertFalse(iter.isValid()); + + // close iterator + iter.close(); + + // destroy sorted collection + kvdkEngine.sortedDestroy(nameHandle); + + // close name handle + nameHandle.close(); + } +} diff --git a/volatile/java/src/test/java/io/pmem/kvdk/WriteBatchTest.java b/volatile/java/src/test/java/io/pmem/kvdk/WriteBatchTest.java new file mode 100644 index 00000000..26c41e4b --- /dev/null +++ b/volatile/java/src/test/java/io/pmem/kvdk/WriteBatchTest.java @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +package io.pmem.kvdk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class WriteBatchTest extends EngineTestBase { + @Test + public void testWriteBatchString() throws KVDKException { + String key1 = "key1"; + String key2 = "key2"; + String value1 = "value1"; + String value2 = "value2"; + + WriteBatch batch = kvdkEngine.writeBatchCreate(); + batch.stringPut(key1.getBytes(), value1.getBytes()); + batch.stringPut(key1.getBytes(), value2.getBytes()); + batch.stringPut(key2.getBytes(), value2.getBytes()); + batch.stringDelete(key2.getBytes()); + + // If the batch is successfully written, there should be only key1-value2 in + // anonymous global collection. + kvdkEngine.batchWrite(batch); + assertEquals(value2, new String(kvdkEngine.get(key1.getBytes()))); + assertNull(kvdkEngine.get(key2.getBytes())); + } + + @Test + public void testWriteBatchSorted() throws KVDKException { + String name = "collection_name"; + String key1 = "key1"; + String key2 = "key2"; + String value1 = "value1"; + String value2 = "value2"; + + NativeBytesHandle nameHandle = new NativeBytesHandle(name.getBytes()); + + kvdkEngine.sortedCreate(nameHandle); + + WriteBatch batch = kvdkEngine.writeBatchCreate(); + batch.sortedPut(nameHandle, key1.getBytes(), value1.getBytes()); + batch.sortedPut(nameHandle, key1.getBytes(), value2.getBytes()); + batch.sortedPut(nameHandle, key2.getBytes(), value2.getBytes()); + batch.sortedDelete(nameHandle, key2.getBytes()); + + // If the batch is successfully written, there should be only key1-value2 in + // the sorted collection. + kvdkEngine.batchWrite(batch); + assertEquals(1, kvdkEngine.sortedSize(nameHandle)); + assertEquals(value2, new String(kvdkEngine.sortedGet(nameHandle, key1.getBytes()))); + assertNull(kvdkEngine.sortedGet(nameHandle, key2.getBytes())); + + batch.close(); + nameHandle.close(); + } +} diff --git a/volatile/kvdk.pc.in b/volatile/kvdk.pc.in new file mode 100644 index 00000000..53c6c512 --- /dev/null +++ b/volatile/kvdk.pc.in @@ -0,0 +1,12 @@ +version=@VERSION@ +prefix=@CMAKE_INSTALL_PREFIX@ +libdir=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_LIBDIR@ +includedir=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: kvdk +Description: kvdk - key-value store for Persistent Memory +Version: ${version} +URL: https://github.com/pmem/kvdk +Requires.private: @PKG_CONFIG_REQUIRES@ +Libs: -L${libdir} -lengine +Cflags: -I${includedir} diff --git a/volatile/scripts/benchmark_impl.py b/volatile/scripts/benchmark_impl.py new file mode 100644 index 00000000..30a009ee --- /dev/null +++ b/volatile/scripts/benchmark_impl.py @@ -0,0 +1,184 @@ +import os +import datetime +import sys +import datetime +import git +from select import select + +from git import repo + + +def __fill(exec, shared_para, data_type, report_path): + new_para = shared_para + " -fill=1 -type={}".format(data_type) + report = report_path + "_fill" + print("Fill {}".format(data_type)) + cmd = "{0} {1} > {2}".format(exec, new_para, report) + print(cmd) + os.system(cmd) + + +def read_random(exec, shared_para, data_type, report_path, num_operations): + new_para = shared_para + \ + " -fill=0 -type={} -read_ratio=1 -num_operations={}".format(data_type, num_operations) + report = report_path + ("read_random" if (data_type != "list") else "pop") + print("Read random {}".format(data_type)) + cmd = "{0} {1} > {2}".format(exec, new_para, report) + print(cmd) + os.system(cmd) + + +def insert_random(exec, shared_para, data_type, report_path, num_operations): + new_para = shared_para + \ + " -fill=0 -type={} -read_ratio=0 -existing_keys_ratio=0 -num_operations={}".format(data_type, num_operations) + report = report_path + ("insert_random" if (data_type != "list") else "push") + print("Insert random {}".format(data_type)) + cmd = "{0} {1} > {2}".format(exec, new_para, report) + print(cmd) + os.system(cmd) + + +def batch_insert_random(exec, shared_para, data_type, report_path, num_operations): + if data_type != "string" and data_type != "sorted" and data_type != "blackhole" and data_type != "hash": + return + new_para = shared_para + \ + " -fill=0 -type={} -read_ratio=0 -batch_size=100"\ + " -existing_keys_ratio=0 -num_operations={}".format(data_type, num_operations) + report = report_path + "batch_insert_random" + print("Batch insert random {}".format(data_type)) + cmd = "{0} {1} > {2}".format(exec, new_para, report) + print(cmd) + os.system(cmd) + + +def update_random(exec, shared_para, data_type, report_path, num_operations): + if data_type == "list": + return + new_para = shared_para + \ + " -fill=0 -type={} -read_ratio=0 -num_operations={}".format(data_type, num_operations) + report = report_path + "update_random" + print("Update random {}".format(data_type)) + cmd = "{0} {1} > {2}".format(exec, new_para, report) + print(cmd) + os.system(cmd) + + +def read_write_random(exec, shared_para, data_type, report_path, num_operations): + ratio = 0.9 + if data_type == "list": + ratio = 0.5 + new_para = shared_para + \ + " -fill=0 -type={} -read_ratio={} -num_operations={}".format(data_type, ratio, num_operations) + report = report_path + ("read_write_random" if (data_type != "list") else "pushpop") + print("Read write random {}".format(data_type)) + cmd = "{0} {1} > {2}".format(exec, new_para, report) + print(cmd) + os.system(cmd) + + +def range_scan(exec, shared_para, data_type, report_path, num_operations): + if data_type == "string" or data_type == "list": + return + new_para = shared_para + \ + " -fill=0 -type={} -read_ratio=1 -scan=1 -num_operations={}".format(data_type, num_operations) + report = report_path + "range_scan" + print("Range scan {}".format(data_type)) + cmd = "{0} {1} > {2}".format(exec, new_para, report) + print(cmd) + os.system(cmd) + + +def confirm(dir): + timeout = 60 + print("Instance path : {}, it will be removed and recreated, confirm? (y/n) (Automatically confirm in {} seconds)".format(dir, timeout)) + rlist, _, _ = select([sys.stdin], [], [], timeout) + y = 'n' + if rlist: + y = sys.stdin.readline() + else: + print("Automatically confirmed after {} seconds!".format(timeout)) + y = 'y' + if y != 'y' and y != 'y\n': + exit(1) + + +def run_benchmark( + data_type, + exec, + pmem_path, + pmem_size, + populate_on_fill, + n_thread, + num_collection, + timeout, + key_distribution, + value_size, + value_size_distribution, + benchmarks, +): + confirm(pmem_path) + os.system("rm -rf {0}".format(pmem_path)) + + # calculate num kv to fill + header_sz = 24 if (key_distribution == 'string') else 40 + k_sz = 8 + avg_v_sz = value_size if (value_size_distribution == 'constant') else (value_size // 2) + avg_kv_size = (header_sz + k_sz + avg_v_sz + 63) // 64 * 64 + + # 1/4 for fill, 1/4 for insert, 1/4 for batch_write + num_kv = pmem_size // 4 // avg_kv_size + + # create report dir + timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M") + git_hash = git.Repo(search_parent_directories=True).head.object.hexsha + report_path = "./results/commit-{0}/data_type-{1}-key_dist-{2}/vsize-{3}-vsize_dist-{4}-threads-{5}-collections-{6}-time-{7}/".format( + git_hash[0:8], + data_type, + key_distribution, + value_size, + value_size_distribution, + n_thread, + num_collection, + timestamp) + os.system("mkdir -p {}".format(report_path)) + + # run benchmarks + print("Run benchmarks for data type :{}, value size distribution: {}".format( + data_type, value_size_distribution)) + shared_para = \ + "-path={0} "\ + "-space={1} "\ + "-populate={2} "\ + "-num_kv={3} "\ + "-threads={4} "\ + "-max_access_threads={5} "\ + "-num_collection={6} "\ + "-timeout={7} "\ + "-value_size={8} "\ + "-value_size_distribution={9} "\ + "-key_distribution={10} ".format( + pmem_path, + pmem_size, + populate_on_fill, + num_kv, + n_thread, + n_thread, + num_collection, + timeout, + value_size, + value_size_distribution, + key_distribution) + # we always fill data before run benchmarks + __fill(exec, shared_para, data_type, report_path) + for benchmark in benchmarks: + if (benchmark == read_random) \ + or (benchmark == update_random) \ + or (benchmark == range_scan) \ + or (data_type == "blackhole") : + num_operations = 1024 * 1024 * 1024 * 10 + else: + # __fill, insert_random, batch_insert_random, read_write_random + num_operations = num_kv + + benchmark(exec, shared_para, data_type, report_path, num_operations) + + os.system("rm -rf {0}".format(pmem_path)) diff --git a/volatile/scripts/clang_format.sh b/volatile/scripts/clang_format.sh new file mode 100755 index 00000000..d4664613 --- /dev/null +++ b/volatile/scripts/clang_format.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +src_dirs=( benchmark engine examples include tests java ) +file_types=( h hpp c cpp cc ) + +for dir in "${src_dirs[@]}" +do + for type in "${file_types[@]}" + do + find "../$dir/" -iname "*.$type" | xargs -I '{}' clang-format-9 -i '{}' + done +done diff --git a/volatile/scripts/clang_tidy.sh b/volatile/scripts/clang_tidy.sh new file mode 100755 index 00000000..8646a08b --- /dev/null +++ b/volatile/scripts/clang_tidy.sh @@ -0,0 +1,19 @@ +#!/bin/ + +includes='-I../include -I../extern' +checks='abseil-*,bugprone-*,cert-*,clang-analyzer-*,cppcoreguidelines-*,-cppcoreguidelines-pro-type-reinterpret-cast,misc-*' + +# Just too much +# find ../ -iname *.cpp -o -iname *.hpp | xargs -I '{}' clang-tidy-13 '{}' -checks=$checks -- $includes + +i=1; +j=$#; +if [ $j -eq 0 ]; then + echo "usage: ./clang_tidy.sh [... ]" +fi +while [ $i -le $j ] +do + clang-tidy-13 $1 -checks=$checks -- $includes + i=$((i + 1)); + shift 1; +done diff --git a/volatile/scripts/cppstyle b/volatile/scripts/cppstyle new file mode 100755 index 00000000..e21a345f --- /dev/null +++ b/volatile/scripts/cppstyle @@ -0,0 +1,44 @@ +#!/usr/bin/perl -w +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2017, Intel Corporation + +use strict; +use Text::Diff; + +my $clangfmt = shift or die; +my $mode = shift or die; + +sub check { + my ($file) = @_; + my $original; + my $formatted; + + $formatted = `$clangfmt -style=file "$file"`; + + if ($mode eq 'check') { + local $/=undef; + open FILE, "$file" or die "Couldn't open file: $file"; + binmode FILE; + $original = ; + close FILE; + + my $diff = diff \$original, \$formatted; + + if ($diff ne "") { + print "Style check using $clangfmt for file $file failed\n"; + print $diff; + die "Style check using $clangfmt for file $file failed\n"; + } + } elsif ($mode eq 'format') { + local $/=undef; + open FILE, '>', "$file" or die "Couldn't open file: $file"; + print FILE "$formatted"; + close FILE; + } else { + die 'unknown mode'; + } +} + +foreach(@ARGV) { + check($_) +} diff --git a/volatile/scripts/run_benchmark.py b/volatile/scripts/run_benchmark.py new file mode 100644 index 00000000..ce8990f2 --- /dev/null +++ b/volatile/scripts/run_benchmark.py @@ -0,0 +1,64 @@ +import benchmark_impl +import sys +import itertools + +numanode = 0 +pmem_path = "/mnt/pmem{}/kvdk_benchmark".format(numanode) +bin = "../build/bench" +exec = "numactl --cpunodebind={0} --membind={0} {1}".format(numanode, bin) + +num_thread = 64 +value_sizes = [120] +# constant: value size always be "value_size", +# random: value size uniformly distributed in [1, value_size] +value_size_distributions = ['constant'] +timeout = 30 # For operations other than fill +populate_on_fill = 1 # For fill only +pmem_size = 384 * 1024 * 1024 * 1024 # we need enough space to test insert +num_collection = 16 + +benchmarks = [ + benchmark_impl.batch_insert_random, + benchmark_impl.insert_random, + benchmark_impl.range_scan, + benchmark_impl.read_random, + benchmark_impl.read_write_random, + benchmark_impl.update_random] + +data_types = [] + +if __name__ == "__main__": + usage = 'usage: run_benchmark.py [data type] [key distribution]\n\ + data type can be "string", "sorted", "hash", "list", or "all"\n\ + key distribution can be "random" or "zipf" or "all".' + if len(sys.argv) != 3: + print(usage) + exit(1) + if sys.argv[1] == 'string': + data_types = ['string'] + elif sys.argv[1] == 'sorted': + data_types = ['sorted'] + elif sys.argv[1] == 'hash': + data_types = ['hash'] + elif sys.argv[1] == 'list': + data_types = ['list'] + elif sys.argv[1] == 'blackhole': + data_types = ['blackhole'] + elif sys.argv[1] == 'all': + data_types = ['blackhole', 'string', 'sorted', 'hash', 'list'] + else: + print(usage) + exit(1) + if sys.argv[2] == 'random': + key_distributions = ['random'] + elif sys.argv[2] == 'zipf': + key_distributions = ['zipf'] + elif sys.argv[2] == 'all': + key_distributions = ['random', 'zipf'] + else: + print(usage) + exit(1) + + for [data_type, value_size, vsz_dist, k_dist] in itertools.product(data_types, value_sizes, value_size_distributions, key_distributions): + benchmark_impl.run_benchmark(data_type, exec, pmem_path, pmem_size, populate_on_fill, + num_thread, num_collection, timeout, k_dist, value_size, vsz_dist, benchmarks) diff --git a/volatile/scripts/test_coverage.sh b/volatile/scripts/test_coverage.sh new file mode 100755 index 00000000..85c96699 --- /dev/null +++ b/volatile/scripts/test_coverage.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# The shell is for view test coverage. + +COVERAGE_FILE=test_coverage.info +REPORT_FOLDER=test_coverage_report +./../build/dbtest +lcov --rc lcov_branch_coverage=1 -c -d $(pwd)/../build/ -o ${COVERAGE_FILE}_tmp +lcov --rc lcov_branch_coverage=1 -e ${COVERAGE_FILE}_tmp "*engine*" -o ${COVERAGE_FILE} +genhtml --rc genhtml_branch_coverage=1 ${COVERAGE_FILE} -o ${REPORT_FOLDER} diff --git a/volatile/tests/CMakeLists.txt b/volatile/tests/CMakeLists.txt new file mode 100644 index 00000000..eab028a2 --- /dev/null +++ b/volatile/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +# For kvdk api general test +set(TEST_SOURCES tests.cpp) +add_executable(dbtest ${TEST_SOURCES}) +target_link_libraries(dbtest PUBLIC engine gtest gtest_main) + +# C API test +add_executable(c_api_test + c_api_test_list.cpp + c_api_test_hash.cpp + ) +target_link_libraries(c_api_test PUBLIC engine gtest gtest_main) + +# For stress tests +set(TEST_SOURCES2 stress_test.cpp) +add_executable(dbstress_test ${TEST_SOURCES2}) +target_link_libraries(dbstress_test PUBLIC engine gtest gtest_main) diff --git a/volatile/tests/c_api_test.hpp b/volatile/tests/c_api_test.hpp new file mode 100644 index 00000000..94850eed --- /dev/null +++ b/volatile/tests/c_api_test.hpp @@ -0,0 +1,51 @@ +#pragma once +#ifndef KVDK_C_API_TEST_HPP +#define KVDK_C_API_TEST_HPP + +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ +#include + +#include + +#include "gtest/gtest.h" +#include "kvdk/volatile/engine.h" +#include "test_util.h" + +class EngineCAPITestBase : public testing::Test { + protected: + KVDKEngine* engine{nullptr}; + KVDKConfigs* configs; + const std::string db_path{"/mnt/pmem0/kvdk_c_api_test"}; + + virtual void SetUp() override { + purgeDB(); + configs = KVDKCreateConfigs(); + KVDKSetConfigs(configs, 32, 1024, 1); + ASSERT_EQ(KVDKOpen(db_path.c_str(), configs, stdout, &engine), + KVDKStatus::Ok) + << "Fail to open the KVDK instance"; + } + + virtual void TearDown() { + KVDKCloseEngine(engine); + KVDKDestroyConfigs(configs); + purgeDB(); + } + + void RebootDB() { + KVDKCloseEngine(engine); + ASSERT_EQ(KVDKOpen(db_path.c_str(), configs, stdout, &engine), + KVDKStatus::Ok) + << "Fail to open the KVDK instance"; + } + + private: + void purgeDB() { + std::string cmd = "rm -rf " + db_path + "\n"; + [[gnu::unused]] int _sink = system(cmd.c_str()); + } +}; + +#endif // KVDK_C_API_TEST_HPP diff --git a/volatile/tests/c_api_test_hash.cpp b/volatile/tests/c_api_test_hash.cpp new file mode 100644 index 00000000..c042a081 --- /dev/null +++ b/volatile/tests/c_api_test_hash.cpp @@ -0,0 +1,196 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include +#include +#include +#include +#include + +#include "c_api_test.hpp" + +struct FetchAddArgs { + size_t old_val; + size_t new_val; + size_t n; +}; + +int FetchAdd(char const* old_val_data, size_t old_val_len, char** new_val_data, + size_t* new_val_len, void* args) { + FetchAddArgs* fa_args = (FetchAddArgs*)args; + if (old_val_data != nullptr) { + assert(sizeof(size_t) == old_val_len); + fa_args->old_val = *(size_t*)old_val_data; + } else { + fa_args->old_val = 0; + } + fa_args->new_val = fa_args->old_val + fa_args->n; + *new_val_len = sizeof(size_t); + *new_val_data = (char*)&fa_args->new_val; + return KVDK_MODIFY_WRITE; +} + +TEST_F(EngineCAPITestBase, Hash) { + size_t num_threads = 16; + size_t count = 1000; + + std::string key{"Hash"}; + ASSERT_EQ(KVDKHashCreate(engine, key.data(), key.size()), KVDKStatus::Ok); + using umap = std::unordered_map; + std::vector local_copies(num_threads); + std::mutex mu; + + auto HPut = [&](size_t tid) { + umap& local_copy = local_copies[tid]; + for (size_t j = 0; j < count; j++) { + std::string field{std::to_string(tid) + "_" + GetRandomString(10)}; + std::string value{GetRandomString(120)}; + ASSERT_EQ(KVDKHashPut(engine, key.data(), key.size(), field.data(), + field.size(), value.data(), value.size()), + KVDKStatus::Ok); + local_copy[field] = value; + } + }; + + auto HGet = [&](size_t tid) { + umap const& local_copy = local_copies[tid]; + for (auto const& kv : local_copy) { + char* resp_data; + size_t resp_len; + ASSERT_EQ(KVDKHashGet(engine, key.data(), key.size(), kv.first.data(), + kv.first.size(), &resp_data, &resp_len), + KVDKStatus::Ok); + ASSERT_EQ(std::string(resp_data, resp_len), kv.second); + free(resp_data); + } + }; + + auto HDelete = [&](size_t tid) { + umap& local_copy = local_copies[tid]; + std::string sink; + for (size_t i = 0; i < count / 2; i++) { + auto iter = local_copy.begin(); + char* resp_data; + size_t resp_len; + ASSERT_EQ(KVDKHashDelete(engine, key.data(), key.size(), + iter->first.data(), iter->first.size()), + KVDKStatus::Ok); + ASSERT_EQ(KVDKHashGet(engine, key.data(), key.size(), iter->first.data(), + iter->first.size(), &resp_data, &resp_len), + KVDKStatus::NotFound); + local_copy.erase(iter); + } + }; + + auto HashSize = [&](size_t) { + size_t len = 0; + ASSERT_EQ(KVDKHashLength(engine, key.data(), key.size(), &len), + KVDKStatus::Ok); + size_t cnt = 0; + for (size_t tid = 0; tid < num_threads; tid++) { + cnt += local_copies[tid].size(); + } + ASSERT_EQ(len, cnt); + }; + + auto HashIterate = [&](size_t tid) { + umap combined; + for (size_t tid = 0; tid < num_threads; tid++) { + umap const& local_copy = local_copies[tid]; + for (auto const& kv : local_copy) { + combined[kv.first] = kv.second; + } + } + + KVDKHashIterator* iter = + KVDKHashIteratorCreate(engine, key.data(), key.size(), NULL, NULL); + + ASSERT_NE(iter, nullptr); + size_t cnt = 0; + for (KVDKHashIteratorSeekToFirst(iter); KVDKHashIteratorIsValid(iter); + KVDKHashIteratorNext(iter)) { + ++cnt; + char* field_data; + size_t field_len; + char* value_data; + size_t value_len; + KVDKHashIteratorGetKey(iter, &field_data, &field_len); + KVDKHashIteratorGetValue(iter, &value_data, &value_len); + ASSERT_EQ(combined[std::string(field_data, field_len)], + std::string(value_data, value_len)); + free(field_data); + free(value_data); + } + ASSERT_EQ(cnt, combined.size()); + + cnt = 0; + for (KVDKHashIteratorSeekToLast(iter); KVDKHashIteratorIsValid(iter); + KVDKHashIteratorPrev(iter)) { + ++cnt; + char* field_data; + size_t field_len; + char* value_data; + size_t value_len; + KVDKHashIteratorGetKey(iter, &field_data, &field_len); + KVDKHashIteratorGetValue(iter, &value_data, &value_len); + ASSERT_EQ(combined[std::string(field_data, field_len)], + std::string(value_data, value_len)); + free(field_data); + free(value_data); + } + ASSERT_EQ(cnt, combined.size()); + + std::string re_str1{".*"}; + std::string re_str2{std::to_string(tid) + "_.*"}; + KVDKRegex* re1 = KVDKRegexCreate(re_str1.data(), re_str1.size()); + KVDKRegex* re2 = KVDKRegexCreate(re_str2.data(), re_str2.size()); + size_t match_cnt1 = 0; + size_t match_cnt2 = 0; + for (KVDKHashIteratorSeekToFirst(iter); KVDKHashIteratorIsValid(iter); + KVDKHashIteratorNext(iter)) { + match_cnt1 += KVDKHashIteratorMatchKey(iter, re1); + match_cnt2 += KVDKHashIteratorMatchKey(iter, re2); + } + ASSERT_EQ(match_cnt1, combined.size()); + ASSERT_EQ(match_cnt2, local_copies[tid].size()); + KVDKRegexDestroy(re2); + KVDKRegexDestroy(re1); + + KVDKHashIteratorDestroy(engine, iter); + }; + + std::string counter{"counter"}; + auto HashModify = [&](size_t) { + FetchAddArgs args; + args.n = 1; + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(KVDKHashModify(engine, key.data(), key.size(), counter.data(), + counter.size(), FetchAdd, &args, NULL), + KVDKStatus::Ok); + } + }; + + for (size_t i = 0; i < 3; i++) { + RebootDB(); + LaunchNThreads(num_threads, HPut); + LaunchNThreads(num_threads, HGet); + LaunchNThreads(num_threads, HDelete); + LaunchNThreads(num_threads, HashIterate); + LaunchNThreads(num_threads, HashSize); + LaunchNThreads(num_threads, HPut); + LaunchNThreads(num_threads, HGet); + LaunchNThreads(num_threads, HDelete); + LaunchNThreads(num_threads, HashIterate); + LaunchNThreads(num_threads, HashSize); + } + LaunchNThreads(num_threads, HashModify); + char* resp_data; + size_t resp_len; + ASSERT_EQ(KVDKHashGet(engine, key.data(), key.size(), counter.data(), + counter.size(), &resp_data, &resp_len), + KVDKStatus::Ok); + ASSERT_EQ(resp_len, sizeof(size_t)); + ASSERT_EQ(*(size_t*)resp_data, num_threads * count); + free(resp_data); +} diff --git a/volatile/tests/c_api_test_list.cpp b/volatile/tests/c_api_test_list.cpp new file mode 100644 index 00000000..9c04f3e7 --- /dev/null +++ b/volatile/tests/c_api_test_list.cpp @@ -0,0 +1,367 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021-2022 Intel Corporation + */ + +#include +#include +#include + +#include "c_api_test.hpp" + +void ConcatStrings(char const* elem_data, size_t elem_len, void* arg) { + std::string* buffer = static_cast(arg); + buffer->append(elem_data, elem_len); + buffer->append(1, '\n'); +} + +TEST_F(EngineCAPITestBase, List) { + size_t num_threads = 16; + size_t count = 1000; + + std::vector> elems_vec(num_threads); + std::vector list_vec(num_threads); + for (size_t i = 0; i < num_threads; i++) { + list_vec[i] = "List_" + std::to_string(i); + ASSERT_EQ(KVDKListCreate(engine, list_vec[i].data(), list_vec[i].size()), + KVDKStatus::Ok); + for (size_t j = 0; j < count; j++) { + elems_vec[i].push_back(std::to_string(i) + "_" + std::to_string(j)); + } + } + + std::vector> list_copy_vec(num_threads); + + auto ListIteratorGetValue = [&](KVDKListIterator* iter) { + char* value; + size_t sz; + // Read the value at the ListIterator + KVDKListIteratorGetValue(iter, &value, &sz); + std::string ret{value, sz}; + free(value); + return ret; + }; + + auto ListPopFront = [&](std::string const& key) { + char* value; + size_t sz; + KVDKStatus s = + KVDKListPopFront(engine, key.data(), key.size(), &value, &sz); + std::string ret{value, sz}; + free(value); + return std::make_pair(s, ret); + }; + + auto ListPopBack = [&](std::string const& key) { + char* value; + size_t sz; + KVDKStatus s = KVDKListPopBack(engine, key.data(), key.size(), &value, &sz); + std::string ret{value, sz}; + free(value); + return std::make_pair(s, ret); + }; + + auto LPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t sz; + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(KVDKListPushFront(engine, key.data(), key.size(), + elems[j].data(), elems[j].size()), + KVDKStatus::Ok); + list_copy.push_front(elems[j]); + ASSERT_EQ(KVDKListSize(engine, key.data(), key.size(), &sz), + KVDKStatus::Ok); + ASSERT_EQ(sz, list_copy.size()); + } + }; + + auto RPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t sz; + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(KVDKListPushBack(engine, key.data(), key.size(), + elems[j].data(), elems[j].size()), + KVDKStatus::Ok); + list_copy.push_back(elems[j]); + ASSERT_EQ(KVDKListSize(engine, key.data(), key.size(), &sz), + KVDKStatus::Ok); + ASSERT_EQ(sz, list_copy.size()); + } + }; + + auto LPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t len; + for (size_t j = 0; j < count; j++) { + if (list_copy.empty()) { + break; + } + ASSERT_EQ(std::make_pair(KVDKStatus::Ok, list_copy.front()), + ListPopFront(key)); + list_copy.pop_front(); + ASSERT_TRUE((KVDKListSize(engine, key.data(), key.size(), &len) == + KVDKStatus::NotFound && + list_copy.empty()) || + len == list_copy.size()); + } + }; + + auto RPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t len; + for (size_t j = 0; j < count; j++) { + if (list_copy.empty()) { + break; + } + ASSERT_EQ(std::make_pair(KVDKStatus::Ok, list_copy.back()), + ListPopBack(key)); + list_copy.pop_back(); + ASSERT_TRUE((KVDKListSize(engine, key.data(), key.size(), &len) == + KVDKStatus::NotFound && + list_copy.empty()) || + len == list_copy.size()); + } + }; + + auto ListIterate = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + + KVDKListIterator* iter = + KVDKListIteratorCreate(engine, key.data(), key.size(), NULL); + if (iter != nullptr) { + KVDKListIteratorSeekPos(iter, 0); + for (auto iter2 = list_copy.begin(); iter2 != list_copy.end(); iter2++) { + ASSERT_TRUE(KVDKListIteratorIsValid(iter)); + ASSERT_EQ(ListIteratorGetValue(iter), *iter2); + KVDKListIteratorNext(iter); + } + + KVDKListIteratorSeekPos(iter, -1); + for (auto iter2 = list_copy.rbegin(); iter2 != list_copy.rend(); + iter2++) { + ASSERT_TRUE(KVDKListIteratorIsValid(iter)); + ASSERT_EQ(ListIteratorGetValue(iter), *iter2); + KVDKListIteratorPrev(iter); + } + } + KVDKListIteratorDestroy(engine, iter); + }; + + auto ConvertParams = [](std::vector const& elems) { + std::vector elems_data; + std::vector elems_len; + for (auto const& elem : elems) { + elems_data.push_back(elem.data()); + elems_len.push_back(elem.size()); + } + return std::make_pair(elems_data, elems_len); + }; + + auto ListInsertPutRemove = [&](size_t tid) { + auto const& list_name = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t len; + std::string elem; + size_t const insert_pos = 5; + + ASSERT_EQ(KVDKListSize(engine, list_name.data(), list_name.size(), &len), + KVDKStatus::Ok); + ASSERT_GT(len, insert_pos); + + KVDKListIterator* iter = KVDKListIteratorCreate(engine, list_name.data(), + list_name.size(), NULL); + ASSERT_NE(iter, nullptr); + + KVDKListIteratorSeekPos(iter, insert_pos); + auto iter2 = std::next(list_copy.begin(), insert_pos); + auto iter_elem = ListIteratorGetValue(iter); + ASSERT_EQ(iter_elem, *iter2); + + elem = *iter2 + "_before"; + ASSERT_EQ(KVDKListInsertBefore(engine, list_name.data(), list_name.size(), + elem.data(), elem.size(), iter_elem.data(), + iter_elem.size()), + KVDKStatus::Ok); + iter2 = list_copy.insert(iter2, elem); + KVDKListIteratorDestroy(engine, iter); + iter = KVDKListIteratorCreate(engine, list_name.data(), list_name.size(), + NULL); + KVDKListIteratorSeekPos(iter, insert_pos); + ASSERT_EQ(ListIteratorGetValue(iter), *iter2); + + auto replace_pos = insert_pos - 2; + KVDKListIteratorPrev(iter); + KVDKListIteratorPrev(iter); + --iter2; + --iter2; + ASSERT_EQ(ListIteratorGetValue(iter), *iter2); + elem = *iter2 + "_new"; + ASSERT_EQ(KVDKListReplace(engine, list_name.data(), list_name.size(), + replace_pos, elem.data(), elem.size()), + KVDKStatus::Ok); + *iter2 = elem; + KVDKListIteratorDestroy(engine, iter); + iter = KVDKListIteratorCreate(engine, list_name.data(), list_name.size(), + NULL); + KVDKListIteratorSeekPos(iter, replace_pos); + ASSERT_EQ(ListIteratorGetValue(iter), *iter2); + + auto erase_pos = replace_pos - 2; + KVDKListIteratorPrev(iter); + KVDKListIteratorPrev(iter); + --iter2; + --iter2; + ASSERT_EQ(ListIteratorGetValue(iter), *iter2); + char* value; + size_t sz; + ASSERT_EQ(KVDKListErase(engine, list_name.data(), list_name.size(), + erase_pos, &value, &sz), + KVDKStatus::Ok); + iter2 = list_copy.erase(iter2); + KVDKListIteratorDestroy(engine, iter); + iter = KVDKListIteratorCreate(engine, list_name.data(), list_name.size(), + NULL); + KVDKListIteratorSeekPos(iter, erase_pos); + ASSERT_EQ(ListIteratorGetValue(iter), *iter2); + KVDKListIteratorDestroy(engine, iter); + }; + + auto LBatchPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + for (size_t j = 0; j < count; j++) { + list_copy.push_front(elems[j]); + } + auto param = ConvertParams(elems); + ASSERT_EQ(KVDKListBatchPushFront(engine, key.data(), key.size(), + param.first.data(), param.second.data(), + elems.size()), + KVDKStatus::Ok); + size_t sz; + ASSERT_EQ(KVDKListSize(engine, key.data(), key.size(), &sz), + KVDKStatus::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto RBatchPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + for (size_t j = 0; j < count; j++) { + list_copy.push_back(elems[j]); + } + auto param = ConvertParams(elems); + ASSERT_EQ(KVDKListBatchPushBack(engine, key.data(), key.size(), + param.first.data(), param.second.data(), + elems.size()), + KVDKStatus::Ok); + size_t sz; + ASSERT_EQ(KVDKListSize(engine, key.data(), key.size(), &sz), + KVDKStatus::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto LBatchPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + std::string buffer1; + std::string buffer2; + ASSERT_EQ(KVDKListBatchPopFront(engine, key.data(), key.size(), count, + ConcatStrings, &buffer1), + KVDKStatus::Ok); + for (size_t j = 0; j < count && !list_copy.empty(); j++) { + ConcatStrings(list_copy.front().data(), list_copy.front().size(), + &buffer2); + list_copy.pop_front(); + } + ASSERT_EQ(buffer1, buffer2); + size_t sz; + ASSERT_EQ(KVDKListSize(engine, key.data(), key.size(), &sz), + KVDKStatus::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto RBatchPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + std::string buffer1; + std::string buffer2; + ASSERT_EQ(KVDKListBatchPopBack(engine, key.data(), key.size(), count, + ConcatStrings, &buffer1), + KVDKStatus::Ok); + for (size_t j = 0; j < count && !list_copy.empty(); j++) { + ConcatStrings(list_copy.back().data(), list_copy.back().size(), &buffer2); + list_copy.pop_back(); + } + ASSERT_EQ(buffer1, buffer2); + size_t sz; + ASSERT_EQ(KVDKListSize(engine, key.data(), key.size(), &sz), + KVDKStatus::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto RPushLPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + + ASSERT_FALSE(list_copy.empty()); + + auto elem_copy = list_copy.front(); + list_copy.push_back(elem_copy); + list_copy.pop_front(); + + char* elem_data; + size_t elem_len; + ASSERT_EQ(KVDKListMove(engine, key.data(), key.size(), KVDK_LIST_FRONT, + key.data(), key.size(), KVDK_LIST_BACK, &elem_data, + &elem_len), + KVDKStatus::Ok); + ASSERT_EQ(std::string(elem_data, elem_len), elem_copy); + free(elem_data); + + size_t sz; + ASSERT_EQ(KVDKListSize(engine, key.data(), key.size(), &sz), + KVDKStatus::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + for (size_t i = 0; i < 3; i++) { + LaunchNThreads(num_threads, LPop); + LaunchNThreads(num_threads, RPop); + LaunchNThreads(num_threads, LPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LBatchPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RBatchPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LBatchPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RBatchPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LPush); + LaunchNThreads(num_threads, ListIterate); + for (size_t j = 0; j < 100; j++) { + LaunchNThreads(num_threads, ListInsertPutRemove); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPushLPop); + LaunchNThreads(num_threads, ListIterate); + } + RebootDB(); + } +} diff --git a/volatile/tests/stress_test.cpp b/volatile/tests/stress_test.cpp new file mode 100644 index 00000000..933d5b31 --- /dev/null +++ b/volatile/tests/stress_test.cpp @@ -0,0 +1,983 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kvdk/volatile/engine.hpp" +#include "kvdk/volatile/types.hpp" +#include "test_util.h" + +DEFINE_bool( + verbose, false, + "If set true, stress test will print a progress bar for operations."); + +DEFINE_string(path, "/mnt/pmem0/kvdk_stress_test", + "Path of KVDK instance on PMem."); + +using kvdk::StringView; + +namespace kvdk_testing { + +using KeyType = StringView; +using ValueType = StringView; +using CollectionNameType = StringView; + +// Operators are just wrappers of the engine and collection name +// They offer an universal interface for calling KVEngine APIs +class HashesOperator { + kvdk::Engine*& engine; + CollectionNameType collection_name; + + public: + HashesOperator() = delete; + HashesOperator(kvdk::Engine*& e, CollectionNameType cn) + : engine{e}, collection_name{cn} {} + kvdk::Status operator()(KeyType key, std::string* value_got) { + return engine->HashGet(collection_name, key, value_got); + } + kvdk::Status operator()(KeyType key, ValueType value) { + return engine->HashPut(collection_name, key, value); + } + kvdk::Status operator()(KeyType key) { + return engine->HashDelete(collection_name, key); + } +}; + +class SortedOperator { + kvdk::Engine*& engine; + CollectionNameType collection_name; + + public: + SortedOperator() = delete; + SortedOperator(kvdk::Engine*& e, CollectionNameType cn) + : engine{e}, collection_name{cn} {} + kvdk::Status operator()(KeyType key, std::string* value_got) { + return engine->SortedGet(collection_name, key, value_got); + } + kvdk::Status operator()(KeyType key, ValueType value) { + return engine->SortedPut(collection_name, key, value); + } + kvdk::Status operator()(KeyType key) { + return engine->SortedDelete(collection_name, key); + } +}; + +class StringOperator { + kvdk::Engine*& engine; + CollectionNameType collection_name; + + public: + StringOperator() = delete; + // For convenince, introducing empty collection_name for global anonymous + // collection + StringOperator(kvdk::Engine*& e, CollectionNameType cn) + : engine{e}, collection_name{} { + if (cn != collection_name) throw; + } + kvdk::Status operator()(KeyType key, std::string* value_got) { + return engine->Get(key, value_got); + } + kvdk::Status operator()(KeyType key, ValueType value) { + return engine->Put(key, value); + } + kvdk::Status operator()(KeyType key) { return engine->Delete(key); } +}; + +enum class IteratingDirection { Forward, Backward }; + +// A ShadowKVEngine operates on one KVEngine collection, +// including the global anonymous collection(string). +// User should call EvenXSetOddXSet() first to modify KVEngine, +// then call UpdatePossibleStates() to update possible_state to +// keep track of the state of the KVEngine. +template +class ShadowKVEngine { + public: + struct StateAndValue { + enum class State { Existing, Deleted } state; + ValueType value; + + bool operator==(StateAndValue other) { + bool match_state = (state == other.state); + bool match_value = + ((state == State::Existing) && (value == other.value)) || + (state == State::Deleted); + return match_state && match_value; + } + + friend std::ostream& operator<<(std::ostream& out, + StateAndValue const& vstate) { + out << "State: " + << (vstate.state == StateAndValue::State::Deleted ? "Deleted" + : "Existing") + << "\t" + << "Value: " << vstate.value << "\n"; + return out; + } + }; + + struct SingleOp { + enum class OpType { Get, Put, Delete } op; + KeyType key; + ValueType value; // Empty for Delete, expected for Get + + // For printing error message + friend std::ostream& operator<<(std::ostream& out, SingleOp const& sop) { + out << "Op: " + << (sop.op == OpType::Get + ? "Get" + : (sop.op == OpType::Put ? "Put" : "Delete")) + << "\n" + << "Key: " << sop.key << "\n" + << "Value: " << sop.value << "\n"; + return out; + } + }; + + using OperationQueue = std::deque; + using PossibleStates = std::unordered_multimap; + using StagedChanges = std::unordered_map; + + private: + kvdk::Engine*& engine; + CollectionNameType collection_name; + EngineOperator engine_operator; + size_t const n_thread; + // A Key may have multiple possible StateAndValue. + // possible_state keep track of these StateAndValues + PossibleStates possible_state; + std::vector task_queues; + + public: + ShadowKVEngine() = delete; + ShadowKVEngine(kvdk::Engine*& e, CollectionNameType cn, size_t nt) + : engine{e}, + collection_name{cn}, + engine_operator{engine, collection_name}, + n_thread{nt}, + possible_state{}, + task_queues(n_thread) {} + + // Execute task_queues in ShadowKVEngine + // Update possible_state + /// TODO: make this private, put it in operateKVEngine(), which should + /// run multiple threads. + void UpdatePossibleStates() { + std::cout << "[Testing] Updating Engine State" << std::endl; + + // Some keys are overwritten by operations in operateKVEngine(), + // states and values before calling operateKVEngine() are + // no longer possible. + { + ProgressBar pbar{std::cout, "", n_thread, 1, FLAGS_verbose}; + for (size_t tid = 0; tid < n_thread; tid++) { + for (auto const& sop : task_queues[tid]) { + possible_state.erase(sop.key); + } + pbar.Update(tid + 1); + } + } + + // Squash every task queue and merge into possible_state + { + ProgressBar pbar{std::cout, "", n_thread, 1, FLAGS_verbose}; + for (size_t tid = 0; tid < n_thread; tid++) { + StagedChanges squashed_changes{task_queues[tid].size() * 2}; + for (auto const& sop : task_queues[tid]) { + switch (sop.op) { + case SingleOp::OpType::Get: { + // Get will not change the state of any KV + continue; + } + case SingleOp::OpType::Put: { + squashed_changes[sop.key] = + StateAndValue{StateAndValue::State::Existing, sop.value}; + continue; + } + case SingleOp::OpType::Delete: { + squashed_changes[sop.key] = + StateAndValue{StateAndValue::State::Deleted, ValueType{}}; + continue; + } + } + } + for (auto const& kvs : squashed_changes) { + possible_state.emplace(kvs); + } + pbar.Update(tid + 1); + } + } + task_queues.clear(); + task_queues.resize(n_thread); + } + + // Modify KVEngine by Put + void EvenXSetOddXSet(size_t tid, std::vector const& keys, + std::vector const& values) { + task_queues[tid] = generateOperations(keys, values, false); + operateKVEngine(tid, (tid == 0) ? FLAGS_verbose : false); + } + + // Modify KVEngine by Put and Delete + void EvenXSetOddXDelete(size_t tid, std::vector const& keys, + std::vector const& values) { + task_queues[tid] = generateOperations(keys, values, true); + operateKVEngine(tid, (tid == 0) ? FLAGS_verbose : false); + } + + // Check KVEngine by iterating through it. + // Iterated KVs are looked up in possible_state. + template + void CheckIterator(Iterator* iterator, IteratingDirection direction) { + PossibleStates possible_state_copy{possible_state}; + + // Iterating forward or backward. + { + ASSERT_TRUE(iterator != nullptr) << "Invalid Iterator"; + switch (direction) { + case IteratingDirection::Forward: { + std::cout << "[Testing] Iterating forward." << std::endl; + iterator->SeekToFirst(); + break; + } + case IteratingDirection::Backward: { + std::cout << "[Testing] Iterating backward." << std::endl; + iterator->SeekToLast(); + break; + } + } + + ProgressBar pbar{std::cout, "", possible_state.size(), 1000, + FLAGS_verbose}; + while (iterator->Valid()) { + auto key = iterator->Key(); + auto value = iterator->Value(); + + checkState(key, {StateAndValue::State::Existing, value}); + + possible_state_copy.erase(key); + pbar.Update(possible_state.size() - possible_state_copy.size()); + + switch (direction) { + case IteratingDirection::Forward: + iterator->Next(); + break; + case IteratingDirection::Backward: + iterator->Prev(); + break; + } + } + // Remaining kv-pairs in possible_kv_pairs are deleted kv-pairs + { + while (!possible_state_copy.empty()) { + auto key = possible_state_copy.begin()->first; + + checkState(key, {StateAndValue::State::Deleted, ValueType{}}); + + possible_state_copy.erase(key); + pbar.Update(possible_state.size() - possible_state_copy.size()); + } + } + } + } + + // Check KVEngine by get every key in possible_state + // and check its value and state. + void CheckGetter() { + kvdk::Status status; + std::string value_got; + PossibleStates possible_state_copy{possible_state}; + { + std::cout << "[Testing] Checking by Get" << std::endl; + ProgressBar pbar{std::cout, "", possible_state.size(), 1000, + FLAGS_verbose}; + while (!possible_state_copy.empty()) { + auto key = possible_state_copy.begin()->first; + + status = engine_operator(key, &value_got); + switch (status) { + case kvdk::Status::Ok: { + checkState(key, {StateAndValue::State::Existing, value_got}); + break; + } + case kvdk::Status::NotFound: { + checkState(key, {StateAndValue::State::Deleted, ValueType{}}); + break; + } + default: { + ASSERT_TRUE(false) << "Invalid kvdk status in CheckGetter."; + break; + } + } + + possible_state_copy.erase(key); + pbar.Update(possible_state.size() - possible_state_copy.size()); + } + } + } + + private: + // Excecute task_queues in KVEngine by calling EngineOperator + // ShadowKVEngine remains unchanged + void operateKVEngine(size_t tid, bool enable_progress_bar) { + OperationQueue const& tasks = task_queues[tid]; + + kvdk::Status status; + std::string value_got; + size_t progress = 0; + ProgressBar progress_bar{std::cout, "", tasks.size(), 100, + enable_progress_bar}; + /// TODO: Catch kill point and clean up tasks + for (auto const& task : tasks) { + switch (task.op) { + case SingleOp::OpType::Get: { + status = engine_operator(task.key, &value_got); + ASSERT_EQ(status, kvdk::Status::Ok) + << "Key cannot be queried with Get\n" + << "Key: " << task.key << "\n"; + ASSERT_EQ(task.value, value_got) + << "Value got does not match expected\n" + << "Value got:\n" + << value_got << "\n" + << "Expected:\n" + << task.value << "\n"; + break; + } + case SingleOp::OpType::Put: { + status = engine_operator(task.key, task.value); + ASSERT_EQ(status, kvdk::Status::Ok) << "Fail to set key\n" + << "Key: " << task.key << "\n"; + break; + } + case SingleOp::OpType::Delete: { + status = engine_operator(task.key); + ASSERT_EQ(status, kvdk::Status::Ok) << "Fail to delete key\n" + << "Key: " << task.key << "\n"; + break; + } + } + ++progress; + progress_bar.Update(progress); + } + } + + // Check whether a key and corresponding state is in possible_state + void checkState(KeyType key, StateAndValue vstate) { + auto ranges = possible_state.equal_range(key); + + bool match = false; + for (auto iter = ranges.first; iter != ranges.second; ++iter) { + match = (match || (vstate == iter->second)); + } + std::stringstream ss; + if (!match) { + for (auto iter = ranges.first; iter != ranges.second; ++iter) { + ss << iter->second << "\n"; + } + } + + ASSERT_TRUE(match) << "Key and State supplied is not possible:\n" + << "Key: " << key << "\n" + << "Supplied:\n" + << vstate << "\n" + << "Possible:\n" + << ss.str() << "\n"; + } + + static OperationQueue generateOperations(std::vector const& keys, + std::vector const& values, + bool interleaved_set_delete) { + OperationQueue queue(keys.size()); + for (size_t i = 0; i < queue.size(); i++) { + if (i % 2 == 0 || !interleaved_set_delete) { + queue[i] = {SingleOp::OpType::Put, keys[i], values[i]}; + } else { + queue[i] = {SingleOp::OpType::Delete, keys[i], ValueType{}}; + } + } + return queue; + } +}; +} // namespace kvdk_testing + +class EngineTestBase : public testing::Test { + protected: + kvdk::Engine* engine = nullptr; + kvdk::Configs configs; + kvdk::Status status; + + /// The following parameters are used to configure the test. + /// Override SetUpParameters to provide different parameters + /// Default configure parameters + size_t n_hash_bucket; + size_t t_background_work_interval; + + /// Test specific parameters + size_t n_thread; + size_t n_kv_per_thread; + // These parameters set the range of sizes of keys and values + size_t sz_key_min; + size_t sz_key_max; + size_t sz_value_min; + size_t sz_value_max; + + // Actual keys an values used by thread for insertion + std::vector> grouped_keys; + std::vector> grouped_values; + + using ShadowHashes = + kvdk_testing::ShadowKVEngine; + using ShadowSorted = + kvdk_testing::ShadowKVEngine; + using ShadowString = + kvdk_testing::ShadowKVEngine; + + std::unordered_map> + shadow_hashes_engines; + std::unordered_map> + shadow_sorted_engines; + std::unique_ptr shadow_string_engine; + + private: + std::vector key_pool; + std::vector value_pool; + std::default_random_engine rand{42}; + + protected: + /// Other tests should overload this function to setup parameters + virtual void SetUpParameters() = 0; + + virtual void SetUp() override { + purgeDB(); + + SetUpParameters(); + + configs.hash_bucket_num = n_hash_bucket; + configs.background_work_interval = t_background_work_interval; + configs.max_access_threads = n_thread + 1; + configs.log_level = kvdk::LogLevel::Debug; + + prepareKVPairs(); + + status = kvdk::Engine::Open(FLAGS_path, &engine, configs, stderr); + ASSERT_EQ(status, kvdk::Status::Ok) << "Fail to open the KVDK instance"; + } + + virtual void TearDown() { + delete engine; + purgeDB(); + } + + void RebootDB() { + delete engine; + + status = kvdk::Engine::Open(FLAGS_path, &engine, configs, stderr); + ASSERT_EQ(status, kvdk::Status::Ok) << "Fail to open the KVDK instance"; + } + + void ShuffleAllKeysValuesWithinThread() { + for (size_t tid = 0; tid < n_thread; tid++) { + shuffleKeys(tid); + shuffleValues(tid); + } + } + + void HashesAllHSet(std::string const& collection_name) { + ShuffleAllKeysValuesWithinThread(); + auto ModifyEngine = [&](int tid) { + shadow_hashes_engines[collection_name]->EvenXSetOddXSet( + tid, grouped_keys[tid], grouped_values[tid]); + }; + + std::cout << "[Testing] Execute HashesAllHSet in " << collection_name << "." + << std::endl; + LaunchNThreads(n_thread, ModifyEngine); + shadow_hashes_engines[collection_name]->UpdatePossibleStates(); + } + + void HashesEvenHSetOddHDelete(std::string const& collection_name) { + ShuffleAllKeysValuesWithinThread(); + auto ModifyEngine = [&](int tid) { + shadow_hashes_engines[collection_name]->EvenXSetOddXDelete( + tid, grouped_keys[tid], grouped_values[tid]); + }; + + std::cout << "[Testing] Execute HashesEvenHSetOddHDelete in " + << collection_name << "." << std::endl; + LaunchNThreads(n_thread, ModifyEngine); + shadow_hashes_engines[collection_name]->UpdatePossibleStates(); + } + + void SortedAllPut(std::string const& collection_name) { + ShuffleAllKeysValuesWithinThread(); + auto ModifyEngine = [&](int tid) { + shadow_sorted_engines[collection_name]->EvenXSetOddXSet( + tid, grouped_keys[tid], grouped_values[tid]); + }; + + std::cout << "[Testing] Execute SortedAllPut in " << collection_name << "." + << std::endl; + LaunchNThreads(n_thread, ModifyEngine); + shadow_sorted_engines[collection_name]->UpdatePossibleStates(); + } + + void SortedEvenPutOddDelete(std::string const& collection_name) { + ShuffleAllKeysValuesWithinThread(); + auto ModifyEngine = [&](int tid) { + shadow_sorted_engines[collection_name]->EvenXSetOddXDelete( + tid, grouped_keys[tid], grouped_values[tid]); + }; + + std::cout << "[Testing] Execute SortedEvenPutOddDelete in " + << collection_name << "." << std::endl; + LaunchNThreads(n_thread, ModifyEngine); + shadow_sorted_engines[collection_name]->UpdatePossibleStates(); + } + + void StringAllSet() { + ShuffleAllKeysValuesWithinThread(); + auto ModifyEngine = [&](int tid) { + shadow_string_engine->EvenXSetOddXSet(tid, grouped_keys[tid], + grouped_values[tid]); + }; + + std::cout << "[Testing] Execute StringAllSet " << std::endl; + LaunchNThreads(n_thread, ModifyEngine); + shadow_string_engine->UpdatePossibleStates(); + } + + void StringEvenSetOddDelete() { + ShuffleAllKeysValuesWithinThread(); + auto ModifyEngine = [&](int tid) { + shadow_string_engine->EvenXSetOddXDelete(tid, grouped_keys[tid], + grouped_values[tid]); + }; + + std::cout << "[Testing] Execute StringEvenSetOddDelete " << std::endl; + LaunchNThreads(n_thread, ModifyEngine); + shadow_string_engine->UpdatePossibleStates(); + } + + void CheckHashesCollection(std::string collection_name) { + std::cout << "[Testing] Checking Hashes Collection: " << collection_name + << std::endl; + shadow_hashes_engines[collection_name]->CheckGetter(); + auto iter = engine->HashIteratorCreate(collection_name); + shadow_hashes_engines[collection_name]->CheckIterator( + iter, kvdk_testing::IteratingDirection::Forward); + shadow_hashes_engines[collection_name]->CheckIterator( + iter, kvdk_testing::IteratingDirection::Backward); + engine->HashIteratorRelease(iter); + } + + void CheckSortedCollection(std::string collection_name) { + std::cout << "[Testing] Checking Sorted Collection: " << collection_name + << std::endl; + shadow_sorted_engines[collection_name]->CheckGetter(); + auto iter = engine->SortedIteratorCreate(collection_name); + shadow_sorted_engines[collection_name]->CheckIterator( + iter, kvdk_testing::IteratingDirection::Forward); + shadow_sorted_engines[collection_name]->CheckIterator( + iter, kvdk_testing::IteratingDirection::Backward); + engine->SortedIteratorRelease(iter); + } + + void CheckStrings() { + std::cout << "[Testing] Checking strings." << std::endl; + shadow_string_engine->CheckGetter(); + } + + void InitializeStrings() { + shadow_string_engine.reset( + new ShadowString{engine, kvdk_testing::CollectionNameType{}, n_thread}); + } + + void InitializeHashes(std::string const& collection_name) { + shadow_hashes_engines[collection_name].reset( + new ShadowHashes{engine, collection_name, n_thread}); + } + + void InitializeSorted(std::string const& collection_name) { + shadow_sorted_engines[collection_name].reset( + new ShadowSorted{engine, collection_name, n_thread}); + } + + void RemoveOutDatedSorted(std::string const& collection_name) { + if (shadow_sorted_engines.find(collection_name) != + shadow_sorted_engines.end()) { + shadow_sorted_engines.erase(collection_name); + } + } + + void RemoveOutDatedHashes(std::string const& collection_name) { + if (shadow_hashes_engines.find(collection_name) != + shadow_hashes_engines.end()) { + shadow_hashes_engines.erase(collection_name); + } + } + + private: + void purgeDB() { + std::string cmd = "rm -rf " + FLAGS_path + "\n"; + [[gnu::unused]] int _sink = system(cmd.data()); + } + + void shuffleKeys(size_t tid) { + std::shuffle(grouped_keys[tid].begin(), grouped_keys[tid].end(), rand); + } + + void shuffleValues(size_t tid) { + std::shuffle(grouped_values[tid].begin(), grouped_values[tid].end(), rand); + } + + void prepareKVPairs() { + key_pool.reserve(n_thread * n_kv_per_thread); + value_pool.reserve(n_kv_per_thread); + grouped_keys.resize(n_thread); + grouped_values.resize(n_thread); + + for (size_t tid = 0; tid < n_thread; tid++) { + grouped_keys[tid].reserve(n_kv_per_thread); + grouped_values[tid].reserve(n_kv_per_thread); + } + + std::cout << "[Testing] Generating string for keys and values" << std::endl; + { + ProgressBar progress_gen_kv{std::cout, "", n_kv_per_thread, 1, + FLAGS_verbose}; + for (size_t i = 0; i < n_kv_per_thread; i++) { + value_pool.push_back(GetRandomString(sz_value_min, sz_value_max)); + for (size_t tid = 0; tid < n_thread; tid++) { + key_pool.push_back(GetRandomString(sz_key_min, sz_key_max)); + } + + if ((i + 1) % 1000 == 0 || (i + 1) == n_kv_per_thread) { + progress_gen_kv.Update(i + 1); + } + } + } + std::cout << "[Testing] Generating string_view for keys and values" + << std::endl; + { + ProgressBar progress_gen_kv_view{std::cout, "", n_thread, 1, + FLAGS_verbose}; + for (size_t tid = 0; tid < n_thread; tid++) { + for (size_t i = 0; i < n_kv_per_thread; i++) { + grouped_keys[tid].emplace_back(key_pool[i * n_thread + tid]); + grouped_values[tid].emplace_back(value_pool[i]); + } + progress_gen_kv_view.Update(tid + 1); + } + } + } +}; + +class EngineStressTest : public EngineTestBase { + protected: + virtual void SetUpParameters() override final { + /// Default configure parameters + // Less buckets to increase hash collisions + n_hash_bucket = (1ULL << 20); + t_background_work_interval = 1; + + /// Test specific parameters + n_thread = 32; + // 1M keys per thread, totaling about 32M(actually less) records + n_kv_per_thread = (1ULL << 20); + // These parameters set the range of sizes of keys and values + sz_key_min = 2; + sz_key_max = 16; + sz_value_min = 0; + sz_value_max = 1024; + } + // Shared among EngineStressTest + const size_t n_reboot = 3; +}; + +TEST_F(EngineStressTest, HashesHSetOnly) { + std::string global_collection_name{"HashesCollection"}; + InitializeHashes(global_collection_name); + ASSERT_EQ(engine->HashCreate(global_collection_name), kvdk::Status::Ok); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + HashesAllHSet(global_collection_name); + CheckHashesCollection(global_collection_name); + + RebootDB(); + CheckHashesCollection(global_collection_name); + } +} + +TEST_F(EngineStressTest, HashesHSetAndHDelete) { + std::string global_collection_name{"HashesCollection"}; + InitializeHashes(global_collection_name); + ASSERT_EQ(engine->HashCreate(global_collection_name), kvdk::Status::Ok); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + HashesEvenHSetOddHDelete(global_collection_name); + CheckHashesCollection(global_collection_name); + + RebootDB(); + CheckHashesCollection(global_collection_name); + } +} + +TEST_F(EngineStressTest, SortedPutOnly) { + for (int index_with_hashtable : {0, 1}) { + std::string global_collection_name{"SortedCollection" + + index_with_hashtable}; + InitializeSorted(global_collection_name); + + kvdk::SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = index_with_hashtable; + ASSERT_EQ(engine->SortedCreate(global_collection_name, s_configs), + kvdk::Status::Ok); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + std::cout << "Sorted collection index with hashtable: " + << index_with_hashtable << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + SortedAllPut(global_collection_name); + CheckSortedCollection(global_collection_name); + + RebootDB(); + CheckSortedCollection(global_collection_name); + } + } +} + +TEST_F(EngineStressTest, SortedPutAndDelete) { + for (int index_with_hashtable : {0, 1}) { + std::string global_collection_name{"SortedCollection" + + index_with_hashtable}; + InitializeSorted(global_collection_name); + + kvdk::SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = index_with_hashtable; + ASSERT_EQ(engine->SortedCreate(global_collection_name, s_configs), + kvdk::Status::Ok); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + std::cout << "Sorted collection index with hashtable: " + << index_with_hashtable << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + SortedEvenPutOddDelete(global_collection_name); + CheckSortedCollection(global_collection_name); + + RebootDB(); + CheckSortedCollection(global_collection_name); + } + } +} + +TEST_F(EngineStressTest, StringSetOnly) { + InitializeStrings(); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + StringAllSet(); + CheckStrings(); + + RebootDB(); + CheckStrings(); + } +} + +TEST_F(EngineStressTest, StringSetAndDelete) { + InitializeStrings(); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + StringEvenSetOddDelete(); + CheckStrings(); + + RebootDB(); + CheckStrings(); + } +} + +class EngineHotspotTest : public EngineTestBase { + protected: + virtual void SetUpParameters() override final { + /// Default configure parameters + // Less buckets to increase hash collisions + n_hash_bucket = (1ULL << 20); + t_background_work_interval = 1; + + /// Test specific parameters + // Too many threads will make this test too slow + n_thread = 4; + // 1M keys per thread + n_kv_per_thread = (1ULL << 20); + // 0-sized key "" is a hotspot, which may reveal many defects + // These parameters set the range of sizes of keys and values + sz_key_min = 0; + sz_key_max = 8; + sz_value_min = 0; + sz_value_max = 128; + } + + size_t n_repeat = 10; + size_t n_reboot = 3; +}; + +TEST_F(EngineHotspotTest, HashesMultipleHotspot) { + std::string global_collection_name{"HashesCollection"}; + InitializeHashes(global_collection_name); + ASSERT_EQ(engine->HashCreate(global_collection_name), kvdk::Status::Ok); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + for (size_t i = 0; i < n_repeat; i++) { + HashesAllHSet(global_collection_name); + CheckHashesCollection(global_collection_name); + HashesEvenHSetOddHDelete(global_collection_name); + CheckHashesCollection(global_collection_name); + } + RebootDB(); + CheckHashesCollection(global_collection_name); + } +} + +TEST_F(EngineHotspotTest, SortedMultipleHotspot) { + for (int index_with_hashtable : {0, 1}) { + std::string global_collection_name{"SortedCollection" + + index_with_hashtable}; + InitializeSorted(global_collection_name); + + kvdk::SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = index_with_hashtable; + ASSERT_EQ(engine->SortedCreate(global_collection_name, s_configs), + kvdk::Status::Ok); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + std::cout << "Sorted collection index with hashtable: " + << index_with_hashtable << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + for (size_t i = 0; i < n_repeat; i++) { + SortedAllPut(global_collection_name); + CheckSortedCollection(global_collection_name); + SortedEvenPutOddDelete(global_collection_name); + CheckSortedCollection(global_collection_name); + } + RebootDB(); + CheckSortedCollection(global_collection_name); + } + } +} + +TEST_F(EngineHotspotTest, StringMultipleHotspot) { + InitializeStrings(); + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + + for (size_t i = 0; i < n_repeat; i++) { + StringAllSet(); + CheckStrings(); + StringEvenSetOddDelete(); + CheckStrings(); + } + RebootDB(); + CheckStrings(); + } +} + +TEST_F(EngineStressTest, BackgroundCleanerTest) { + std::string global_sorted_collection_name{"SortedCollection_withHashTable"}; + std::string global_hash_collection_name{"HashCollection"}; + + kvdk::SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = 1; + + std::cout << "[Testing] Modify, check, reboot and check engine for " + << n_reboot << " times." << std::endl; + for (size_t i = 0; i < n_reboot; i++) { + InitializeSorted(global_sorted_collection_name); + InitializeHashes(global_hash_collection_name); + InitializeStrings(); + std::cout << "[Testing] Repeat: " << i + 1 << std::endl; + std::cout << " Create Sorted collection index with hashtable: " << 1 + << std::endl; + ASSERT_EQ(engine->SortedCreate(global_sorted_collection_name, s_configs), + kvdk::Status::Ok); + std::cout << "Create Hash collection" << std::endl; + ASSERT_EQ(engine->HashCreate(global_hash_collection_name), + kvdk::Status::Ok); + for (size_t j = 0; j < 2; j++) { + // Sorted + SortedAllPut(global_sorted_collection_name); + CheckSortedCollection(global_sorted_collection_name); + SortedEvenPutOddDelete(global_sorted_collection_name); + CheckSortedCollection(global_sorted_collection_name); + // String + StringAllSet(); + CheckStrings(); + StringEvenSetOddDelete(); + CheckStrings(); + // Hash + HashesAllHSet(global_hash_collection_name); + CheckHashesCollection(global_hash_collection_name); + HashesEvenHSetOddHDelete(global_hash_collection_name); + CheckHashesCollection(global_hash_collection_name); + } + + RebootDB(); + CheckSortedCollection(global_sorted_collection_name); + CheckHashesCollection(global_hash_collection_name); + ASSERT_EQ(engine->SortedDestroy(global_sorted_collection_name), + kvdk::Status::Ok); + RemoveOutDatedSorted(global_sorted_collection_name); + ASSERT_EQ(engine->Expire(global_hash_collection_name, -1), + kvdk::Status::Ok); + RemoveOutDatedHashes(global_hash_collection_name); + } +} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + google::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} diff --git a/volatile/tests/test_util.h b/volatile/tests/test_util.h new file mode 100644 index 00000000..163e93d3 --- /dev/null +++ b/volatile/tests/test_util.h @@ -0,0 +1,155 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Create a string that contains 8 bytes from uint64_t. */ +inline std::string uint64_to_string(uint64_t& key) { + return std::string(reinterpret_cast(&key), 8); +} + +inline void random_str(char* str, unsigned int size) { + for (unsigned int i = 0; i < size; i++) { + switch (rand() % 3) { + case 0: + str[i] = rand() % 10 + '0'; + break; + case 1: + str[i] = rand() % 26 + 'A'; + break; + case 2: + str[i] = rand() % 26 + 'a'; + break; + default: + break; + } + } + str[size] = 0; +} + +// Return a string of length len with random characters in ['a', 'z'] +inline std::string GetRandomString(size_t len) { + static std::default_random_engine re; + std::string str; + str.reserve(len); + for (size_t i = 0; i < len; i++) str.push_back('a' + re() % 26); + return str; +} + +// Return a string of length in [min_len, max_len] with random characters in +// ['a', 'z'] +inline std::string GetRandomString(size_t min_len, size_t max_len) { + static std::default_random_engine re; + size_t len = min_len + re() % (max_len - min_len + 1); + return GetRandomString(len); +} + +inline void LaunchNThreads(int n_thread, std::function func, + int id_start = 0) { + std::vector ts; + for (int i = id_start; i < id_start + n_thread; i++) { + ts.emplace_back(std::thread(func, i)); + } + for (auto& t : ts) t.join(); +} + +class ProgressBar { + private: + std::ostream& out_stream_; + std::string tag_; + size_t total_progress_; + size_t current_progress_; + size_t last_report_; + size_t report_interval_; + size_t bar_length_; + size_t step_; + bool enabled_; + + bool flush_newline_{false}; + + static constexpr char symbol_done_{'#'}; + static constexpr char symbol_fill_{'-'}; + + public: + explicit ProgressBar(std::ostream& out, std::string tag, + size_t total_progress, size_t report_interval, + bool enabled, size_t bar_length = 50) + : out_stream_{out}, + tag_{tag}, + total_progress_{total_progress}, + current_progress_{0}, + last_report_{0}, + report_interval_{report_interval}, + bar_length_{bar_length}, + step_{total_progress / bar_length}, + enabled_{enabled} { + assert(total_progress_ > 0); + assert(bar_length_ > 0); + if (step_ == 0) { + step_ = 1; + bar_length_ = total_progress; + } + // Actual bar length may be 1 char longer than given bar length + // 51 = 2048 / (2048 / 50). This prevents overflowing the bar + bar_length_ = total_progress_ / step_; + + showProgress(); + } + + void Update(size_t current_progress) { + assert(!flush_newline_ && "Trying to update a completed progress!"); + assert(current_progress_ < current_progress && + current_progress <= total_progress_); + + current_progress_ = current_progress; + if (current_progress_ == total_progress_) { + flush_newline_ = true; + } + + if (last_report_ + report_interval_ <= current_progress_ || + current_progress_ == total_progress_) { + showProgress(); + last_report_ = current_progress_; + } + } + + ~ProgressBar() { + if (!flush_newline_) { + flush_newline_ = true; + showProgress(); + } + } + + private: + void showProgress() { + if (!enabled_) return; + + assert(current_progress_ <= current_progress_); + + out_stream_ << "\r" << tag_ << std::setw(10) << std::right + << current_progress_ << "/" << std::setw(10) << std::left + << total_progress_ << "\t" + << "["; + + { + size_t n_step_done = current_progress_ / step_; + for (size_t i = 0; i < n_step_done; i++) out_stream_ << symbol_done_; + for (size_t i = 0; i < bar_length_ - n_step_done; i++) + out_stream_ << symbol_fill_; + } + + out_stream_ << "]" << std::flush; + + if (flush_newline_) out_stream_ << std::endl; + } +}; diff --git a/volatile/tests/tests.cpp b/volatile/tests/tests.cpp new file mode 100644 index 00000000..34ed875c --- /dev/null +++ b/volatile/tests/tests.cpp @@ -0,0 +1,2573 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2021 Intel Corporation + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include "../engine/kv_engine.hpp" +#include "../engine/utils/sync_point.hpp" +#include "kvdk/volatile/engine.hpp" +#include "test_util.h" + +DEFINE_string(path, "/mnt/pmem0/kvdk_unit_test", + "Path of KVDK instance on PMem."); + +using namespace KVDK_NAMESPACE; +static const uint64_t str_pool_length = 1024000; + +using PutOpsFunc = + std::function; +using DeleteOpsFunc = std::function; + +using DestroyFunc = std::function; + +using GetOpsFunc = std::function; + +enum class Types { String, Sorted, Hash }; + +class EngineBasicTest : public testing::Test { + protected: + Engine* engine = nullptr; + Configs configs; + std::string db_path; + std::string backup_path; + std::string backup_log; + std::string str_pool; + + virtual void SetUp() override { + str_pool.resize(str_pool_length); + random_str(&str_pool[0], str_pool_length); + // No logs by default, for debug, set it to All + configs.log_level = LogLevel::Debug; + configs.hash_bucket_num = (1 << 10); + // For faster test, no interval so it would not block engine closing + configs.background_work_interval = 0.1; + configs.max_access_threads = 8; + db_path = FLAGS_path; + backup_path = FLAGS_path + "_backup"; + backup_log = FLAGS_path + ".backup"; + char cmd[1024]; + sprintf(cmd, "rm -rf %s && rm -rf %s && rm -rf %s\n", db_path.c_str(), + backup_path.c_str(), backup_log.c_str()); + int res __attribute__((unused)) = system(cmd); + config_option_ = OptionConfig::Default; + cnt_ = 500; + } + + virtual void TearDown() { +#if KVDK_DEBUG_LEVEL > 0 + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->Reset(); +#endif + Destroy(); + } + + std::string FastRandomString(int len) { + return std::string( + str_pool.data() + fast_random_64() % (str_pool_length - len), len); + } + + void Destroy() { + // delete db_path + char cmd[1024]; + sprintf(cmd, "rm -rf %s && rm -rf %s && rm -rf %s\n", db_path.c_str(), + backup_path.c_str(), backup_log.c_str()); + int res __attribute__((unused)) = system(cmd); + } + + bool ChangeConfig() { + config_option_++; + if (config_option_ >= End) { + return false; + } else { + ReCreateEngine(); + return engine != nullptr; + } + } + + void ReCreateEngine() { + delete engine; + engine = nullptr; + Destroy(); + configs = CurrentConfigs(); + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + } + + void Reboot() {} + + // Return the current configuration. + Configs CurrentConfigs() { + switch (config_option_) { + case OptRestore: + configs.opt_large_sorted_collection_recovery = true; + break; + default: + break; + } + return configs; + } + + // Put/Get/Delete + void TestString(uint64_t n_threads) { + auto StringPutFunc = [&](const std::string&, const std::string& key, + const std::string& value) -> Status { + return engine->Put(key, value); + }; + + auto StringGetFunc = [&](const std::string&, const std::string& key, + std::string* value) -> Status { + return engine->Get(key, value); + }; + + auto StringDeleteFunc = [&](const std::string&, + const std::string& key) -> Status { + return engine->Delete(key); + }; + + testEmptyKey("", StringPutFunc, StringGetFunc, StringDeleteFunc); + auto global_func = [=](uint64_t id) { + this->createBasicOperationTest("", StringPutFunc, StringGetFunc, + StringDeleteFunc, id); + }; + LaunchNThreads(n_threads, global_func); + } + + void TestGlobalSortedCollection(const std::string& collection, + const SortedCollectionConfigs& s_configs) { + auto SortedPutFunc = [&](const std::string& collection, + const std::string& key, + const std::string& value) -> Status { + return engine->SortedPut(collection, key, value); + }; + + auto SortedGetFunc = [&](const std::string& collection, + const std::string& key, + std::string* value) -> Status { + return engine->SortedGet(collection, key, value); + }; + + auto SortedDeleteFunc = [&](const std::string& collection, + const std::string& key) -> Status { + return engine->SortedDelete(collection, key); + }; + + auto SortedDestroyFunc = [&](const std::string& collection) { + return engine->SortedDestroy(collection); + }; + + ASSERT_EQ(engine->SortedCreate(collection, s_configs), Status::Ok); + + testDestroy(collection, SortedDestroyFunc, SortedPutFunc, SortedGetFunc, + SortedDeleteFunc); + + ASSERT_EQ(engine->SortedCreate(collection, s_configs), Status::Ok); + + auto global_func = [=](uint64_t id) { + this->createBasicOperationTest(collection, SortedPutFunc, SortedGetFunc, + SortedDeleteFunc, id); + }; + LaunchNThreads(configs.max_access_threads, global_func); + } + + void TestLocalSortedCollection(Engine* engine, const std::string& collection, + const SortedCollectionConfigs& s_configs) { + auto SortedPutFunc = [&](const std::string& collection, + const std::string& key, + const std::string& value) -> Status { + return engine->SortedPut(collection, key, value); + }; + + auto SortedGetFunc = [&](const std::string& collection, + const std::string& key, + std::string* value) -> Status { + return engine->SortedGet(collection, key, value); + }; + + auto SortedDeleteFunc = [&](const std::string& collection, + const std::string& key) -> Status { + return engine->SortedDelete(collection, key); + }; + + auto SortedDestroyFunc = [&](const std::string& collection) { + return engine->SortedDestroy(collection); + }; + + auto AccessTest = [&](uint64_t id) { + std::string thread_local_collection = collection + std::to_string(id); + ASSERT_EQ(engine->SortedCreate(thread_local_collection, s_configs), + Status::Ok); + + testEmptyKey(thread_local_collection, SortedPutFunc, SortedGetFunc, + SortedDeleteFunc); + testDestroy(thread_local_collection, SortedDestroyFunc, SortedPutFunc, + SortedGetFunc, SortedDeleteFunc); + + ASSERT_EQ(engine->SortedCreate(thread_local_collection, s_configs), + Status::Ok); + createBasicOperationTest(thread_local_collection, SortedPutFunc, + SortedGetFunc, SortedDeleteFunc, id); + }; + LaunchNThreads(configs.max_access_threads, AccessTest); + } + + void TestSortedIterator(const std::string& collection, + bool is_local = false) { + auto IteratingThrough = [&](uint32_t id) { + int entries = 0; + std::string new_collection = collection; + if (is_local) { + new_collection += std::to_string(id); + } + + size_t collection_size; + ASSERT_EQ(engine->SortedSize(new_collection, &collection_size), + Status::Ok); + + auto iter = engine->SortedIteratorCreate(new_collection); + ASSERT_TRUE(iter != nullptr); + // forward iterator + iter->SeekToFirst(); + if (iter->Valid()) { + ++entries; + std::string prev = iter->Key(); + iter->Next(); + while (iter->Valid()) { + ++entries; + std::string k = iter->Key(); + iter->Next(); + ASSERT_EQ(true, k.compare(prev) > 0); + prev = k; + } + } + ASSERT_EQ(collection_size, entries); + if (is_local) { + ASSERT_EQ(cnt_, entries); + } else { + ASSERT_EQ(cnt_ * configs.max_access_threads, entries); + } + + // backward iterator + iter->SeekToLast(); + if (iter->Valid()) { + --entries; + std::string next = iter->Key(); + iter->Prev(); + while (iter->Valid()) { + --entries; + std::string k = iter->Key(); + iter->Prev(); + ASSERT_EQ(true, k.compare(next) < 0); + next = k; + } + } + ASSERT_EQ(entries, 0); + engine->SortedIteratorRelease(iter); + }; + LaunchNThreads(configs.max_access_threads, IteratingThrough); + } + + private: + void testEmptyKey(const std::string& collection, PutOpsFunc PutFunc, + GetOpsFunc GetFunc, DeleteOpsFunc DeleteFunc) { + std::string key, val, got_val; + key = "", val = "val"; + ASSERT_EQ(PutFunc(collection, key, val), Status::Ok); + ASSERT_EQ(GetFunc(collection, key, &got_val), Status::Ok); + ASSERT_EQ(val, got_val); + ASSERT_EQ(DeleteFunc(collection, key), Status::Ok); + ASSERT_EQ(GetFunc(collection, key, &got_val), Status::NotFound); + } + + void testDestroy(const std::string& collection, DestroyFunc DestroyFunc, + PutOpsFunc PutFunc, GetOpsFunc GetFunc, + DeleteOpsFunc DeleteFunc) { + std::string key{"test_key"}; + std::string val{"test_val"}; + std::string got_val; + ASSERT_EQ(PutFunc(collection, key, val), Status::Ok); + ASSERT_EQ(GetFunc(collection, key, &got_val), Status::Ok); + ASSERT_EQ(val, got_val); + ASSERT_EQ(DestroyFunc(collection), Status::Ok); + ASSERT_EQ(PutFunc(collection, key, val), Status::NotFound); + ASSERT_EQ(GetFunc(collection, key, &got_val), Status::NotFound); + ASSERT_EQ(DeleteFunc(collection, key), Status::Ok); + } + + void createBasicOperationTest(const std::string& collection, + PutOpsFunc PutFunc, GetOpsFunc GetFunc, + DeleteOpsFunc DeleteFunc, uint32_t id) { + std::string val1, val2, got_val1, got_val2; + int t_cnt = cnt_; + while (t_cnt--) { + std::string key1(std::to_string(id) + "_" + std::to_string(t_cnt)); + std::string key2(std::to_string(id) + "@" + std::to_string(t_cnt)); + val1 = FastRandomString(fast_random_64() % 1024); + val2 = FastRandomString(fast_random_64() % 1024); + + // Put + ASSERT_EQ(PutFunc(collection, key1, val1), Status::Ok); + ASSERT_EQ(PutFunc(collection, key2, val2), Status::Ok); + + // Get + ASSERT_EQ(GetFunc(collection, key1, &got_val1), Status::Ok); + ASSERT_EQ(val1, got_val1); + ASSERT_EQ(GetFunc(collection, key2, &got_val2), Status::Ok); + ASSERT_EQ(val2, got_val2); + + // Delete + ASSERT_EQ(DeleteFunc(collection, key1), Status::Ok); + ASSERT_EQ(GetFunc(collection, key1, &got_val1), Status::NotFound); + + // Update + val2 = FastRandomString(fast_random_64() % 1024); + ASSERT_EQ(PutFunc(collection, key2, val2), Status::Ok); + ASSERT_EQ(GetFunc(collection, key2, &got_val2), Status::Ok); + ASSERT_EQ(got_val2, val2); + } + } + + // Sequence of option configurations to try + enum OptionConfig { Default, MultiThread, OptRestore, End }; + int config_option_; + int cnt_; +}; + +class BatchWriteTest : public EngineBasicTest {}; + +TEST_F(EngineBasicTest, TestUniqueKey) { + std::string sorted_collection("sorted_collection"); + std::string hash_collection("unordered_collection"); + std::string list("list"); + std::string str("str"); + std::string elem_key("elem"); + std::string val("val"); + + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + ASSERT_EQ(engine->Put(str, val), Status::Ok); + + ASSERT_EQ(engine->SortedCreate(sorted_collection), Status::Ok); + + ASSERT_EQ(engine->HashCreate(hash_collection), Status::Ok); + ASSERT_EQ(engine->HashPut(hash_collection, elem_key, val), Status::Ok); + + ASSERT_EQ(engine->ListCreate(list), Status::Ok); + ASSERT_EQ(engine->ListPushBack(list, elem_key), Status::Ok); + + std::string got_val; + // Test string + for (const std::string& string_key : + {sorted_collection, hash_collection, list, str}) { + Status op_ret = string_key == str ? Status::Ok : Status::WrongType; + std::string new_val("new_str_val"); + // Put + ASSERT_EQ(engine->Put(string_key, new_val), op_ret); + // Get + ASSERT_EQ(engine->Get(string_key, &got_val), op_ret); + if (op_ret == Status::Ok) { + ASSERT_EQ(got_val, new_val); + } + } + + // Test sorted + for (const std::string& collection_name : + {sorted_collection, hash_collection, list, str}) { + Status create_ret = collection_name == sorted_collection + ? Status::Existed + : Status::WrongType; + Status op_ret = + collection_name == sorted_collection ? Status::Ok : Status::WrongType; + std::string new_val("new_sorted_val"); + // Create + ASSERT_EQ(engine->SortedCreate(collection_name), create_ret); + // Put + ASSERT_EQ(engine->SortedPut(collection_name, elem_key, new_val), op_ret); + + // Get + ASSERT_EQ(engine->SortedGet(collection_name, elem_key, &got_val), op_ret); + + if (op_ret == Status::Ok) { + ASSERT_EQ(got_val, new_val); + } + // Delete elem + ASSERT_EQ(engine->SortedDelete(collection_name, elem_key), op_ret); + } + + // Test unordered + for (const std::string& collection_name : + {sorted_collection, hash_collection, list, str}) { + Status create_ret = collection_name == hash_collection ? Status::Existed + : Status::WrongType; + Status op_ret = + collection_name == hash_collection ? Status::Ok : Status::WrongType; + std::string new_val("new_unordered_val"); + // Create + ASSERT_EQ(engine->HashCreate(collection_name), create_ret); + // Put + ASSERT_EQ(engine->HashPut(collection_name, elem_key, new_val), op_ret); + // Get + ASSERT_EQ(engine->HashGet(collection_name, elem_key, &got_val), op_ret); + if (op_ret == Status::Ok) { + ASSERT_EQ(got_val, new_val); + } + // Delete + ASSERT_EQ(engine->HashDelete(collection_name, elem_key), op_ret); + } + + // Test list + for (const std::string& collection_name : + {sorted_collection, hash_collection, list, str}) { + Status create_ret = + collection_name == list ? Status::Existed : Status::WrongType; + Status op_ret = collection_name == list ? Status::Ok : Status ::WrongType; + std::string new_val_back("new_back_val"); + std::string new_val_front("new_front_val"); + size_t length; + std::string got_val_back; + std::string got_val_front; + + // Create + ASSERT_EQ(engine->ListCreate(collection_name), create_ret); + // Push + ASSERT_EQ(engine->ListPushBack(collection_name, new_val_back), op_ret); + ASSERT_EQ(engine->ListPushFront(collection_name, new_val_front), op_ret); + // Pop + ASSERT_EQ(engine->ListPopBack(collection_name, &got_val_back), op_ret); + ASSERT_EQ(engine->ListPopFront(collection_name, &got_val_front), op_ret); + // Length + ASSERT_EQ(engine->ListSize(collection_name, &length), op_ret); + + if (op_ret == Status::Ok) { + ASSERT_EQ(got_val_back, new_val_back); + ASSERT_EQ(got_val_front, new_val_front); + ASSERT_EQ(length, 1); + } + } + delete engine; +} + +TEST_F(EngineBasicTest, TypeOfKey) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + std::unordered_map key_types; + for (auto type : {ValueType::String, ValueType::HashCollection, + ValueType::List, ValueType::SortedCollection}) { + std::string key = KVDKValueTypeString[type]; + key_types[key] = type; + ValueType type_resp; + switch (type) { + case ValueType::String: { + ASSERT_EQ(engine->Put(key, ""), Status::Ok); + break; + } + case ValueType::HashCollection: { + ASSERT_EQ(engine->HashCreate(key), Status::Ok); + break; + } + case ValueType::List: { + ASSERT_EQ(engine->ListCreate(key), Status::Ok); + break; + } + case ValueType::SortedCollection: { + ASSERT_EQ(engine->SortedCreate(key), Status::Ok); + break; + } + } + ASSERT_EQ(engine->TypeOf(key, &type_resp), Status::Ok); + ASSERT_EQ(type_resp, type); + ASSERT_EQ(engine->TypeOf("non-exist", &type_resp), Status::NotFound); + } + delete engine; +} + +// Test iterator/backup/checkpoint on a snapshot +TEST_F(EngineBasicTest, TestBasicSnapshot) { + uint32_t num_threads = 16; + int count = 100; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + std::string sorted_collection("sorted_collection"); + std::string hash_collection("hash_collection"); + std::string list("list"); + std::string sorted_collection_after_snapshot( + "sorted_collection_after_snapshot"); + std::string hash_collection_after_snapshot("hash_collection_after_snapshot"); + std::string list_after_snapshot("list_after_snapshot"); + ASSERT_EQ(engine->SortedCreate(sorted_collection), Status::Ok); + ASSERT_EQ(engine->HashCreate(hash_collection), Status::Ok); + ASSERT_EQ(engine->ListCreate(list), Status::Ok); + + bool snapshot_done(false); + std::atomic_uint64_t set_finished_threads(0); + SpinMutex spin; + std::condition_variable_any cv; + + // Insert kv, then update/delete them and insert new kv after snapshot + auto WriteThread = [&](uint32_t id) { + int cnt = count; + // Insert + while (cnt--) { + // Insert + std::string key1(std::string(id + 1, 'a') + std::to_string(cnt)); + std::string key2(std::string(id + 1, 'b') + std::to_string(cnt)); + WriteOptions write_options; + ASSERT_EQ(engine->Put(key1, key1, write_options), Status::Ok); + ASSERT_EQ(engine->Put(key2, key2, write_options), Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection, key1, key1), Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection, key2, key2), Status::Ok); + ASSERT_EQ(engine->HashPut(hash_collection, key1, key1), Status::Ok); + ASSERT_EQ(engine->HashPut(hash_collection, key2, key2), Status::Ok); + ASSERT_EQ(engine->ListPushBack(list, key1), Status::Ok); + ASSERT_EQ(engine->ListPushBack(list, key2), Status::Ok); + } + // Wait snapshot done + set_finished_threads.fetch_add(1); + { + std::unique_lock ul(spin); + while (!snapshot_done) { + cv.wait(ul); + } + } + + cnt = count; + while (cnt--) { + // Update, Delete, and Insert new + std::string key1(std::string(id + 1, 'a') + std::to_string(cnt)); + std::string key2(std::string(id + 1, 'b') + std::to_string(cnt)); + std::string key3(std::string(id + 1, 'c') + std::to_string(cnt)); + ASSERT_EQ(engine->Put(key1, "updated " + key1), Status::Ok); + ASSERT_EQ(engine->Delete(key1), Status::Ok); + ASSERT_EQ(engine->Put(key3, key3), Status::Ok); + + ASSERT_EQ(engine->SortedPut(sorted_collection, key1, "updated " + key1), + Status::Ok); + ASSERT_EQ(engine->SortedDelete(sorted_collection, key2), Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection, key3, key3), Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection_after_snapshot, key1, key1), + Ok); + + ASSERT_EQ(engine->HashPut(hash_collection, key1, "updated " + key1), + Status::Ok); + ASSERT_EQ(engine->HashDelete(hash_collection, key2), Status::Ok); + ASSERT_EQ(engine->HashPut(hash_collection, key3, key3), Status::Ok); + ASSERT_EQ(engine->HashPut(hash_collection_after_snapshot, key1, key1), + Ok); + + std::string elem; + ASSERT_EQ(engine->ListPopBack(list, &elem), Status::Ok); + ASSERT_EQ(engine->ListPopBack(list, &elem), Status::Ok); + ASSERT_EQ(engine->ListPushBack(list, key3), Status::Ok); + ASSERT_EQ(engine->ListPushBack(list_after_snapshot, key1), Ok); + } + }; + + std::vector ths; + for (size_t i = 0; i < num_threads; i++) { + ths.emplace_back(std::thread(WriteThread, i)); + } + // wait until all threads insert done + while (set_finished_threads.load() != num_threads) { + asm volatile("pause"); + } + Snapshot* snapshot = engine->GetSnapshot(true); + // Insert a new collection after snapshot + ASSERT_EQ(engine->SortedCreate(sorted_collection_after_snapshot), Status::Ok); + ASSERT_EQ(engine->HashCreate(hash_collection_after_snapshot), Status::Ok); + ASSERT_EQ(engine->ListCreate(list_after_snapshot), Status::Ok); + { + std::lock_guard ul(spin); + snapshot_done = true; + cv.notify_all(); + } + engine->Backup(backup_log, snapshot); + for (auto& t : ths) { + t.join(); + } + + { // sorted snapshot iterator + SortedIterator* sorted_snapshot_iter = + engine->SortedIteratorCreate(sorted_collection, snapshot); + // Destroyed collection still should be accessable by snapshot_iter + engine->SortedDestroy(sorted_collection); + + uint64_t snapshot_iter_cnt = 0; + sorted_snapshot_iter->SeekToFirst(); + while (sorted_snapshot_iter->Valid()) { + ASSERT_TRUE(sorted_snapshot_iter->Valid()); + snapshot_iter_cnt++; + ASSERT_EQ(sorted_snapshot_iter->Key(), sorted_snapshot_iter->Value()); + sorted_snapshot_iter->Next(); + } + ASSERT_EQ(snapshot_iter_cnt, num_threads * count * 2); + engine->SortedIteratorRelease(sorted_snapshot_iter); + + sorted_snapshot_iter = engine->SortedIteratorCreate( + sorted_collection_after_snapshot, snapshot); + sorted_snapshot_iter->SeekToFirst(); + ASSERT_FALSE(sorted_snapshot_iter->Valid()); + engine->SortedIteratorRelease(sorted_snapshot_iter); + } + + { // Hash snapshot iterator + auto hash_snapshot_iter = + engine->HashIteratorCreate(hash_collection, snapshot); + engine->HashDestroy(hash_collection); + + uint64_t snapshot_iter_cnt = 0; + hash_snapshot_iter->SeekToFirst(); + while (hash_snapshot_iter->Valid()) { + ASSERT_TRUE(hash_snapshot_iter->Valid()); + snapshot_iter_cnt++; + ASSERT_EQ(hash_snapshot_iter->Key(), hash_snapshot_iter->Value()); + hash_snapshot_iter->Next(); + } + ASSERT_EQ(snapshot_iter_cnt, num_threads * count * 2); + engine->HashIteratorRelease(hash_snapshot_iter); + + hash_snapshot_iter = + engine->HashIteratorCreate(hash_collection_after_snapshot, snapshot); + hash_snapshot_iter->SeekToFirst(); + ASSERT_FALSE(hash_snapshot_iter->Valid()); + engine->HashIteratorRelease(hash_snapshot_iter); + } + + { // List snapshot iterator + auto list_snapshot_iter = engine->ListIteratorCreate(list, snapshot); + engine->ListDestroy(list); + + uint64_t snapshot_iter_cnt = 0; + list_snapshot_iter->SeekToFirst(); + while (list_snapshot_iter->Valid()) { + ASSERT_TRUE(list_snapshot_iter->Valid()); + snapshot_iter_cnt++; + ASSERT_TRUE(list_snapshot_iter->Value()[0] == 'a' || + list_snapshot_iter->Value()[0] == 'b'); + list_snapshot_iter->Next(); + } + ASSERT_EQ(snapshot_iter_cnt, num_threads * count * 2); + engine->ListIteratorRelease(list_snapshot_iter); + + list_snapshot_iter = + engine->ListIteratorCreate(list_after_snapshot, snapshot); + list_snapshot_iter->SeekToFirst(); + ASSERT_FALSE(list_snapshot_iter->Valid()); + engine->ListIteratorRelease(list_snapshot_iter); + } + + delete engine; + + auto Validation = [&]() { + // Test backup and checkpoint instance + // All changes after snapshot should not be seen in backup and checkpoint + // Writes on backup should work well + size_t size; + ASSERT_EQ(engine->SortedSize(sorted_collection, &size), Status::Ok); + ASSERT_EQ(size, num_threads * count * 2); + ASSERT_EQ(engine->HashSize(hash_collection, &size), Status::Ok); + ASSERT_EQ(size, num_threads * count * 2); + ASSERT_EQ(engine->ListSize(list, &size), Status::Ok); + ASSERT_EQ(size, num_threads * count * 2); + for (uint32_t id = 0; id < num_threads; id++) { + int cnt = count; + std::string got_v1, got_v2, got_v3; + while (cnt--) { + std::string key1(std::string(id + 1, 'a') + std::to_string(cnt)); + std::string key2(std::string(id + 1, 'b') + std::to_string(cnt)); + std::string key3(std::string(id + 1, 'c') + std::to_string(cnt)); + // string + ASSERT_EQ(engine->Get(key1, &got_v1), Status::Ok); + ASSERT_EQ(engine->Get(key2, &got_v2), Status::Ok); + ASSERT_EQ(engine->Get(key3, &got_v3), Status::NotFound); + ASSERT_EQ(got_v1, key1); + ASSERT_EQ(got_v2, key2); + // sorted + ASSERT_EQ(engine->SortedGet(sorted_collection, key1, &got_v1), + Status::Ok); + ASSERT_EQ(engine->SortedGet(sorted_collection, key2, &got_v2), + Status::Ok); + ASSERT_EQ(engine->SortedGet(sorted_collection, key3, &got_v3), + Status::NotFound); + ASSERT_EQ(got_v1, key1); + ASSERT_EQ(got_v2, key2); + ASSERT_EQ( + engine->SortedGet(sorted_collection_after_snapshot, key1, &got_v1), + Status::NotFound); + // hash + ASSERT_EQ(engine->HashGet(hash_collection, key1, &got_v1), Status::Ok); + ASSERT_EQ(engine->HashGet(hash_collection, key2, &got_v2), Status::Ok); + ASSERT_EQ(engine->HashGet(hash_collection, key3, &got_v3), + Status::NotFound); + ASSERT_EQ(got_v1, key1); + ASSERT_EQ(got_v2, key2); + ASSERT_EQ( + engine->HashGet(hash_collection_after_snapshot, key1, &got_v1), + Status::NotFound); + } + } + + { // sorted iterator + uint64_t sorted_iter_cnt = 0; + auto sorted_iter = engine->SortedIteratorCreate(sorted_collection); + ASSERT_TRUE(sorted_iter != nullptr); + sorted_iter->SeekToFirst(); + while (sorted_iter->Valid()) { + ASSERT_TRUE(sorted_iter->Valid()); + sorted_iter_cnt++; + ASSERT_EQ(sorted_iter->Key(), sorted_iter->Value()); + sorted_iter->Next(); + } + ASSERT_EQ(sorted_iter_cnt, num_threads * count * 2); + engine->SortedIteratorRelease(sorted_iter); + ASSERT_EQ(engine->SortedIteratorCreate(sorted_collection_after_snapshot), + nullptr); + } + + { // hash iterator + uint64_t hash_iter_cnt = 0; + auto hash_iter = engine->HashIteratorCreate(hash_collection); + ASSERT_TRUE(hash_iter != nullptr); + hash_iter->SeekToFirst(); + while (hash_iter->Valid()) { + ASSERT_TRUE(hash_iter->Valid()); + hash_iter_cnt++; + ASSERT_EQ(hash_iter->Key(), hash_iter->Value()); + hash_iter->Next(); + } + engine->HashIteratorRelease(hash_iter); + ASSERT_EQ(hash_iter_cnt, num_threads * count * 2); + ASSERT_EQ(engine->HashIteratorCreate(hash_collection_after_snapshot), + nullptr); + } + + { // list iterator + uint64_t list_iter_cnt = 0; + auto list_iter = engine->ListIteratorCreate(list); + ASSERT_TRUE(list_iter != nullptr); + list_iter->SeekToFirst(); + while (list_iter->Valid()) { + ASSERT_TRUE(list_iter->Valid()); + list_iter_cnt++; + ASSERT_TRUE(list_iter->Value()[0] == 'a' || + list_iter->Value()[0] == 'b'); + list_iter->Next(); + } + engine->ListIteratorRelease(list_iter); + ASSERT_EQ(list_iter_cnt, num_threads * count * 2); + ASSERT_EQ(engine->ListIteratorCreate(hash_collection_after_snapshot), + nullptr); + } + }; + + std::vector opt_restore_skiplists{0, 1}; + for (auto is_opt : opt_restore_skiplists) { + configs.opt_large_sorted_collection_recovery = is_opt; + + ASSERT_EQ( + Engine::Restore(backup_path, backup_log, &engine, configs, stdout), + Status::Ok); + Validation(); + delete engine; + } +} + +TEST_F(EngineBasicTest, TestBasicStringOperations) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + do { + TestString(configs.max_access_threads); + } while (ChangeConfig()); + delete engine; +} + +TEST_F(EngineBasicTest, TestStringModify) { + struct IncNArgs { + size_t incr_by; + size_t result; + }; + auto IncN = [](const std::string* old_val, std::string* new_value, + void* modify_args) { + assert(modify_args); + IncNArgs* args = static_cast(modify_args); + size_t old_num; + if (old_val == nullptr) { + // if key not exist, start from 0 + old_num = 0; + } else { + if (old_val->size() != sizeof(size_t)) { + return ModifyOperation::Abort; + } else { + memcpy(&old_num, old_val->data(), sizeof(size_t)); + } + } + args->result = old_num + args->incr_by; + + new_value->assign((char*)&args->result, sizeof(size_t)); + return ModifyOperation::Write; + }; + + int num_threads = 16; + int ops_per_thread = 1000; + uint64_t incr_by = 5; + + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + std::string incr_key = "plus"; + + std::string wrong_value_key = "wrong_value"; + ASSERT_EQ(engine->Put(wrong_value_key, std::string(10, 'a')), Status::Ok); + + auto TestModify = [&](int) { + IncNArgs args{5, 0}; + ASSERT_EQ(engine->Modify(wrong_value_key, IncN, &args), Status::Abort); + for (int i = 0; i < ops_per_thread; i++) { + size_t prev_num = args.result; + ASSERT_EQ(engine->Modify(incr_key, IncN, &args), Status::Ok); + ASSERT_TRUE(args.result > prev_num); + } + }; + + LaunchNThreads(num_threads, TestModify); + std::string val; + size_t val_num; + ASSERT_EQ(engine->Get(incr_key, &val), Status::Ok); + ASSERT_EQ(val.size(), sizeof(size_t)); + memcpy(&val_num, val.data(), sizeof(size_t)); + ASSERT_EQ(val_num, ops_per_thread * num_threads * incr_by); + delete engine; +} + +TEST_F(BatchWriteTest, Sorted) { + size_t num_threads = 1; + for (int index_with_hashtable : {0, 1}) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + size_t batch_size = 100; + size_t count = 100 * batch_size; + + std::string collection_name{"sorted" + + std::to_string(index_with_hashtable)}; + SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = index_with_hashtable; + ASSERT_EQ(engine->SortedCreate(collection_name), Status::Ok); + + std::vector> elems(num_threads); + std::vector> values(num_threads); + for (size_t tid = 0; tid < num_threads; tid++) { + for (size_t i = 0; i < count; i++) { + elems[tid].push_back(std::to_string(tid) + "_" + std::to_string(i)); + values[tid].emplace_back(); + } + } + + auto Put = [&](size_t tid) { + for (size_t i = 0; i < count; i++) { + values[tid][i] = GetRandomString(120); + ASSERT_EQ( + engine->SortedPut(collection_name, elems[tid][i], values[tid][i]), + Status::Ok); + } + }; + + auto BatchWrite = [&](size_t tid) { + auto batch = engine->WriteBatchCreate(); + for (size_t i = 0; i < count; i++) { + if (i % 2 == 0) { + values[tid][i] = GetRandomString(120); + batch->SortedPut(collection_name, elems[tid][i], values[tid][i]); + } else { + values[tid][i].clear(); + batch->SortedDelete(collection_name, elems[tid][i]); + } + if ((i + 1) % batch_size == 0) { + // Delete a non-existing elem + batch->SortedDelete(collection_name, "non-existing"); + ASSERT_EQ(engine->BatchWrite(batch), Status::Ok); + batch->Clear(); + } + } + }; + + auto Check = [&](size_t tid) { + for (size_t i = 0; i < count; i++) { + std::string val_resp; + if (values[tid][i].empty()) { + ASSERT_EQ( + engine->SortedGet(collection_name, elems[tid][i], &val_resp), + Status::NotFound); + } else { + ASSERT_EQ( + engine->SortedGet(collection_name, elems[tid][i], &val_resp), + Status::Ok); + ASSERT_EQ(values[tid][i], val_resp); + } + } + }; + + LaunchNThreads(num_threads, BatchWrite); + LaunchNThreads(num_threads, Check); + + Reboot(); + LaunchNThreads(num_threads, Check); + LaunchNThreads(num_threads, Put); + LaunchNThreads(num_threads, Check); + LaunchNThreads(num_threads, BatchWrite); + LaunchNThreads(num_threads, Check); + + delete engine; + } +} + +TEST_F(BatchWriteTest, String) { + size_t num_threads = 16; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + size_t batch_size = 100; + size_t count = 100 * batch_size; + std::vector> keys(num_threads); + std::vector> values(num_threads); + for (size_t tid = 0; tid < num_threads; tid++) { + for (size_t i = 0; i < count; i++) { + keys[tid].push_back(std::to_string(tid) + "_" + std::to_string(i)); + values[tid].emplace_back(); + } + } + + auto Put = [&](size_t tid) { + for (size_t i = 0; i < count; i++) { + values[tid][i] = GetRandomString(120); + ASSERT_EQ(engine->Put(keys[tid][i], values[tid][i]), Status::Ok); + } + }; + + auto BatchWrite = [&](size_t tid) { + auto batch = engine->WriteBatchCreate(); + for (size_t i = 0; i < count; i++) { + if (i % 2 == 0) { + values[tid][i] = GetRandomString(120); + // The first Put is overwritten by the second Put. + batch->StringPut(keys[tid][i], GetRandomString(120)); + batch->StringPut(keys[tid][i], values[tid][i]); + } else { + values[tid][i].clear(); + batch->StringDelete(keys[tid][i]); + batch->StringDelete(keys[tid][i]); + } + if ((i + 1) % batch_size == 0) { + // Delete a non-existing key + batch->StringDelete("non-existing"); + ASSERT_EQ(batch->Size(), batch_size + 1); + ASSERT_EQ(engine->BatchWrite(batch), Status::Ok); + batch->Clear(); + } + } + }; + + auto Check = [&](size_t tid) { + for (size_t i = 0; i < count; i++) { + std::string val_resp; + if (values[tid][i].empty()) { + ASSERT_EQ(engine->Get(keys[tid][i], &val_resp), Status::NotFound); + } else { + ASSERT_EQ(engine->Get(keys[tid][i], &val_resp), Status::Ok); + ASSERT_EQ(values[tid][i], val_resp); + } + } + }; + + LaunchNThreads(num_threads, BatchWrite); + LaunchNThreads(num_threads, Check); + + Reboot(); + LaunchNThreads(num_threads, Check); + LaunchNThreads(num_threads, Put); + LaunchNThreads(num_threads, Check); + LaunchNThreads(num_threads, BatchWrite); + LaunchNThreads(num_threads, Check); + + delete engine; +} + +TEST_F(BatchWriteTest, Hash) { + size_t num_threads = 16; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + size_t batch_size = 100; + size_t count = 100 * batch_size; + + std::string key{"hash"}; + ASSERT_EQ(engine->HashCreate(key), Status::Ok); + + std::vector> fields(num_threads); + std::vector> values(num_threads); + for (size_t tid = 0; tid < num_threads; tid++) { + for (size_t i = 0; i < count; i++) { + fields[tid].push_back(std::to_string(tid) + "_" + std::to_string(i)); + values[tid].emplace_back(); + } + } + + auto Put = [&](size_t tid) { + for (size_t i = 0; i < count; i++) { + values[tid][i] = GetRandomString(120); + ASSERT_EQ(engine->HashPut(key, fields[tid][i], values[tid][i]), + Status::Ok); + } + }; + + auto BatchWrite = [&](size_t tid) { + auto batch = engine->WriteBatchCreate(); + for (size_t i = 0; i < count; i++) { + if (i % 2 == 0) { + values[tid][i] = GetRandomString(120); + batch->HashPut(key, fields[tid][i], values[tid][i]); + } else { + values[tid][i].clear(); + batch->HashDelete(key, fields[tid][i]); + } + if ((i + 1) % batch_size == 0) { + // Delete a non-existing key + batch->HashDelete(key, "non-existing"); + ASSERT_EQ(engine->BatchWrite(batch), Status::Ok); + batch->Clear(); + } + } + }; + + auto Check = [&](size_t tid) { + for (size_t i = 0; i < count; i++) { + std::string val_resp; + if (values[tid][i].empty()) { + ASSERT_EQ(engine->HashGet(key, fields[tid][i], &val_resp), + Status::NotFound); + } else { + ASSERT_EQ(engine->HashGet(key, fields[tid][i], &val_resp), Status::Ok); + ASSERT_EQ(values[tid][i], val_resp); + } + } + }; + + LaunchNThreads(num_threads, BatchWrite); + LaunchNThreads(num_threads, Check); + + Reboot(); + + LaunchNThreads(num_threads, Check); + LaunchNThreads(num_threads, Put); + LaunchNThreads(num_threads, Check); + LaunchNThreads(num_threads, BatchWrite); + LaunchNThreads(num_threads, Check); + + delete engine; +} + +TEST_F(EngineBasicTest, TestLocalSortedCollection) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + do { + for (int index_with_hashtable : {0, 1}) { + SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = index_with_hashtable; + TestLocalSortedCollection(engine, + "hash_index" + + std::to_string(index_with_hashtable) + + "thread_skiplist", + s_configs); + TestSortedIterator("hash_index" + std::to_string(index_with_hashtable) + + "thread_skiplist", + true); + } + } while (ChangeConfig()); + + delete engine; +} + +TEST_F(EngineBasicTest, TestGlobalSortedCollection) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + do { + for (int index_with_hashtable : {0, 1}) { + SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = index_with_hashtable; + std::string collection = + std::to_string(index_with_hashtable) + "global_skiplist"; + TestGlobalSortedCollection(collection, s_configs); + TestSortedIterator(collection, false); + } + } while (ChangeConfig()); + delete engine; +} + +TEST_F(EngineBasicTest, TestSeek) { + std::string val; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + // Test Seek + std::string collection = "col1"; + ASSERT_EQ(engine->SortedCreate(collection), Status::Ok); + uint64_t z = 0; + auto zero_filled_str = uint64_to_string(z); + ASSERT_EQ(engine->SortedPut(collection, zero_filled_str, zero_filled_str), + Status::Ok); + ASSERT_EQ(engine->SortedGet(collection, zero_filled_str, &val), Status::Ok); + auto iter = engine->SortedIteratorCreate(collection); + ASSERT_NE(iter, nullptr); + iter->Seek(zero_filled_str); + ASSERT_TRUE(iter->Valid()); + + // Test SeekToFirst + collection.assign("col2"); + ASSERT_EQ(engine->SortedCreate(collection), Status::Ok); + ASSERT_EQ(engine->SortedPut(collection, "foo", "bar"), Status::Ok); + ASSERT_EQ(engine->SortedGet(collection, "foo", &val), Status::Ok); + ASSERT_EQ(engine->SortedDelete(collection, "foo"), Status::Ok); + ASSERT_EQ(engine->SortedGet(collection, "foo", &val), Status::NotFound); + ASSERT_EQ(engine->SortedPut(collection, "foo2", "bar2"), Status::Ok); + engine->SortedIteratorRelease(iter); + iter = engine->SortedIteratorCreate(collection); + ASSERT_NE(iter, nullptr); + iter->SeekToFirst(); + ASSERT_TRUE(iter->Valid()); + ASSERT_EQ(iter->Value(), "bar2"); + engine->SortedIteratorRelease(iter); + delete engine; +} + +TEST_F(EngineBasicTest, TestStringLargeValue) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + for (size_t sz = 1024; sz < (1UL << 30); sz *= 2) { + std::string key{"large"}; + std::string value(sz, 'a'); + std::string sink; + + ASSERT_EQ(engine->Put(key, value), Status::Ok); + ASSERT_EQ(engine->Get(key, &sink), Status::Ok); + ASSERT_EQ(value, sink); + } + delete engine; +} + +TEST_F(EngineBasicTest, TestList) { + size_t num_threads = 1; + size_t count = 1000; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + std::vector> elems_vec(num_threads); + std::vector list_vec(num_threads); + for (size_t i = 0; i < num_threads; i++) { + list_vec[i] = "List_" + std::to_string(i); + ASSERT_EQ(engine->ListCreate(list_vec[i]), Status::Ok); + ASSERT_EQ(engine->ListDestroy(list_vec[i]), Status::Ok); + ASSERT_EQ(engine->ListCreate(list_vec[i]), Status::Ok); + for (size_t j = 0; j < count; j++) { + elems_vec[i].push_back(std::to_string(i) + "_" + std::to_string(j)); + } + } + std::vector> list_copy_vec(num_threads); + + auto LPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t sz; + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(engine->ListPushFront(key, elems[j]), Status::Ok); + list_copy.push_front(elems[j]); + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + } + }; + + auto RPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t sz; + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(engine->ListPushBack(key, elems[j]), Status::Ok); + list_copy_vec[tid].push_back(elems[j]); + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + } + }; + + auto LPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + std::string value_got; + size_t sz; + for (size_t j = 0; j < count; j++) { + if (list_copy.empty()) { + ASSERT_EQ(engine->ListPopFront(key, &value_got), Status::NotFound); + break; + } + ASSERT_EQ(engine->ListPopFront(key, &value_got), Status::Ok); + ASSERT_EQ(list_copy.front(), value_got); + list_copy.pop_front(); + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + } + }; + + auto RPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + std::string value_got; + size_t sz; + for (size_t j = 0; j < count; j++) { + if (list_copy.empty()) { + ASSERT_EQ(engine->ListPopFront(key, &value_got), Status::NotFound); + break; + } + ASSERT_EQ(engine->ListPopBack(key, &value_got), Status::Ok); + ASSERT_EQ(list_copy.back(), value_got); + list_copy.pop_back(); + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + } + }; + + auto LBatchPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + for (size_t j = 0; j < count; j++) { + list_copy.push_front(elems[j]); + } + ASSERT_EQ(engine->ListBatchPushFront(key, elems), Status::Ok); + size_t sz; + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto RBatchPush = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto const& elems = elems_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + for (size_t j = 0; j < count; j++) { + list_copy.push_back(elems[j]); + } + ASSERT_EQ(engine->ListBatchPushBack(key, elems), Status::Ok); + size_t sz; + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto LBatchPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + std::vector elems_resp; + ASSERT_EQ(engine->ListBatchPopFront(key, count, &elems_resp), Status::Ok); + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(list_copy.front(), elems_resp[j]); + list_copy.pop_front(); + } + size_t sz; + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto RBatchPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + std::vector elems_resp; + ASSERT_EQ(engine->ListBatchPopBack(key, count, &elems_resp), Status::Ok); + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(list_copy.back(), elems_resp[j]); + list_copy.pop_back(); + } + size_t sz; + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto RPushLPop = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + + auto elem_copy = list_copy.front(); + list_copy.push_back(elem_copy); + list_copy.pop_front(); + + std::string elem; + ASSERT_EQ(engine->ListMove(key, ListPos::Front, key, ListPos::Back, &elem), + Status::Ok); + ASSERT_EQ(elem, elem_copy); + + size_t sz; + ASSERT_EQ(engine->ListSize(key, &sz), Status::Ok); + ASSERT_EQ(sz, list_copy.size()); + }; + + auto ListIterate = [&](size_t tid) { + auto const& key = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + + auto iter = engine->ListIteratorCreate(key); + ASSERT_TRUE((list_copy.empty() && iter == nullptr) || (iter != nullptr)); + if (iter != nullptr) { + iter->Seek(0); + for (auto iter2 = list_copy.begin(); iter2 != list_copy.end(); iter2++) { + ASSERT_TRUE(iter->Valid()); + ASSERT_EQ(iter->Value(), *iter2); + iter->Next(); + } + + iter->Seek(-1); + for (auto iter2 = list_copy.rbegin(); iter2 != list_copy.rend(); + iter2++) { + ASSERT_TRUE(iter->Valid()); + ASSERT_EQ(iter->Value(), *iter2); + iter->Prev(); + } + engine->ListIteratorRelease(iter); + } + }; + + auto ListInsertPutRemove = [&](size_t tid) { + auto const& list_name = list_vec[tid]; + auto& list_copy = list_copy_vec[tid]; + size_t len; + size_t const insert_pos = 5; + std::string elem; + + ASSERT_EQ(engine->ListSize(list_name, &len), Status::Ok); + ASSERT_GT(len, insert_pos); + + auto iter = engine->ListIteratorCreate(list_name); + ASSERT_NE(iter, nullptr); + + iter->Seek(insert_pos); + auto iter2 = std::next(list_copy.begin(), insert_pos); + ASSERT_EQ(iter->Value(), *iter2); + + elem = *iter2 + "_before"; + ASSERT_EQ(engine->ListInsertBefore(list_name, elem, iter->Value()), + Status::Ok); + iter2 = list_copy.insert(iter2, elem); + engine->ListIteratorRelease(iter); + iter = engine->ListIteratorCreate(list_name); + iter->Seek(insert_pos); + ASSERT_EQ(iter->Value(), *iter2); + + auto replace_pos = insert_pos - 2; + iter->Prev(); + iter->Prev(); + --iter2; + --iter2; + ASSERT_EQ(iter->Value(), *iter2); + elem = *iter2 + "_new"; + ASSERT_EQ(engine->ListReplace(list_name, replace_pos, elem), Status::Ok); + *iter2 = elem; + engine->ListIteratorRelease(iter); + iter = engine->ListIteratorCreate(list_name); + iter->Seek(replace_pos); + ASSERT_EQ(iter->Value(), *iter2); + + auto erase_pos = replace_pos - 2; + iter->Prev(); + iter->Prev(); + --iter2; + --iter2; + ASSERT_EQ(iter->Value(), *iter2); + std::string value; + ASSERT_EQ(engine->ListErase(list_name, erase_pos, &value), Status::Ok); + ASSERT_EQ(value, *iter2); + iter2 = list_copy.erase(iter2); + engine->ListIteratorRelease(iter); + iter = engine->ListIteratorCreate(list_name); + iter->Seek(erase_pos); + ASSERT_EQ(iter->Value(), *iter2); + engine->ListIteratorRelease(iter); + }; + + for (size_t i = 0; i < 3; i++) { + LaunchNThreads(num_threads, LPop); + LaunchNThreads(num_threads, RPop); + LaunchNThreads(num_threads, LPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LBatchPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RBatchPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LBatchPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RBatchPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPop); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPush); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, LPush); + LaunchNThreads(num_threads, ListIterate); + for (size_t j = 0; j < 100; j++) { + LaunchNThreads(num_threads, ListInsertPutRemove); + LaunchNThreads(num_threads, ListIterate); + LaunchNThreads(num_threads, RPushLPop); + LaunchNThreads(num_threads, ListIterate); + } + Reboot(); + } + + delete engine; +} + +TEST_F(EngineBasicTest, TestHash) { + size_t num_threads = 1; + size_t count = 1000; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + std::string key{"Hash"}; + ASSERT_EQ(engine->HashCreate(key), Status::Ok); + ASSERT_EQ(engine->HashDestroy(key), Status::Ok); + ASSERT_EQ(engine->HashCreate(key), Status::Ok); + using umap = std::unordered_map; + std::vector local_copies(num_threads); + std::mutex mu; + + auto HPut = [&](size_t tid) { + umap& local_copy = local_copies[tid]; + for (size_t j = 0; j < count; j++) { + std::string field{std::to_string(tid) + "_" + GetRandomString(10)}; + std::string value{GetRandomString(120)}; + ASSERT_EQ(engine->HashPut(key, field, value), Status::Ok); + local_copy[field] = value; + } + }; + + auto HGet = [&](size_t tid) { + umap const& local_copy = local_copies[tid]; + for (auto const& kv : local_copy) { + std::string resp; + ASSERT_EQ(engine->HashGet(key, kv.first, &resp), Status::Ok); + ASSERT_EQ(resp, kv.second) << "Field:\t" << kv.first << "\n"; + } + }; + + auto HDelete = [&](size_t tid) { + umap& local_copy = local_copies[tid]; + std::string sink; + for (size_t i = 0; i < count / 2; i++) { + auto iter = local_copy.begin(); + ASSERT_EQ(engine->HashDelete(key, iter->first), Status::Ok); + ASSERT_EQ(engine->HashGet(key, iter->first, &sink), Status::NotFound); + local_copy.erase(iter); + } + }; + + auto HashSize = [&](size_t) { + size_t len = 0; + ASSERT_EQ(engine->HashSize(key, &len), Status::Ok); + size_t cnt = 0; + for (size_t tid = 0; tid < num_threads; tid++) { + cnt += local_copies[tid].size(); + } + ASSERT_EQ(len, cnt); + }; + + auto HashIterate = [&](size_t tid) { + umap combined; + for (size_t tid = 0; tid < num_threads; tid++) { + umap const& local_copy = local_copies[tid]; + for (auto const& kv : local_copy) { + combined[kv.first] = kv.second; + } + } + + auto iter = engine->HashIteratorCreate(key); + + ASSERT_NE(iter, nullptr); + size_t cnt = 0; + for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { + ++cnt; + ASSERT_EQ(combined[iter->Key()], iter->Value()); + } + ASSERT_EQ(cnt, combined.size()); + + cnt = 0; + for (iter->SeekToLast(); iter->Valid(); iter->Prev()) { + ++cnt; + ASSERT_EQ(combined[iter->Key()], iter->Value()); + } + ASSERT_EQ(cnt, combined.size()); + + std::regex re1{".*"}; + std::regex re2{std::to_string(tid) + "_.*"}; + size_t match_cnt1 = 0; + size_t match_cnt2 = 0; + for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { + match_cnt1 += iter->MatchKey(re1) ? 1 : 0; + match_cnt2 += iter->MatchKey(re2) ? 1 : 0; + } + ASSERT_EQ(match_cnt1, combined.size()); + ASSERT_EQ(match_cnt2, local_copies[tid].size()); + engine->HashIteratorRelease(iter); + }; + + std::string counter{"counter"}; + auto HashModify = [&](size_t) { + struct FetchAddArgs { + size_t old; + size_t n; + }; + auto FetchAdd = [](std::string const* old_val, std::string* new_value, + void* args) { + FetchAddArgs* fa_args = static_cast(args); + if (old_val != nullptr) { + try { + fa_args->old = std::stoul(*old_val); + } catch (std::invalid_argument const&) { + return ModifyOperation::Abort; + } catch (std::out_of_range const&) { + return ModifyOperation::Abort; + } + } else { + fa_args->old = 0; + } + new_value->assign(std::to_string(fa_args->old + fa_args->n)); + return ModifyOperation::Write; + }; + + FetchAddArgs args; + args.n = 1; + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(engine->HashModify(key, counter, FetchAdd, &args), Status::Ok); + } + }; + + for (size_t i = 0; i < 3; i++) { + GlobalLogger.Debug("### round %lu ####\n", i); + Reboot(); + LaunchNThreads(num_threads, HPut); + LaunchNThreads(num_threads, HGet); + LaunchNThreads(num_threads, HDelete); + LaunchNThreads(num_threads, HashIterate); + LaunchNThreads(num_threads, HashSize); + LaunchNThreads(num_threads, HPut); + LaunchNThreads(num_threads, HGet); + LaunchNThreads(num_threads, HDelete); + LaunchNThreads(num_threads, HashIterate); + LaunchNThreads(num_threads, HashSize); + } + LaunchNThreads(num_threads, HashModify); + std::string resp; + ASSERT_EQ(engine->HashGet(key, counter, &resp), Status::Ok); + ASSERT_EQ(resp, std::to_string(num_threads * count)); + + delete engine; +} + +TEST_F(EngineBasicTest, TestStringHotspot) { + size_t n_thread_reading = 16; + size_t n_thread_writing = 16; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + size_t count = 100000; + std::string key{"SuperHotspot"}; + std::string val1(1024, 'a'); + std::string val2(1023, 'b'); + + ASSERT_EQ(engine->Put(key, val1), Status::Ok); + + auto EvenWriteOddRead = [&](uint32_t id) { + for (size_t i = 0; i < count; i++) { + if (id % 2 == 0) { + // Even Write + if (id % 4 == 0) { + ASSERT_EQ(engine->Put(key, val1), Status::Ok); + } else { + ASSERT_EQ(engine->Put(key, val2), Status::Ok); + } + } else { + // Odd Read + std::string got_val; + ASSERT_EQ(engine->Get(key, &got_val), Status::Ok); + bool match = false; + match = match || (got_val == val1); + match = match || (got_val == val2); + if (!match) { + std::string msg; + msg.append("Wrong value!\n"); + msg.append("The value should be 1024 of a's or 1023 of b's.\n"); + msg.append("Actual result is:\n"); + msg.append(got_val); + msg.append("\n"); + msg.append("Length: "); + msg.append(std::to_string(got_val.size())); + msg.append("\n"); + GlobalLogger.Error(msg.data()); + } + ASSERT_TRUE(match); + } + } + }; + + LaunchNThreads(n_thread_reading + n_thread_writing, EvenWriteOddRead); + delete engine; +} + +TEST_F(EngineBasicTest, TestSortedHotspot) { + size_t n_thread_reading = 16; + size_t n_thread_writing = 16; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + size_t count = 100000; + std::string collection_name{"collection"}; + std::vector keys{"SuperHotSpot0", "SuperHotSpot2", + "SuperHotSpot1"}; + std::string val1(1024, 'a'); + std::string val2(1024, 'b'); + ASSERT_EQ(engine->SortedCreate(collection_name), Status::Ok); + + for (const std::string& key : keys) { + ASSERT_EQ(engine->SortedPut(collection_name, key, val1), Status::Ok); + + auto EvenWriteOddRead = [&](uint32_t id) { + for (size_t i = 0; i < count; i++) { + if (id % 2 == 0) { + // Even Write + if (id % 4 == 0) { + ASSERT_EQ(engine->SortedPut(collection_name, key, val1), + Status::Ok); + } else { + ASSERT_EQ(engine->SortedPut(collection_name, key, val2), + Status::Ok); + } + } else { + // Odd Read + std::string got_val; + ASSERT_EQ(engine->SortedGet(collection_name, key, &got_val), + Status::Ok); + bool match = false; + match = match || (got_val == val1); + match = match || (got_val == val2); + if (!match) { + std::string msg; + msg.append("Wrong value!\n"); + msg.append("The value should be 1024 of a's or 1023 of b's.\n"); + msg.append("Actual result is:\n"); + msg.append(got_val); + msg.append("\n"); + msg.append("Length: "); + msg.append(std::to_string(got_val.size())); + msg.append("\n"); + GlobalLogger.Error(msg.data()); + } + ASSERT_TRUE(match); + } + } + }; + + LaunchNThreads(n_thread_reading + n_thread_writing, EvenWriteOddRead); + } + delete engine; +} + +TEST_F(EngineBasicTest, TestSortedCustomCompareFunction) { + using kvpair = std::pair; + size_t num_threads = 16; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + std::vector collections{"collection0", "collection1", + "collection2"}; + + auto cmp0 = [](const StringView& a, const StringView& b) -> int { + double scorea = std::stod(string_view_2_string(a)); + double scoreb = std::stod(string_view_2_string(b)); + if (scorea == scoreb) + return 0; + else if (scorea < scoreb) + return 1; + else + return -1; + }; + + auto cmp1 = [](const StringView& a, const StringView& b) -> int { + double scorea = std::stod(string_view_2_string(a)); + double scoreb = std::stod(string_view_2_string(b)); + if (scorea == scoreb) + return 0; + else if (scorea > scoreb) + return 1; + else + return -1; + }; + + size_t count = 10; + std::vector key_values(count); + std::map dedup_kvs; + std::generate(key_values.begin(), key_values.end(), [&]() { + const char v = rand() % (90 - 65 + 1) + 65; + std::string k = std::to_string(rand() % 100); + dedup_kvs[k] = v; + return std::make_pair(k, std::string(1, v)); + }); + + // register compare function + engine->registerComparator("collection0_cmp", cmp0); + engine->registerComparator("collection1_cmp", cmp1); + for (size_t i = 0; i < collections.size(); ++i) { + Status s; + if (i < 2) { + std::string comp_name = "collection" + std::to_string(i) + "_cmp"; + SortedCollectionConfigs s_configs; + s_configs.comparator_name = comp_name; + s = engine->SortedCreate(collections[i], s_configs); + } else { + s = engine->SortedCreate(collections[i]); + } + ASSERT_EQ(s, Status::Ok); + } + for (size_t i = 0; i < collections.size(); ++i) { + auto Write = [&](size_t) { + for (size_t j = 0; j < count; j++) { + ASSERT_EQ(engine->SortedPut(collections[i], key_values[j].first, + key_values[j].second), + Status::Ok); + } + }; + LaunchNThreads(num_threads, Write); + } + + for (size_t i = 0; i < collections.size(); ++i) { + std::vector expected_res(dedup_kvs.begin(), dedup_kvs.end()); + if (i == 0) { + std::sort(expected_res.begin(), expected_res.end(), + [&](const kvpair& a, const kvpair& b) -> bool { + return cmp0(a.first, b.first) <= 0; + }); + + } else if (i == 1) { + std::sort(expected_res.begin(), expected_res.end(), + [&](const kvpair& a, const kvpair& b) -> bool { + return cmp1(a.first, b.first) <= 0; + }); + } + auto iter = engine->SortedIteratorCreate(collections[i]); + ASSERT_TRUE(iter != nullptr); + iter->SeekToFirst(); + size_t cnt = 0; + while (iter->Valid()) { + std::string key = iter->Key(); + std::string val = iter->Value(); + ASSERT_EQ(key, expected_res[cnt].first); + ASSERT_EQ(val, expected_res[cnt].second); + iter->Next(); + cnt++; + } + engine->SortedIteratorRelease(iter); + } + delete engine; +} + +TEST_F(EngineBasicTest, TestHashTableIterator) { + size_t num_threads = 32; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + std::string collection_name = "sortedcollection"; + engine->SortedCreate(collection_name); + auto MixedPut = [&](size_t id) { + if (id % 2 == 0) { + ASSERT_EQ(engine->Put("stringkey" + std::to_string(id), "stringval"), + Status::Ok); + } else { + ASSERT_EQ( + engine->SortedPut(collection_name, "sortedkey" + std::to_string(id), + "sortedval"), + Status::Ok); + } + }; + LaunchNThreads(num_threads, MixedPut); + + auto test_kvengine = static_cast(engine); + auto hash_table = test_kvengine->GetHashTable(); + size_t total_entry_num = 0; + // Hash Table Iterator + // scan hash table with locked slot. + { + auto hashtable_iter = hash_table->GetIterator(0, hash_table->GetSlotsNum()); + while (hashtable_iter.Valid()) { + auto slot_iter = hashtable_iter.Slot(); + while (slot_iter.Valid()) { + switch (slot_iter->GetIndexType()) { + case PointerType::StringRecord: { + total_entry_num++; + ASSERT_EQ(string_view_2_string( + slot_iter->GetIndex().string_record->Value()), + "stringval"); + break; + } + case PointerType::Skiplist: { + total_entry_num++; + ASSERT_EQ( + string_view_2_string(slot_iter->GetIndex().skiplist->Name()), + collection_name); + break; + } + case PointerType::SkiplistNode: { + total_entry_num++; + ASSERT_EQ(string_view_2_string( + slot_iter->GetIndex().skiplist_node->record->Value()), + "sortedval"); + break; + } + case PointerType::DLRecord: { + total_entry_num++; + ASSERT_EQ( + string_view_2_string(slot_iter->GetIndex().dl_record->Value()), + "sortedval"); + break; + } + default: + ASSERT_EQ((slot_iter->GetIndexType() == PointerType::Invalid) || + (slot_iter->GetIndexType() == PointerType::Empty), + true); + break; + } + slot_iter++; + } + hashtable_iter.Next(); + } + ASSERT_EQ(total_entry_num, num_threads + 1); + } + delete engine; +} + +TEST_F(EngineBasicTest, TestExpireAPI) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + std::string got_val; + int64_t ttl_time; + std::string key = "expired_key"; + std::string val(10, 'a'); + std::string val2(10, 'b'); + std::string list_collection = "ListCollection"; + std::string sorted_collection = "SortedCollection"; + std::string hashes_collection = "HashesCollection"; + int64_t normal_ttl_time = 10000; /* 10s */ + int64_t max_ttl_time = INT64_MAX - 1; + + // For string + { + // key is expired. Check expired time when reading. + WriteOptions write_options1{1, true}; + ASSERT_EQ(engine->Put(key, val, write_options1), Status::Ok); + sleep(1); + ASSERT_EQ(engine->Get(key, &got_val), Status::NotFound); + + // update kv pair with new expired time. + WriteOptions write_options2{INT64_MAX / 1000, true}; + ASSERT_EQ(engine->Put(key, val2, write_options2), Status::Ok); + ASSERT_EQ(engine->Get(key, &got_val), Status::Ok); + ASSERT_EQ(got_val, val2); + + // test update_ttl option + WriteOptions write_options3{1, false}; + ASSERT_EQ(engine->Put(key, val, write_options3), Status::Ok); + sleep(1); + ASSERT_EQ(engine->Get(key, &got_val), Status::Ok); + ASSERT_EQ(got_val, val); + ASSERT_EQ( + engine->Modify( + key, + [=](const std::string* old_value, std::string* new_value, void*) { + if (old_value != nullptr) { + new_value->assign(val2); + return ModifyOperation::Write; + } + return ModifyOperation::Abort; + }, + nullptr, write_options3), + Status::Ok); + sleep(1); + ASSERT_EQ(engine->Get(key, &got_val), Status::Ok); + ASSERT_EQ(got_val, val2); + + // Get expired time. + ASSERT_EQ(engine->GetTTL(key, &ttl_time), Status::Ok); + + // reset expired time for string record. + ASSERT_EQ(engine->Expire(key, normal_ttl_time), Status::Ok); + + // set negative ttl time. + std::string expire_key = "expired_key1"; + std::string expire_val = "expired_val1"; + ASSERT_EQ(engine->Put(expire_key, expire_val, WriteOptions{}), Status::Ok); + ASSERT_EQ(engine->GetTTL(expire_key, &ttl_time), Status::Ok); + ASSERT_EQ(ttl_time, kPersistTime); + ASSERT_EQ(engine->Expire(expire_key, -30), Status::Ok); + ASSERT_EQ(engine->GetTTL(expire_key, &ttl_time), Status::NotFound); + } + + // For sorte collection + { + Status s = engine->SortedCreate(sorted_collection); + ASSERT_EQ(s, Status::Ok); + ASSERT_EQ( + engine->SortedPut(sorted_collection, "sorted" + key, "sorted" + val), + Status::Ok); + // Set expired time for collection + ASSERT_EQ(engine->Expire(sorted_collection, max_ttl_time), + Status::InvalidArgument); + ASSERT_EQ( + engine->SortedPut(sorted_collection, "sorted2" + key, "sorted2" + val), + Status::Ok); + ASSERT_EQ(engine->GetTTL(sorted_collection, &ttl_time), Status::Ok); + // check sorted_collection is persist; + ASSERT_EQ(ttl_time, kPersistTTL); + // reset expired time for collection + ASSERT_EQ(engine->Expire(sorted_collection, 2), Status::Ok); + sleep(2); + ASSERT_EQ(engine->SortedGet(sorted_collection, "sorted" + key, &got_val), + Status::NotFound); + ASSERT_EQ(engine->GetTTL(sorted_collection, &ttl_time), Status::NotFound); + ASSERT_EQ(ttl_time, kInvalidTTL); + + // set negative or 0 ttl time. + ASSERT_EQ(engine->SortedCreate(sorted_collection), Status::Ok); + ASSERT_EQ(engine->GetTTL(sorted_collection, &ttl_time), Status::Ok); + ASSERT_EQ(ttl_time, kPersistTime); + ASSERT_EQ(engine->Expire(sorted_collection, 0), Status::Ok); + ASSERT_EQ(engine->GetTTL(sorted_collection, &ttl_time), Status::NotFound); + ASSERT_EQ(ttl_time, kInvalidTTL); + } + + // For hashes collection + { + ASSERT_EQ(engine->HashCreate(hashes_collection), Status::Ok); + ASSERT_EQ( + engine->HashPut(hashes_collection, "hashes" + key, "hashes" + val), + Status::Ok); + // Set expired time for collection, max_ttl_time is overflow. + ASSERT_EQ(engine->Expire(hashes_collection, max_ttl_time), + Status::InvalidArgument); + ASSERT_EQ( + engine->HashPut(hashes_collection, "hashes2" + key, "hashes2" + val), + Status::Ok); + + // reset expired time for collection + ASSERT_EQ(engine->Expire(hashes_collection, normal_ttl_time), Status::Ok); + ASSERT_EQ(engine->HashGet(hashes_collection, "hashes" + key, &got_val), + Status::Ok); + ASSERT_EQ(got_val, "hashes" + val); + // get collection ttl time + sleep(2); + ASSERT_EQ(engine->GetTTL(hashes_collection, &ttl_time), Status::Ok); + } + + // For list + { + ASSERT_EQ(engine->ListCreate(list_collection), Status::Ok); + ASSERT_EQ(engine->ListPushFront(list_collection, "list" + val), Status::Ok); + // Set expired time for collection + ASSERT_EQ(engine->Expire(list_collection, max_ttl_time), + Status::InvalidArgument); + ASSERT_EQ(engine->GetTTL(list_collection, &ttl_time), Status::Ok); + // check list is persist + ASSERT_EQ(ttl_time, kPersistTime); + // reset expired time for collection + ASSERT_EQ(engine->Expire(list_collection, normal_ttl_time), Status::Ok); + ASSERT_EQ(engine->GetTTL(list_collection, &ttl_time), Status::Ok); + } + + // Close engine and Recovery + Reboot(); + + // Get string record expired time + ASSERT_EQ(engine->GetTTL(key, &ttl_time), Status::Ok); + + // Get sorted record expired time + ASSERT_EQ(engine->GetTTL(sorted_collection, &ttl_time), Status::NotFound); + + // Get hashes record expired time + ASSERT_EQ(engine->GetTTL(hashes_collection, &ttl_time), Status::Ok); + + // Get list record expired time + ASSERT_EQ(engine->GetTTL(list_collection, &ttl_time), Status::Ok); + delete engine; +} + +TEST_F(EngineBasicTest, TestbackgroundDestroyCollections) { + size_t n_thread_writing = 16; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + TTLType ttl = 1000; // 1s + int cnt = 100; + size_t num_thread = n_thread_writing; + + auto list0_push = [&](size_t id) { + std::string list_key0 = "listkey0" + std::to_string(id); + ASSERT_EQ(engine->ListCreate(list_key0), Status::Ok); + for (int i = 0; i < cnt; ++i) { + ASSERT_EQ( + engine->ListPushFront(list_key0, "list_elem" + std::to_string(i)), + Status::Ok); + } + ASSERT_EQ(engine->Expire(list_key0, ttl), Status::Ok); + }; + auto list1_push = [&](size_t id) { + std::string list_key1 = "listkey1" + std::to_string(id); + ASSERT_EQ(engine->ListCreate(list_key1), Status::Ok); + for (int i = 0; i < cnt; ++i) { + ASSERT_EQ( + engine->ListPushFront(list_key1, "list_elem" + std::to_string(i)), + Status::Ok); + } + }; + auto hash0_push = [&](size_t id) { + std::string hash_key0 = "hashkey0" + std::to_string(id); + ASSERT_EQ(engine->HashCreate(hash_key0), Status::Ok); + for (int i = 0; i < cnt; ++i) { + std::string str = std::to_string(i); + ASSERT_EQ( + engine->HashPut(hash_key0, "hash_elem" + str, "hash_value" + str), + Status::Ok); + } + ASSERT_EQ(engine->Expire(hash_key0, ttl), Status::Ok); + }; + + auto sorted0_push = [&](size_t id) { + std::string sorted_key0 = "sortedkey0" + std::to_string(id); + ASSERT_EQ(engine->SortedCreate(sorted_key0), Status::Ok); + for (int i = 0; i < cnt; ++i) { + std::string str = std::to_string(i); + ASSERT_EQ(engine->SortedPut(sorted_key0, "sorted_elem" + str, + "sorted_value" + str), + Status::Ok); + } + ASSERT_EQ(engine->Expire(sorted_key0, ttl), Status::Ok); + }; + + LaunchNThreads(num_thread, list0_push); + LaunchNThreads(num_thread, list1_push); + LaunchNThreads(num_thread, hash0_push); + LaunchNThreads(num_thread, sorted0_push); + + sleep(2); + for (size_t i = 0; i < num_thread; ++i) { + std::string str = std::to_string(i); + TTLType got_ttl; + ASSERT_EQ(engine->GetTTL("hashkey0" + str, &got_ttl), Status::NotFound); + ASSERT_EQ(engine->GetTTL("listkey0" + str, &got_ttl), Status::NotFound); + ASSERT_EQ(engine->GetTTL("sortedkey0" + str, &got_ttl), Status::NotFound); + ASSERT_EQ(engine->GetTTL("listkey1" + str, &got_ttl), Status::Ok); + } + + delete engine; +} + +// ========================= Sync Point ====================================== + +#if KVDK_DEBUG_LEVEL > 0 +// Example: +// {key0, val0} <-> {key2, val2} +// thread1 insert : {key0, val0} <-> {key1, val1} <-> {key2, val2} +// thread2: iter +TEST_F(EngineBasicTest, TestSortedSyncPoint) { + Configs test_config = configs; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, test_config, stdout), + Status::Ok); + std::vector ths; + std::string collection_name = "skiplist"; + ASSERT_EQ(engine->SortedCreate(collection_name), Status::Ok); + + engine->SortedPut(collection_name, "key0", "val0"); + engine->SortedPut(collection_name, "key2", "val2"); + + std::atomic first_record(false); + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->Reset(); + SyncPoint::GetInstance()->LoadDependency( + {{"KVEngine::DLList::LinkDLRecord::HalfLink", "Test::Iter::key0"}}); + SyncPoint::GetInstance()->EnableProcessing(); + + // insert + ths.emplace_back(std::thread([&]() { + engine->SortedPut(collection_name, "key1", "val1"); + std::string got_val; + ASSERT_EQ(engine->SortedGet(collection_name, "key1", &got_val), Status::Ok); + })); + + // Iter + ths.emplace_back(std::thread([&]() { + sleep(1); + auto sorted_iter = engine->SortedIteratorCreate(collection_name); + sorted_iter->SeekToLast(); + if (sorted_iter->Valid()) { + std::string next = sorted_iter->Key(); + ASSERT_EQ(next, "key2"); + sorted_iter->Prev(); + while (sorted_iter->Valid()) { + std::string k = sorted_iter->Key(); + TEST_SYNC_POINT("Test::Iter::" + k); + if (k == "key0") { + sorted_iter->Next(); + ASSERT_EQ(sorted_iter->Key(), "key1"); + sorted_iter->Prev(); + } + sorted_iter->Prev(); + ASSERT_EQ(true, k.compare(next) < 0); + next = k; + } + } + engine->SortedIteratorRelease(sorted_iter); + })); + for (auto& thread : ths) { + thread.join(); + } + delete engine; +} + +TEST_F(EngineBasicTest, TestHashTableRangeIter) { + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + std::string key = "stringkey"; + std::string val = "stringval"; + std::string updated_val = "stringupdatedval"; + + ASSERT_EQ(engine->Put(key, val), Status::Ok); + + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->Reset(); + SyncPoint::GetInstance()->LoadDependency( + {{"ScanHashTable", "KVEngine::stringPutImpl::BeforeLock"}}); + SyncPoint::GetInstance()->EnableProcessing(); + + auto StringUpdate = [&]() { + ASSERT_EQ(engine->Put(key, updated_val), Status::Ok); + }; + + auto HashTableScan = [&]() { + auto test_kvengine = static_cast(engine); + auto hash_table = test_kvengine->GetHashTable(); + auto hashtable_iter = hash_table->GetIterator(0, hash_table->GetSlotsNum()); + while (hashtable_iter.Valid()) { + auto slot_lock = hashtable_iter.AcquireSlotLock(); + auto slot_iter = hashtable_iter.Slot(); + while (slot_iter.Valid()) { + if (slot_iter->GetIndexType() == PointerType::StringRecord) { + TEST_SYNC_POINT("ScanHashTable"); + sleep(2); + ASSERT_EQ(slot_iter->GetIndex().string_record->Key(), key); + ASSERT_EQ(slot_iter->GetIndex().string_record->Value(), val); + } + slot_iter++; + } + hashtable_iter.Next(); + } + }; + + std::vector ts; + ts.emplace_back(std::thread(StringUpdate)); + ts.emplace_back(std::thread(HashTableScan)); + for (auto& t : ts) t.join(); + delete engine; +} + +TEST_F(EngineBasicTest, TestBackGroundCleaner) { + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->Reset(); + // abandon background cleaner thread + SyncPoint::GetInstance()->SetCallBack( + "KVEngine::backgroundCleaner::NothingToDo", [&](void* close_reclaimer) { + *((std::atomic_bool*)close_reclaimer) = true; + return; + }); + SyncPoint::GetInstance()->EnableProcessing(); + + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + int cnt = 100; + + auto PutString = [&]() { + for (int i = 0; i < cnt; ++i) { + std::string key = std::to_string(i) + "stringk"; + std::string val = std::to_string(i) + "stringval"; + ASSERT_EQ(engine->Put(key, val, WriteOptions{INT32_MAX}), Status::Ok); + } + }; + + auto ExpireString = [&]() { + for (int i = 0; i < cnt; ++i) { + // string + std::string key = std::to_string(i) + "stringk"; + std::string got_val; + if (engine->Get(key, &got_val) == Status::Ok) { + ASSERT_EQ(engine->Expire(key, 1), Status::Ok); + } + } + }; + + auto GetString = [&]() { + for (int i = 0; i < cnt; ++i) { + // string + std::string key = std::to_string(i) + "stringk"; + std::string got_val; + int64_t ttl_time; + Status s = engine->GetTTL(key, &ttl_time); + if (s == Status::Ok) { + if (ttl_time > 1) { + ASSERT_EQ(ttl_time <= INT32_MAX, true); + } + } else { + ASSERT_EQ(s, Status::NotFound); + ASSERT_EQ(ttl_time, kInvalidTTL); + } + } + }; + + auto PutSorted = [&]() { + for (int i = 0; i < cnt; ++i) { + for (int index_with_hashtable : {0, 1}) { + std::string sorted_collection = + std::to_string(i) + "sorted" + std::to_string(index_with_hashtable); + std::string key = std::to_string(i) + "sortedk"; + std::string val = std::to_string(i) + "sortedval"; + SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = index_with_hashtable; + ASSERT_EQ(engine->SortedCreate(sorted_collection, s_configs), + Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection, key, val), Status::Ok); + bool set_expire = fast_random_64() % 2 == 0; + if (set_expire) { + ASSERT_EQ(engine->Expire(sorted_collection, 1), Status::Ok); + } + } + } + }; + + auto GetSorted = [&]() { + for (int i = 0; i < cnt; ++i) { + for (int index_with_hashtable : {0, 1}) { + std::string sorted_collection = + std::to_string(i) + "sorted" + std::to_string(index_with_hashtable); + std::string key = std::to_string(i) + "sortedk"; + std::string val = std::to_string(i) + "sortedval"; + std::string got_val; + int64_t ttl_time; + Status s = engine->GetTTL(sorted_collection, &ttl_time); + if (s == Status::Ok) { + if (ttl_time == kPersistTime) { + ASSERT_EQ(engine->SortedGet(sorted_collection, key, &got_val), + Status::Ok); + ASSERT_EQ(got_val, val); + } else { + ASSERT_TRUE(ttl_time <= 1); + } + } else { + ASSERT_EQ(s, Status::NotFound); + ASSERT_EQ(ttl_time, kInvalidTTL); + ASSERT_EQ(engine->SortedGet(sorted_collection, key, &got_val), + Status::NotFound); + } + } + } + }; + + std::string sorted_collection = "sorted_collection"; + std::string list_collection = "list_collection"; + std::string hashlist_collection = "hashlist_collection"; + + auto CreateAndDestroySorted = [&]() { + std::string key = "sorted_key"; + ASSERT_EQ(engine->SortedCreate(sorted_collection), Status::Ok); + for (int i = 0; i < cnt; ++i) { + auto new_key = key + std::to_string(i); + ASSERT_EQ(engine->SortedPut(sorted_collection, new_key, "sorted_value"), + Status::Ok); + ASSERT_EQ( + engine->SortedPut(sorted_collection, new_key, "sorted_update_value"), + Status::Ok); + if (i % 2 != 0) { + ASSERT_EQ(engine->SortedDelete(sorted_collection, new_key), Status::Ok); + } + } + ASSERT_EQ(engine->SortedDestroy(sorted_collection), Status::Ok); + ASSERT_EQ(engine->SortedCreate(sorted_collection), Status::Ok); + }; + + auto CreateAndDestroyList = [&]() { + std::string key = "list_key"; + std::string got_key; + ASSERT_EQ(engine->ListCreate(list_collection), Status::Ok); + for (int i = 0; i < cnt; ++i) { + auto new_key = key + std::to_string(i); + ASSERT_EQ(engine->ListPushFront(list_collection, new_key), Status::Ok); + ASSERT_EQ(engine->ListPopBack(list_collection, &got_key), Status::Ok); + ASSERT_EQ(got_key, new_key); + } + ASSERT_EQ(engine->ListDestroy(list_collection), Status::Ok); + ASSERT_EQ(engine->ListCreate(list_collection), Status::Ok); + }; + + auto CreateAndDestroyHashList = [&]() { + std::string key = "hashlist_key"; + ASSERT_EQ(engine->HashCreate(hashlist_collection), Status::Ok); + for (int i = 0; i < cnt; ++i) { + auto new_key = key + std::to_string(i); + ASSERT_EQ(engine->HashPut(hashlist_collection, new_key, "hashlist_value"), + Status::Ok); + ASSERT_EQ(engine->HashPut(hashlist_collection, new_key, + "hashlist_update_value"), + Status::Ok); + if (i % 2 != 0) { + ASSERT_EQ(engine->HashDelete(hashlist_collection, new_key), Status::Ok); + } + } + ASSERT_EQ(engine->HashDestroy(hashlist_collection), Status::Ok); + ASSERT_EQ(engine->HashCreate(hashlist_collection), Status::Ok); + }; + + auto ExpiredClean = [&]() { + auto test_kvengine = static_cast(engine); + auto cleaner = test_kvengine->EngineCleaner(); + cleaner->Start(); + sleep(2); + cleaner->Close(); + }; + + { + std::vector ts; + ts.emplace_back(std::thread(PutString)); + ts.emplace_back(std::thread(ExpireString)); + ts.emplace_back(std::thread(ExpiredClean)); + + for (auto& t : ts) t.join(); + + // check + GetString(); + } + + { + std::vector ts; + ts.emplace_back(std::thread(PutString)); + ts.emplace_back(std::thread(ExpireString)); + ts.emplace_back(std::thread(ExpiredClean)); + ts.emplace_back(std::thread(GetString)); + for (auto& t : ts) t.join(); + } + + { + PutSorted(); + auto t = std::thread(ExpiredClean); + GetSorted(); + t.join(); + } + + { + CreateAndDestroySorted(); + CreateAndDestroyHashList(); + CreateAndDestroyList(); + ExpiredClean(); + { + size_t size; + ASSERT_EQ(engine->SortedSize(sorted_collection, &size), Status::Ok); + ASSERT_EQ(size, 0); + ASSERT_EQ(engine->HashSize(hashlist_collection, &size), Status::Ok); + ASSERT_EQ(size, 0); + ASSERT_EQ(engine->ListSize(list_collection, &size), Status::Ok); + ASSERT_EQ(size, 0); + } + } + delete engine; +} + +TEST_F(EngineBasicTest, TestBackGroundIterNoHashIndexSkiplist) { + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->Reset(); + // abandon background cleaner thread + SyncPoint::GetInstance()->SetCallBack( + "KVEngine::backgroundCleaner::NothingToDo", [&](void* close_reclaimer) { + *((std::atomic_bool*)close_reclaimer) = true; + return; + }); + SyncPoint::GetInstance()->LoadDependency( + {{"KVEngine::BackgroundCleaner::IterSkiplist::UnlinkDeleteRecord", + "KVEngine::SkiplistNoHashIndex::Put"}}); + SyncPoint::GetInstance()->EnableProcessing(); + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + std::string collection_name = "Skiplist_with_hash_index"; + SortedCollectionConfigs s_configs; + s_configs.index_with_hashtable = false; + ASSERT_EQ(engine->SortedCreate(collection_name, s_configs), Status::Ok); + int cnt = 100; + + // Two case: (1) record->old_record->old_record; + // (2)record->delete_record->old_record + auto PutAndDeleteSorted = [&]() { + for (int i = 0; i < cnt; ++i) { + std::string key = "sorted_key" + std::to_string(i); + std::string value = "sorted_value" + std::to_string(i); + ASSERT_EQ(engine->SortedPut(collection_name, key, value), Status::Ok); + if ((i % 2) == 0) { + ASSERT_EQ(engine->SortedDelete(collection_name, key), Status::Ok); + } else { + ASSERT_EQ(engine->SortedPut(collection_name, key, + "update_value" + std::to_string(i)), + Status::Ok); + } + + TEST_SYNC_POINT("KVEngine::SkiplistNoHashIndex::Put"); + ASSERT_EQ(engine->SortedPut(collection_name, key, + "update_value_again" + std::to_string(i)), + Status::Ok); + } + }; + + auto backgroundCleaner = [&]() { + auto test_kvengine = static_cast(engine); + auto cleaner = test_kvengine->EngineCleaner(); + cleaner->Start(); + sleep(2); + cleaner->Close(); + }; + std::vector ts; + ts.emplace_back(PutAndDeleteSorted); + ts.emplace_back(backgroundCleaner); + for (auto& t : ts) t.join(); + + int entries = 0; + // iterating sorted collection + auto iter = engine->SortedIteratorCreate(collection_name); + ASSERT_TRUE(iter != nullptr); + // forward iterator + iter->SeekToFirst(); + if (iter->Valid()) { + ++entries; + std::string prev = iter->Key(); + iter->Next(); + while (iter->Valid()) { + ++entries; + std::string k = iter->Key(); + iter->Next(); + ASSERT_EQ(true, k.compare(prev) > 0); + prev = k; + } + } + engine->SortedIteratorRelease(iter); + ASSERT_EQ(entries, cnt); + delete engine; +} + +TEST_F(EngineBasicTest, TestDynamicCleaner) { + enum class OpType { insert, update, outdated }; + OpType op; + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->Reset(); + // abandon background cleaner thread + SyncPoint::GetInstance()->SetCallBack( + "KVEngine::backgroundCleaner::NothingToDo", [&](void* close_reclaimer) { + *((std::atomic_bool*)close_reclaimer) = true; + return; + }); + SyncPoint::GetInstance()->SetCallBack("KVEngine::Cleaner::AdjustCleanWorkers", + [&](void* advice_thread_num) { + if (op == OpType::update) { + *((size_t*)advice_thread_num) = 6; + } else if (op == OpType::outdated) { + *((size_t*)advice_thread_num) = 8; + } else { + *((size_t*)advice_thread_num) = 1; + } + return; + }); + SyncPoint::GetInstance()->EnableProcessing(); + configs.hash_bucket_num = 256; + configs.clean_threads = 8; + ASSERT_EQ(Engine::Open(db_path.c_str(), &engine, configs, stdout), + Status::Ok); + + std::string sorted_collection = "sorted_collection"; + std::string common_str_key = "string_key"; + std::string common_sorted_key = "sorted_key"; + std::string common_value = "val"; + ASSERT_EQ(engine->SortedCreate(sorted_collection), Status::Ok); + auto test_kvengine = static_cast(engine); + auto space_cleaner = test_kvengine->EngineCleaner(); + space_cleaner->Start(); + + size_t cnt = 16; + // only insert + op = OpType::insert; + for (size_t id = 0; id < cnt; ++id) { + std::string value = std::to_string(id) + common_value; + std::string str_key = std::to_string(id) + common_str_key; + std::string sorted_key = std::to_string(id) + common_sorted_key; + ASSERT_EQ(engine->Put(str_key, value), Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection, sorted_key, value), + Status::Ok); + } + ASSERT_EQ(space_cleaner->ActiveThreadNum(), 1); + + // update + op = OpType::update; + sleep(1); + for (size_t id = 0; id < cnt; ++id) { + std::string str_key = std::to_string(id) + common_str_key; + std::string sorted_key = std::to_string(id) + common_sorted_key; + std::string value = std::to_string(id) + common_value; + for (int i = 0; i < 100; ++i) { + auto update_val = value + std::to_string(i); + ASSERT_EQ(engine->Put(str_key, update_val), Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection, sorted_key, update_val), + Status::Ok); + } + } + ASSERT_EQ(space_cleaner->ActiveThreadNum(), 6); + op = OpType::outdated; + sleep(1); + for (size_t id = 0; id < cnt; ++id) { + std::string str_key = std::to_string(id) + common_str_key; + std::string sorted_key = std::to_string(id) + common_sorted_key; + ASSERT_EQ(engine->Expire(str_key, -1), Status::Ok); + ASSERT_EQ(engine->SortedDelete(sorted_collection, sorted_key), Status::Ok); + } + ASSERT_EQ(space_cleaner->ActiveThreadNum(), 8); + + op = OpType::insert; + sleep(1); + for (size_t id = 0; id < cnt; ++id) { + std::string value = std::to_string(id) + common_value; + std::string str_key = std::to_string(id) + common_str_key; + std::string sorted_key = std::to_string(id) + common_sorted_key; + ASSERT_EQ(engine->Put(str_key, value), Status::Ok); + ASSERT_EQ(engine->SortedPut(sorted_collection, sorted_key, value), + Status::Ok); + } + ASSERT_EQ(space_cleaner->ActiveThreadNum(), 1); + + delete engine; +} +#endif + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + google::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} From dc4c21475ab8447d9e544788e589ba4738db670d Mon Sep 17 00:00:00 2001 From: "Yu, Peng" Date: Mon, 14 Nov 2022 11:25:54 +0800 Subject: [PATCH 2/5] Adapt benchmarks for volatile KV storage Signed-off-by: Yu, Peng --- volatile/benchmark/bench.cpp | 268 +++++++++++++++++++++++++---------- 1 file changed, 195 insertions(+), 73 deletions(-) diff --git a/volatile/benchmark/bench.cpp b/volatile/benchmark/bench.cpp index 34f55e6e..ee71e21a 100644 --- a/volatile/benchmark/bench.cpp +++ b/volatile/benchmark/bench.cpp @@ -25,10 +25,8 @@ DEFINE_string(path, "/mnt/pmem0/kvdk", "Instance path"); DEFINE_uint64(num_kv, (1 << 30), "Number of KVs to place"); DEFINE_uint64(num_operations, (1 << 30), - "Number of total operations. Asserted to be equal to num_kv if " - "(fill == true)."); - -DEFINE_bool(fill, false, "Fill num_kv uniform kv pairs to a new instance"); + "Number of total operations. " + "num_kv will override this when benchmarking fill/insert"); DEFINE_int64(timeout, 30, "Time to benchmark, this is valid only if fill=false"); @@ -40,15 +38,9 @@ DEFINE_string(value_size_distribution, "constant", "default is constant. If set to random, the max value size " "will be FLAGS_value_size."); -DEFINE_uint64(threads, 10, "Number of concurrent threads to run benchmark"); - -DEFINE_double(read_ratio, 0, "Read threads = threads * read_ratio"); - -DEFINE_double( - existing_keys_ratio, 1, - "Ratio of keys to read / write that existed in the filled instance, for " - "example, if set to " - "1, all writes will be updates, and all read keys will be existed"); +DEFINE_uint64(threads, 10, + "Number of concurrent threads to run benchmark. " + "max_access_threads will override this when benchmarking fill."); DEFINE_bool(latency, false, "Stat operation latencies"); @@ -56,12 +48,10 @@ DEFINE_string(type, "string", "Storage engine to benchmark, can be string, sorted, hash, list " "or blackhole"); -DEFINE_bool(scan, false, - "If set true, read threads will do scan operations, this is valid " - "only if we benchmark sorted or hash engine"); - DEFINE_uint64(num_collection, 1, - "Number of collections in the instance to benchmark"); + "Number of collections in the instance to benchmark. " + "This will be ignored when benchmarking data type " + "string/blackhole."); DEFINE_uint64( batch_size, 0, @@ -89,6 +79,10 @@ DEFINE_bool(opt_large_sorted_collection_restore, true, DEFINE_bool(use_devdax_mode, false, "Use devdax device for kvdk"); +DEFINE_string(dest_memory_nodes, "", + "Set the memory nodes where volatile KV " + "memory allocator binds to"); + class Timer { public: void Start() { clock_gettime(CLOCK_REALTIME, &start); } @@ -128,12 +122,19 @@ enum class KeyDistribution { Range, Uniform, Zipf } key_dist; enum class ValueSizeDistribution { Constant, Uniform } vsz_dist; +// Define variables those differ across benchmarks +bool fill = false; +double read_ratio = 0; +double existing_keys_ratio = 0; +std::uint64_t batch_size = 0; +bool scan = false; +std::uint64_t num_operations = 0; + +std::uint64_t max_key = UINT64_MAX; +extd::zipfian_distribution* zipf = nullptr; +std::uniform_int_distribution uniform{0, UINT64_MAX}; + std::uint64_t generate_key(size_t tid) { - static std::uint64_t max_key = FLAGS_existing_keys_ratio == 0 - ? UINT64_MAX - : FLAGS_num_kv / FLAGS_existing_keys_ratio; - static extd::zipfian_distribution zipf{max_key, 0.99}; - static std::uniform_int_distribution uniform{0, max_key}; switch (key_dist) { case KeyDistribution::Range: { return ranges[tid].gen(); @@ -142,7 +143,7 @@ std::uint64_t generate_key(size_t tid) { return uniform(random_engines[tid].gen); } case KeyDistribution::Zipf: { - return zipf(random_engines[tid].gen); + return (*zipf)(random_engines[tid].gen); } default: { throw; @@ -189,11 +190,11 @@ void DBWrite(int tid) { Status s; switch (bench_data_type) { case DataType::String: { - if (FLAGS_batch_size == 0) { + if (batch_size == 0) { s = engine->Put(key, value, WriteOptions()); } else { batch->StringPut(key, std::string{value.data(), value.size()}); - if (operations % FLAGS_batch_size == 0) { + if ((operations + 1) % batch_size == 0) { s = engine->BatchWrite(batch); batch->Clear(); } @@ -201,12 +202,12 @@ void DBWrite(int tid) { break; } case DataType::Sorted: { - if (FLAGS_batch_size == 0) { + if (batch_size == 0) { s = engine->SortedPut(collections[cid], key, value); } else { batch->SortedPut(collections[cid], key, std::string{value.data(), value.size()}); - if (operations % FLAGS_batch_size == 0) { + if ((operations + 1) % batch_size == 0) { s = engine->BatchWrite(batch); batch->Clear(); } @@ -214,12 +215,12 @@ void DBWrite(int tid) { break; } case DataType::Hashes: { - if (FLAGS_batch_size == 0) { + if (batch_size == 0) { s = engine->HashPut(collections[cid], key, value); } else { batch->HashPut(collections[cid], key, std::string{value.data(), value.size()}); - if (operations % FLAGS_batch_size == 0) { + if ((operations + 1) % batch_size == 0) { s = engine->BatchWrite(batch); batch->Clear(); } @@ -403,7 +404,7 @@ void DBRead(int tid) { return; } -void ProcessBenchmarkConfigs() { +void InitializeBenchmark() { if (FLAGS_type == "sorted") { bench_data_type = DataType::Sorted; } else if (FLAGS_type == "string") { @@ -417,6 +418,32 @@ void ProcessBenchmarkConfigs() { } else { throw std::invalid_argument{"Unsupported data type"}; } + + if (bench_data_type != DataType::Blackhole) { + Configs configs; + configs.max_access_threads = FLAGS_max_access_threads; + configs.opt_large_sorted_collection_recovery = + FLAGS_opt_large_sorted_collection_restore; + configs.dest_memory_nodes = FLAGS_dest_memory_nodes; + Status s = Engine::Open(FLAGS_path, &engine, configs, stdout); + if (s != Status::Ok) { + throw std::runtime_error{ + std::string{"Fail to open KVDK instance. Status: "} + + KVDKStatusStrings[static_cast(s)]}; + } + } + + { + value_pool.clear(); + value_pool.reserve(FLAGS_value_size); + std::default_random_engine rand_engine{42}; + for (size_t i = 0; i < FLAGS_value_size; i++) { + value_pool.push_back('a' + rand_engine() % 26); + } + } +} + +void ProcessBenchmarkConfigs() { // Initialize collections and batch parameters switch (bench_data_type) { case DataType::String: @@ -434,7 +461,7 @@ void ProcessBenchmarkConfigs() { } } - if (FLAGS_batch_size > 0 && (bench_data_type == DataType::List)) { + if (batch_size > 0 && (bench_data_type == DataType::List)) { throw std::invalid_argument{R"(List does not support batch write.)"}; } @@ -442,7 +469,7 @@ void ProcessBenchmarkConfigs() { switch (bench_data_type) { case DataType::String: case DataType::List: { - if (FLAGS_scan) { + if (scan) { throw std::invalid_argument{ R"(Scan is not supported for "String" and "List" type data.)"}; } @@ -457,16 +484,17 @@ void ProcessBenchmarkConfigs() { } random_engines.resize(FLAGS_threads); - if (FLAGS_fill) { - assert(FLAGS_read_ratio == 0); + if (fill) { + assert(read_ratio == 0); key_dist = KeyDistribution::Range; - operations_per_thread = FLAGS_num_kv / FLAGS_threads + 1; - for (size_t i = 0; i < FLAGS_threads; i++) { + operations_per_thread = FLAGS_num_kv / FLAGS_max_access_threads + 1; + ranges.clear(); + for (size_t i = 0; i < FLAGS_max_access_threads; i++) { ranges.emplace_back(i * operations_per_thread, (i + 1) * operations_per_thread); } } else { - operations_per_thread = FLAGS_num_operations / FLAGS_threads; + operations_per_thread = num_operations / FLAGS_threads; if (FLAGS_key_distribution == "random") { key_dist = KeyDistribution::Uniform; } else if (FLAGS_key_distribution == "zipf") { @@ -483,45 +511,41 @@ void ProcessBenchmarkConfigs() { } else { throw std::runtime_error{"Invalid value size distribution"}; } + + max_key = existing_keys_ratio == 0 ? UINT64_MAX + : FLAGS_num_kv / existing_keys_ratio; + if (zipf) { + free(zipf); + } + zipf = new extd::zipfian_distribution(max_key, 0.99); + uniform = std::uniform_int_distribution(0, max_key); } -int main(int argc, char** argv) { - ParseCommandLineFlags(&argc, &argv, true); - ProcessBenchmarkConfigs(); +void ResetBenchmarkData() { + read_ops = 0; + write_ops = 0; + read_not_found = 0; + has_timed_out = false; + has_finished.clear(); + has_finished.resize(FLAGS_threads, 0); - if (bench_data_type != DataType::Blackhole) { - Configs configs; - configs.max_access_threads = FLAGS_max_access_threads; - configs.opt_large_sorted_collection_recovery = - FLAGS_opt_large_sorted_collection_restore; - Status s = Engine::Open(FLAGS_path, &engine, configs, stdout); - if (s != Status::Ok) { - throw std::runtime_error{ - std::string{"Fail to open KVDK instance. Status: "} + - KVDKStatusStrings[static_cast(s)]}; - } + if (FLAGS_latency) { + printf("calculate latencies\n"); + latencies.clear(); + latencies.resize(FLAGS_threads, std::vector(MAX_LAT, 0)); } +} - { - value_pool.clear(); - value_pool.reserve(FLAGS_value_size); - std::default_random_engine rand_engine{42}; - for (size_t i = 0; i < FLAGS_value_size; i++) { - value_pool.push_back('a' + rand_engine() % 26); - } - } +void RunBenchmark() { + ProcessBenchmarkConfigs(); + ResetBenchmarkData(); size_t write_threads = - FLAGS_fill ? FLAGS_threads - : FLAGS_threads - FLAGS_read_ratio * 100 * FLAGS_threads / 100; + fill ? FLAGS_max_access_threads + : FLAGS_threads - read_ratio * 100 * FLAGS_threads / 100; int read_threads = FLAGS_threads - write_threads; std::vector ts; - if (FLAGS_latency) { - printf("calculate latencies\n"); - latencies.resize(FLAGS_threads, std::vector(MAX_LAT, 0)); - } - switch (bench_data_type) { case DataType::Sorted: { printf("Create %ld Sorted Collections\n", FLAGS_num_collection); @@ -557,8 +581,6 @@ int main(int argc, char** argv) { } } - has_finished.resize(FLAGS_threads, 0); - std::cout << "Init " << read_threads << " readers " << "and " << write_threads << " writers." << std::endl; @@ -566,7 +588,7 @@ int main(int argc, char** argv) { ts.emplace_back(DBWrite, i); } for (size_t i = write_threads; i < FLAGS_threads; i++) { - ts.emplace_back(FLAGS_scan ? DBScan : DBRead, i); + ts.emplace_back(scan ? DBScan : DBRead, i); } size_t const field_width = 15; @@ -609,7 +631,7 @@ int main(int argc, char** argv) { if (num_finished == FLAGS_threads) { break; } - if (!FLAGS_fill && (duration.count() >= FLAGS_timeout * 1000)) { + if (!fill && (duration.count() >= FLAGS_timeout * 1000)) { // Signal a timeout for read, scan, update and insert // Fill will never timeout has_timed_out = true; @@ -656,7 +678,7 @@ int main(int argc, char** argv) { double l995 = 0; double l999 = 0; double l9999 = 0; - for (std::uint64_t i = 1; i <= MAX_LAT; i++) { + for (std::uint64_t i = 1; i < MAX_LAT; i++) { for (auto j = 0; j < read_threads; j++) { cur += latencies[write_threads + j][i]; total += latencies[write_threads + j][i] * i; @@ -692,7 +714,7 @@ int main(int argc, char** argv) { double l995 = 0; double l999 = 0; double l9999 = 0; - for (std::uint64_t i = 1; i <= MAX_LAT; i++) { + for (std::uint64_t i = 1; i < MAX_LAT; i++) { for (size_t j = 0; j < write_threads; j++) { cur += latencies[j][i]; total += latencies[j][i] * i; @@ -718,8 +740,108 @@ int main(int argc, char** argv) { avg, l50, l99, l995, l999, l9999); } } +} +void FinalizeBenchmark() { if (bench_data_type != DataType::Blackhole) delete engine; + if (zipf) { + free(zipf); + } +} + +int main(int argc, char** argv) { + // Gflags function + ParseCommandLineFlags(&argc, &argv, true); + + InitializeBenchmark(); + + // fill + std::cout << "##########################################################\n" + << "Benchmark started: fill" << std::endl; + fill = true; + read_ratio = 0; + existing_keys_ratio = 0; + batch_size = 0; + scan = false; + num_operations = FLAGS_num_operations; + RunBenchmark(); + + // random batch insert + std::cout << "##########################################################\n" + << "Benchmark started: random batch insert" << std::endl; + fill = false; + read_ratio = 0; + existing_keys_ratio = 0; + batch_size = 100; + scan = false; + if (bench_data_type != DataType::Blackhole) { + num_operations = FLAGS_num_kv; + } + RunBenchmark(); + + // random insert + std::cout << "##########################################################\n" + << "Benchmark started: random insert" << std::endl; + fill = false; + read_ratio = 0; + existing_keys_ratio = 0; + batch_size = 0; + scan = false; + if (bench_data_type != DataType::Blackhole) { + num_operations = FLAGS_num_kv; + } + RunBenchmark(); + + // range scan + if (bench_data_type != DataType::String && + bench_data_type != DataType::List) { + std::cout << "##########################################################\n" + << "Benchmark started: range scan" << std::endl; + fill = false; + read_ratio = 1; + existing_keys_ratio = 1; + batch_size = 0; + scan = true; + num_operations = FLAGS_num_operations; + RunBenchmark(); + } + + // random read + + std::cout << "##########################################################\n" + << "Benchmark started: random read" << std::endl; + fill = false; + read_ratio = 1; + existing_keys_ratio = 1; + batch_size = 0; + scan = false; + num_operations = FLAGS_num_operations; + RunBenchmark(); + + // random read write (9R:1W) + std::cout << "##########################################################\n" + << "Benchmark started: random read write (9R:1W)" << std::endl; + fill = false; + read_ratio = 0.9; + existing_keys_ratio = 1; + batch_size = 0; + scan = false; + num_operations = FLAGS_num_operations; + RunBenchmark(); + + // random update + std::cout << "##########################################################\n" + << "Benchmark started: random update" << std::endl; + fill = false; + read_ratio = 0; + existing_keys_ratio = 1; + batch_size = 0; + scan = false; + num_operations = FLAGS_num_operations; + RunBenchmark(); + + FinalizeBenchmark(); + return 0; } From 4c003c3fe52026ca79dc1e0ed3cd6b1d3f16b976 Mon Sep 17 00:00:00 2001 From: "Yu, Peng" Date: Mon, 14 Nov 2022 15:19:56 +0800 Subject: [PATCH 3/5] reduce memory consumption of default benchmark configurations; update doc Signed-off-by: Yu, Peng --- volatile/benchmark/bench.cpp | 37 +++++---- volatile/doc/benchmark.md | 149 +++++------------------------------ 2 files changed, 36 insertions(+), 150 deletions(-) diff --git a/volatile/benchmark/bench.cpp b/volatile/benchmark/bench.cpp index ee71e21a..44592181 100644 --- a/volatile/benchmark/bench.cpp +++ b/volatile/benchmark/bench.cpp @@ -22,9 +22,9 @@ using namespace KVDK_NAMESPACE; // Benchmark configs DEFINE_string(path, "/mnt/pmem0/kvdk", "Instance path"); -DEFINE_uint64(num_kv, (1 << 30), "Number of KVs to place"); +DEFINE_uint64(num_kv, (1 << 23), "Number of KVs to place"); -DEFINE_uint64(num_operations, (1 << 30), +DEFINE_uint64(num_operations, (1 << 20), "Number of total operations. " "num_kv will override this when benchmarking fill/insert"); @@ -63,14 +63,10 @@ DEFINE_string(key_distribution, "random", "be ignored and only uniform distribution will be used"); // Engine configs -DEFINE_bool( - populate, false, - "Populate pmem space while creating a new instance. This can improve write " - "performance in runtime, but will take long time to init the instance"); - DEFINE_uint64(max_access_threads, 64, "Max access threads of the instance"); -DEFINE_uint64(space, (256ULL << 30), "Max usable PMem space of the instance"); +DEFINE_uint64(hash_bucket_num, (1 << 20), + "The number of initial buckets in hash table"); DEFINE_bool(opt_large_sorted_collection_restore, true, " Optional optimization strategy which Multi-thread recovery a " @@ -129,6 +125,7 @@ double existing_keys_ratio = 0; std::uint64_t batch_size = 0; bool scan = false; std::uint64_t num_operations = 0; +std::uint64_t benchmark_threads = 0; std::uint64_t max_key = UINT64_MAX; extd::zipfian_distribution* zipf = nullptr; @@ -422,6 +419,7 @@ void InitializeBenchmark() { if (bench_data_type != DataType::Blackhole) { Configs configs; configs.max_access_threads = FLAGS_max_access_threads; + configs.hash_bucket_num = FLAGS_hash_bucket_num; configs.opt_large_sorted_collection_recovery = FLAGS_opt_large_sorted_collection_restore; configs.dest_memory_nodes = FLAGS_dest_memory_nodes; @@ -483,18 +481,19 @@ void ProcessBenchmarkConfigs() { throw std::invalid_argument{"value size too large"}; } - random_engines.resize(FLAGS_threads); + benchmark_threads = fill ? FLAGS_max_access_threads : FLAGS_threads; + random_engines.resize(benchmark_threads); if (fill) { assert(read_ratio == 0); key_dist = KeyDistribution::Range; - operations_per_thread = FLAGS_num_kv / FLAGS_max_access_threads + 1; + operations_per_thread = FLAGS_num_kv / benchmark_threads + 1; ranges.clear(); - for (size_t i = 0; i < FLAGS_max_access_threads; i++) { + for (size_t i = 0; i < benchmark_threads; i++) { ranges.emplace_back(i * operations_per_thread, (i + 1) * operations_per_thread); } } else { - operations_per_thread = num_operations / FLAGS_threads; + operations_per_thread = num_operations / benchmark_threads; if (FLAGS_key_distribution == "random") { key_dist = KeyDistribution::Uniform; } else if (FLAGS_key_distribution == "zipf") { @@ -527,12 +526,12 @@ void ResetBenchmarkData() { read_not_found = 0; has_timed_out = false; has_finished.clear(); - has_finished.resize(FLAGS_threads, 0); + has_finished.resize(benchmark_threads, 0); if (FLAGS_latency) { printf("calculate latencies\n"); latencies.clear(); - latencies.resize(FLAGS_threads, std::vector(MAX_LAT, 0)); + latencies.resize(benchmark_threads, std::vector(MAX_LAT, 0)); } } @@ -541,9 +540,9 @@ void RunBenchmark() { ResetBenchmarkData(); size_t write_threads = - fill ? FLAGS_max_access_threads - : FLAGS_threads - read_ratio * 100 * FLAGS_threads / 100; - int read_threads = FLAGS_threads - write_threads; + fill ? benchmark_threads + : benchmark_threads - read_ratio * 100 * benchmark_threads / 100; + int read_threads = fill ? 0 : benchmark_threads - write_threads; std::vector ts; switch (bench_data_type) { @@ -587,7 +586,7 @@ void RunBenchmark() { for (size_t i = 0; i < write_threads; i++) { ts.emplace_back(DBWrite, i); } - for (size_t i = write_threads; i < FLAGS_threads; i++) { + for (size_t i = write_threads; i < benchmark_threads; i++) { ts.emplace_back(scan ? DBScan : DBRead, i); } @@ -628,7 +627,7 @@ void RunBenchmark() { if (num_finished == 0 || idx < 2) { last_effective_idx = idx; } - if (num_finished == FLAGS_threads) { + if (num_finished == benchmark_threads) { break; } if (!fill && (duration.count() >= FLAGS_timeout * 1000)) { diff --git a/volatile/doc/benchmark.md b/volatile/doc/benchmark.md index 34311427..968dcfd7 100644 --- a/volatile/doc/benchmark.md +++ b/volatile/doc/benchmark.md @@ -2,151 +2,38 @@ To test performance of KVDK, you can run our benchmark tool "bench", the tool is auto-built along with KVDK library in the build dir. -You can manually run individual benchmark follow the examples as shown bellow, or simply run our basic benchmark script "scripts/run_benchmark.py" to test all the basic read/write performance. - -To run the script, you shoulf first build kvdk, then run: - +Here is an example to run benchmarks on `string` type: +```bash +./bench -path=./kvdk_bench_dir -type=string -num_kv=8388608 -num_operations=1048576 -threads=10 -max_access_threads=64 -value_size=120 -latency=1 ``` -scripts/run_benchmark.py [data_type] [key distribution] -``` - -data_type: Which data type to benchmark, it can be string/sorted/hash/list/blackhole/all -key distribution: Distribution of key of the benchmark workloads, it can be random/zipf/all -## Fill data to new instance - -To test performance, we need to first fill key-value pairs to the KVDK instance. Since KVDK did not support cross-socket access yet, we need to bind bench program to a numa node: - - numactl --cpunodebind=0 --membind=0 ./bench -fill=1 -value_size=120 -threads=64 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string -populate=1 +To benchmark performance when KVs are stored on separted memory nodes, we can use `numactl`: +```bash +numactl --cpunodebind=0 --membind=0 ./bench -path=./kvdk_bench_dir -type=string -num_kv=8388608 -num_operations=1048576 -threads=10 -max_access_threads=64 -value_size=120 -latency=1 -dest_memory_nodes=1 +``` -This command will fill 83886088 uniform distributed string-type key-value pairs to the KVDK instance that located at /mnt/pmem0/kvdk. +The above configurations will consume ~7 GB memory. Explanation of arguments: - -fill: Indicates filling data to a new instance. - - -threads: Number of threads of benchmark. + -path: KVDK initialized here - -space: PMem space that allocate to the KVDK instance. + -type: Type of key-value pairs to benchmark, it can be string/sorted/hash/list/blackhole. - -max_access_threads: Max concurrent access threads in the KVDK instance, set it to the number of the hyper-threads for performance consideration. You can call KVDK API with any number of threads, but if your parallel threads more than max_access_threads, the performance will be degraded due to synchronization cost + -num_kv: Number of KV when benchmarking fill/insert. - -type: Type of key-value pairs to benchmark, it can be "string", "hash" or "sorted". + -num_operations: Number of operations running benchmarks other than fill/insert. - -populate: Populate pmem space while creating new KVDK instance for best write performance in runtime, see "include/kvdk/configs.hpp" for explanation. + -threads: Number of threads of benchmark. `max_access_threads` will override this when benchmarking `fill`. -## Test read/write performance + -max_access_threads: Max concurrent access threads in the KVDK instance, set it to the number of the hyper-threads for performance consideration. You can call KVDK API with any number of threads, but if your parallel threads more than max_access_threads, the performance will be degraded due to synchronization cost. -### Read performance + -value_size: Value length of values in Byte. -After fill the instance, we can test read performance with the command below: + -latency: Print latencies of operations or not. - numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=1 -existing_keys_ratio=1 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string - -This will read key-value pairs from the KVDK instance with 48 threads in 10 seconds. - -Explanation of arguments: - - -read_ratio: Ratio of read threads among benchmark threads, for example, if set it to 0.5, then there will be 24 write threads and 24 read threads. - - -existing_keys_ratio: Ratio of keys among key-value pairs to read that already filled in the instance. For example, if set it to 0.5, then 50% read operations will return NotFound. - -Benchmark tool will print performance stats to stdout, include throughput in each second and average ops: - - $numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=1 -existing_keys_ratio=1 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string - - [LOG] time 0 ms: Initializing PMem size 274877906944 in file /mnt/pmem0/kvdk/data - [LOG] time 1864 ms: Map pmem space done - [LOG] time 9033 ms: In restoring: iterated 840882543 records - init 0 write threads - init 64 read threads - ------- ops in seconds ----------- - time (ms), read ops, not found, write ops, total read, total write - 1000 73691000 0 0 73691000 0 - 2001 73613000 0 0 147304000 0 - 3002 73643000 0 0 220947000 0 - 4003 73656000 0 0 294603000 0 - 5004 73675000 0 0 368278000 0 - 6005 73667000 0 0 441945000 0 - 7006 73699000 0 0 515644000 0 - 8007 73647000 0 0 589291000 0 - 9008 73634000 0 0 662925000 0 - 10009 73677000 0 0 736602000 0 - finish bench - ------------ statistics ------------ - read ops 73660400, write ops 0 - [LOG] time 19051 ms: instance closed - - - -### Write performance - -Similarily, to test write performance, we can simply modify "read_ratio": - - numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=0 -existing_keys_ratio=0 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string - -This command will insert new key-value pairs to the KVDK instance in 10 seconds. Likely wise, by modify "existing_keys_ratio", we can control how many write operations are updates. - - $numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=0 -existing_keys_ratio=0 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string - - [LOG] time 0 ms: Initializing PMem size 274877906944 in file /mnt/pmem0/kvdk/data - [LOG] time 1865 ms: Map pmem space done - [LOG] time 9015 ms: In restoring: iterated 840882543 records - init 64 write threads - init 0 read threads - ------- ops in seconds ----------- - time (ms), read ops, not found, write ops, total read, total write - 1000 0 0 50610000 0 50610000 - 2007 0 0 50053000 0 100663000 - 3016 0 0 49669000 0 150332000 - 4017 0 0 49048000 0 199380000 - 5018 0 0 48540000 0 247920000 - 6022 0 0 48210000 0 296130000 - 7023 0 0 47725000 0 343855000 - 8024 0 0 47354000 0 391209000 - 9027 0 0 47080000 0 438289000 - 10028 0 0 46544000 0 484833000 - finish bench - ------------ statistics ------------ - read ops 0, write ops 48483400 - [LOG] time 19055 ms: instance closed - - -### Stat latencies - -We can also stat latency information by add "-latency=1" to the benchmark command. - - $ numactl --cpunodebind=0 --membind=0 ./bench -fill=0 -time=10 -value_size=120 -threads=64 -read_ratio=0.5 -existing_keys_ratio=1 -path=/mnt/pmem0/kvdk -space=274877906944 -num=838860800 -max_write_threads=64 -type=string -latency=1 - - [LOG] time 0 ms: Initializing PMem size 274877906944 in file /mnt/pmem0/kvdk/data - [LOG] time 1869 ms: Map pmem space done - [LOG] time 14963 ms: In restoring: iterated 1323729106 records - calculate latencies - init 6 write threads - init 58 read threads - ------- ops in seconds ----------- - time (ms), read ops, not found, write ops, total read, total write - 1000 62763000 0 3933000 62763000 3933000 - 2001 62297000 0 4303000 125060000 8236000 - 3002 62190000 0 4530000 187250000 12766000 - 4003 62194000 0 4530000 249444000 17296000 - 5004 62206000 0 4531000 311650000 21827000 - 6005 62172000 0 4527000 373822000 26354000 - 7006 62194000 0 4530000 436016000 30884000 - 8007 62227000 0 4535000 498243000 35419000 - 9008 62196000 0 4529000 560439000 39948000 - 10009 62190000 0 4527000 622629000 44475000 - finish bench - ------------ statistics ------------ - read ops 62263100, write ops 4447500 - read lantencies (us): Avg: 0.89, P50: 0.83, P99: 1.54, P99.5: 1.67, P99.9: 2.77, P99.99: 4.20 - write lantencies (us): Avg: 0.09, P50: 1.22, P99: 2.64, P99.5: 3.25, P99.9: 4.22, P99.99: 5.35 - [LOG] time 28382 ms: instance closed + -dest_memory_nodes: The memory nodes to store KV data. ## More configurations -For more configurations of the benchmark tool, please reference to "benchmark/bench.cpp" and "scripts/basic_benchmarks.py". - - - - +For more configurations of the benchmark tool, please reference to "benchmark/bench.cpp". From 1fcb55cc5663d328bb381351b8097200a9d5cf7f Mon Sep 17 00:00:00 2001 From: "Yu, Peng" Date: Wed, 16 Nov 2022 11:52:53 +0800 Subject: [PATCH 4/5] update doc Signed-off-by: Yu, Peng --- volatile/doc/benchmark.md | 8 ++++---- volatile/engine/kv_engine.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/volatile/doc/benchmark.md b/volatile/doc/benchmark.md index 968dcfd7..fa1b544d 100644 --- a/volatile/doc/benchmark.md +++ b/volatile/doc/benchmark.md @@ -4,15 +4,15 @@ To test performance of KVDK, you can run our benchmark tool "bench", the tool is Here is an example to run benchmarks on `string` type: ```bash -./bench -path=./kvdk_bench_dir -type=string -num_kv=8388608 -num_operations=1048576 -threads=10 -max_access_threads=64 -value_size=120 -latency=1 +./bench -path=./kvdk_bench_dir -type=string -num_kv=8388608 -num_operations=1048576 -threads=10 -max_access_threads=64 -value_size=120 -latency=0 ``` To benchmark performance when KVs are stored on separted memory nodes, we can use `numactl`: ```bash -numactl --cpunodebind=0 --membind=0 ./bench -path=./kvdk_bench_dir -type=string -num_kv=8388608 -num_operations=1048576 -threads=10 -max_access_threads=64 -value_size=120 -latency=1 -dest_memory_nodes=1 +numactl --cpunodebind=0 --membind=0 ./bench -path=./kvdk_bench_dir -type=string -num_kv=8388608 -num_operations=1048576 -threads=10 -max_access_threads=64 -value_size=120 -latency=0 -dest_memory_nodes=1 ``` -The above configurations will consume ~7 GB memory. +The above configurations will consume ~5.4 GB memory. Specifically, ~4.5 GB memory is used by KVDK. ~0.9 GB memory is used by the benchmark itself. Explanation of arguments: @@ -30,7 +30,7 @@ Explanation of arguments: -value_size: Value length of values in Byte. - -latency: Print latencies of operations or not. + -latency: Print latencies of operations or not. Enable this makes benchmark use more memory. -dest_memory_nodes: The memory nodes to store KV data. diff --git a/volatile/engine/kv_engine.cpp b/volatile/engine/kv_engine.cpp index 3fc1d0da..b57c06a4 100644 --- a/volatile/engine/kv_engine.cpp +++ b/volatile/engine/kv_engine.cpp @@ -1041,7 +1041,7 @@ template DLRecord* KVEngine::removeOutDatedVersion(DLRecord*, namespace KVDK_NAMESPACE { /// TODO: move this into VersionController. -Snapshot* KVEngine::GetSnapshot(bool make_checkpoint) { +Snapshot* KVEngine::GetSnapshot(bool) { Snapshot* ret = version_controller_.NewGlobalSnapshot(); return ret; } From 8b2fc9ff54ad3667d68baf16d70f5aef53aebc95 Mon Sep 17 00:00:00 2001 From: "Yu, Peng" Date: Wed, 16 Nov 2022 11:53:58 +0800 Subject: [PATCH 5/5] update doc Signed-off-by: Yu, Peng --- volatile/doc/benchmark.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatile/doc/benchmark.md b/volatile/doc/benchmark.md index fa1b544d..9156c5a9 100644 --- a/volatile/doc/benchmark.md +++ b/volatile/doc/benchmark.md @@ -30,7 +30,7 @@ Explanation of arguments: -value_size: Value length of values in Byte. - -latency: Print latencies of operations or not. Enable this makes benchmark use more memory. + -latency: Print latencies of operations or not. Enabling this makes benchmark use more memory. -dest_memory_nodes: The memory nodes to store KV data.