19 Commits
Author SHA1 Message Date
andrew e23018c952 Merge pull request 'Improve perf/codegen' (#94) from perf into main
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (push) Successful in 3m46s
CI / release (arm64, , ubuntu-latest-arm64) (push) Successful in 3m48s
CI / pre-commit (push) Successful in 2m11s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (push) Successful in 4m4s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (push) Successful in 3m28s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (push) Successful in 3m28s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (push) Successful in 3m29s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (push) Successful in 6m6s
CI / coverage (push) Successful in 4m5s
Reviewed-on: #94
2026-10-27 20:45:52 +00:00
andrew b1c1f8f2ee Merge pull request 'Replace test_fixup_256 with a regression test for the Node256 fixupMaxVersion false-Commit bug' (#90) from weaselbot/conflict-set:weaselbot/issue-89 into main
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (push) Successful in 3m47s
CI / release (arm64, , ubuntu-latest-arm64) (push) Successful in 3m27s
CI / pre-commit (push) Successful in 2m11s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (push) Successful in 4m6s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (push) Successful in 3m24s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (push) Successful in 3m24s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (push) Successful in 3m37s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (push) Successful in 5m53s
CI / coverage (push) Successful in 4m4s
Reviewed-on: #90
2026-10-27 19:33:17 +00:00
andrew 4bec2cf4eb Merge pull request 'Make SHOW_MEMORY builds work: fix deferred node capacity assert and eraseTree use-after-free' (#91) from weaselbot/conflict-set:weaselbot/issue-88 into main
CI / pre-commit (push) Canceled after 0s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (push) Canceled after 1s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (push) Canceled after 0s
CI / release (arm64, , ubuntu-latest-arm64) (push) Canceled after 0s
CI / coverage (push) Canceled after 0s
Reviewed-on: #91
2026-10-27 19:32:43 +00:00
andrew fa1701479c Merge pull request 'SkipList: don't lower oldestVersion in setOldestVersion' (#92) from weaselbot/conflict-set:weaselbot/issue-87 into main
CI / pre-commit (push) Canceled after 0s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (push) Canceled after 0s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (push) Canceled after 0s
CI / release (arm64, , ubuntu-latest-arm64) (push) Canceled after 0s
CI / coverage (push) Canceled after 0s
Reviewed-on: #92
2026-10-27 19:30:11 +00:00
andrew 20e2e332e4 Merge pull request 'Skip DEBUG_VERBOSE end print for point writes (uninitialized Result::endRemaining)' (#93) from weaselbot/conflict-set:weaselbot/issue-86 into main
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (push) Successful in 3m50s
CI / pre-commit (push) Canceled after 0s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (push) Canceled after 0s
CI / release (arm64, , ubuntu-latest-arm64) (push) Canceled after 35s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (push) Canceled after 0s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (push) Canceled after 0s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (push) Canceled after 0s
CI / coverage (push) Canceled after 0s
Reviewed-on: #93
2026-10-27 19:29:26 +00:00
andrew 23d092ab69 Avoid IFUNC for short prefixes in LCP
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (push) Successful in 3m53s
CI / release (arm64, , ubuntu-latest-arm64) (push) Successful in 3m26s
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m46s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m29s
CI / pre-commit (push) Successful in 2m10s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (push) Successful in 4m6s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (push) Successful in 3m30s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (push) Successful in 3m29s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (push) Successful in 3m32s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (push) Successful in 6m3s
CI / coverage (push) Successful in 4m0s
CI / pre-commit (pull_request) Successful in 2m11s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 4m14s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m32s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m32s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m29s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 6m11s
CI / coverage (pull_request) Successful in 4m5s
2026-10-27 15:09:58 -04:00
andrew 84bf5707f9 Skip IFUNC call for Node3 in fixupMaxVersion
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (push) Successful in 3m45s
CI / release (arm64, , ubuntu-latest-arm64) (push) Successful in 3m35s
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m48s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m26s
CI / pre-commit (pull_request) Canceled after 0s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Canceled after 0s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Canceled after 0s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Canceled after 0s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Canceled after 0s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Canceled after 0s
CI / coverage (pull_request) Canceled after 0s
CI / pre-commit (push) Successful in 2m22s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (push) Successful in 4m27s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (push) Successful in 7m43s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (push) Successful in 3m32s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (push) Successful in 3m35s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (push) Successful in 6m12s
CI / coverage (push) Successful in 4m0s
2026-10-27 14:21:58 -04:00
weaselbot c94e9aaee6 Remove test_fixup_256, which cannot detect the bug
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m43s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m24s
CI / pre-commit (pull_request) Successful in 2m24s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 4m12s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m28s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 7m40s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 4m14s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 7m11s
CI / coverage (pull_request) Successful in 3m57s
Every child of the Node256 in test_fixup_256 is written at the same
version, so max(childMaxVersion[0..15]) and max(maxOfMax) agree and the
buggy code passes. test_fixup_256_heterogeneous_versions covers the same
Node256-under-a-range-end shape with distinct versions, so keep only that.
2026-10-27 14:10:36 -04:00
andrew d71d963d9a Improve codegen for checkMaxBetweenExclusiveImpl(Node3
$ ninja -C build && nm -C -S build/CMakeFiles/conflict-set-object.dir/ConflictSet.cpp.o | grep 'checkMaxBetweenExclusive(Node3'
...
0000000000000000 0000000000000529 T checkMaxBetweenExclusive(Node3*, int, int, InternalVersionT, ReadContext*)
$ git stash pop
...
$ ninja -C build && nm -C -S build/CMakeFiles/conflict-set-object.dir/ConflictSet.cpp.o | grep 'checkMaxBetweenExclusive(Node3'
...
0000000000000000 0000000000000106 T checkMaxBetweenExclusive(Node3*, int, int, InternalVersionT, ReadContext*)
2026-10-27 14:06:58 -04:00
andrew dabddbcb56 Use individual comparison operators instead of operator<=>
This gives better codegen
2026-10-27 13:55:44 -04:00
andrew 3b4271dac1 Update readme 2026-10-27 13:44:11 -04:00
weaselbot 56e0673fc7 Make the skip list corpus tests opt-in, not default
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m50s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m20s
CI / pre-commit (pull_request) Successful in 2m27s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 8m0s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 4m40s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m24s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m25s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 12m18s
CI / coverage (pull_request) Successful in 4m15s
Review feedback: running the skip list tests on every default ctest run is a
waste of time. Gate them behind a TEST_SKIP_LIST CMake option, off by default,
so they remain easy to run when cross-checking SkipList.cpp against
ReferenceImpl.

Verified with -DTEST_SKIP_LIST=ON: all 2845 skip_list corpus tests pass; the
default test list is unchanged.
2026-10-27 13:02:45 -04:00
weaselbot 80f084cc43 Compile eraseTree's search path length bookkeeping out of non-SHOW_MEMORY builds
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 5m8s
CI / pre-commit (pull_request) Successful in 6m12s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m49s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 4m55s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m57s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m54s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m34s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 5m56s
CI / coverage (pull_request) Successful in 3m58s
`eraseTree` only carries the search path lengths of queued nodes around so
that the SHOW_MEMORY key accounting doesn't have to walk `parent` pointers of
nodes that have already been released. Put the lengths on a parallel stack
that only exists when SHOW_MEMORY is enabled, so that other builds keep the
upstream worklist of bare `Node *` and don't compute anything extra.
2026-10-27 12:56:55 -04:00
weaselbot c0bd10effa Drop redundant coverage exclusion markers in the excluded tail region
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m48s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m18s
CI / pre-commit (pull_request) Successful in 2m15s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 3m54s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m23s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m21s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m24s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 5m44s
CI / coverage (pull_request) Successful in 3m54s
`getNodeSize`'s unreachable `default:` sits inside ConflictSet.cpp's
`GCOVR_EXCL_START`/`GCOVR_EXCL_STOP` block, so the `GCOVR_EXCL_LINE` markers on
those two lines are redundant: they cannot change the report, but they make
gcovr print

    (WARNING) GCOVR_EXCL_LINE found on line 6002 in excluded region started on
    line 5535

once per line for each of the four reports the coverage job writes. Drop them,
matching the rest of the excluded tail, which carries no exclusion markers.

Context: the previous coverage job for this branch did not fail in any of its
steps. Build, Test (5754/5754 passed), Coverage report (`gcovr
--fail-under-line 100` reported 100% for ConflictSet.cpp, LongestCommonPrefix.h
and Metrics.h), the upload and the test summary all reported success; the job
was marked failed while tearing the job container down. The post-actions
(actions/cache, actions/checkout) could no longer `docker exec` into the
container and the runner's rootless podman socket was gone ("Cannot connect to
the Docker daemon at unix:///run/user/992/podman/podman.sock", "Error while stop
job container"); the actions/cache service on 192.168.0.110:41379 had been
refusing connections for the whole run.

Verified locally on this commit: Debug, gcc and `--coverage` builds of the
coverage job's configuration; 5754/5754 tests pass; merged line coverage (union
over the library, SHOW_MEMORY and fuzz-driver objects, honouring the
GCOVR_EXCL_START/STOP regions) is 100% for all three measured files, and no
GCOVR_EXCL_LINE markers remain inside an excluded region.
2026-10-26 21:47:41 -04:00
weaselbot df986810ff Give the SHOW_MEMORY instrumentation a single definition per function
CI / pre-commit (pull_request) Successful in 2m8s
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m49s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 3m54s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m25s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m22s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m18s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m25s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 5m50s
CI / coverage (pull_request) Failing after 3m10s
The coverage job compiles ConflictSet.cpp twice: once without SHOW_MEMORY,
like the rest of the build, and once with it for the SHOW_MEMORY test. Each
config previously saw a *different* definition of the accounting helpers:
no-op `constexpr` stubs near the top of the file when SHOW_MEMORY was off,
and the real definitions at the bottom when it was on.

gcovr reads the .gcda files for a source file and merges them, and its
default (strict) function merge mode asserts that a function name always
maps to one line. Since `addNode`, `removeNode`, `addKey`, both `removeKey`
overloads and `getSearchPathLength` were reported on two different lines,
report generation aborted with

    AssertionError: Got function getSearchPathLength(Node*) on multiple
    lines: 99, 6002.

Define those functions in exactly one place instead, and compile the
accounting out from within the bodies. Then every configuration reports each
function on the same line and the coverage data can be merged. The empty
calls are inlined away: the Release shared library contains no calls to
them.

With the merge fixed, the accounting lines that only exist in the SHOW_MEMORY
build show up in the report, so the coverage job's 100% line requirement
covers them. Extend the SHOW_MEMORY test to grow a node through every node
type (so `getNodeSize` covers every case) and to erase entries one at a time
via `setOldestVersion` (so `removeKey` and the `internal_setOldestVersion`
cross-check are covered). The remaining `abort()`s in the accounting are
defensive checks that can never be taken on a passing run; mark them
GCOVR_EXCL_LINE like the rest of the unreachable code in this file.
2026-10-26 20:34:45 -04:00
weaselbot 8b6389236a Enable the skip list corpus tests by default
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m50s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m33s
CI / pre-commit (pull_request) Successful in 2m8s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 4m12s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m37s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m33s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m37s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 6m13s
CI / coverage (pull_request) Successful in 4m1s
driver_skip_list checks SkipList.cpp against ReferenceImpl, but its tests
were disabled, so the implementation diverged from the documented API
without anything noticing. The corpus tests pass in Debug and Release
builds, so run them like the other implementations' tests.
2026-10-26 18:46:02 -04:00
weaselbot b22d5f4816 SkipList: don't lower oldestVersion in setOldestVersion
setOldestVersion is documented to set oldestVersion to the maximum of its
previous value and its argument, so passing a version older than the
current oldestVersion is valid input. SkipList.cpp instead asserted that
the argument was monotonically non-decreasing, aborting Debug builds, and
with NDEBUG it lowered oldestVersion. That resurrected a window of
versions that callers had been told result in TooOld: reads in that
window were evaluated against entries that may already have been garbage
collected, so they could report Commit or Conflict instead.

Leave oldestVersion alone when the argument isn't newer, mirroring
HashTable.cpp. Falling through on equality keeps garbage collection
making progress without advancing the oldest version.

Add a scripted regression test, which bisimulates against the radix tree.
2026-10-26 18:46:00 -04:00
weaselbot bba328f79f Fix SHOW_MEMORY crashes on valid input
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 4m8s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 4m9s
CI / pre-commit (pull_request) Successful in 2m9s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 3m56s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m29s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m21s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m24s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 5m48s
CI / coverage (pull_request) Failing after 3m26s
`Node::getCapacity()` asserted `!releaseDeferred`, but releasing a deferred
node requires its capacity, so any node replacement aborted an assertion-
enabled `SHOW_MEMORY` build. `partialKeyCapacity` is not repurposed by
`deferRelease`, so there is no reason to reject deferred nodes. Drop the
assertion.

`eraseTree` released a node before visiting its children, so the `parent`
pointers walked by `removeKey` pointed to freed memory (and `deferRelease`
repurposes `parent` as a list link). Carry the search path length of each node
in the worklist, computed while its parent is still alive, and account for it
with the new `removeKey` overload.

Add a test covering the SHOW_MEMORY instrumentation, which the rest of the
test suite doesn't compile with.
2026-10-26 18:32:03 -04:00
weaselbot 3d3184ef5f Add regression test for Node256 fixupMaxVersion
CI / pre-commit (pull_request) Successful in 2m37s
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m53s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 3m51s
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m31s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m26s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m17s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m25s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 6m3s
CI / coverage (pull_request) Successful in 3m50s
2026-10-26 18:20:37 -04:00
9 changed files with 437 additions and 54 deletions

No files matched your search

+23 -2
View File
@@ -182,6 +182,8 @@ endif()
include(CTest)
option(TEST_SKIP_LIST "Run the slow skip list cross-check tests" OFF)
# disable tests if this is being used through e.g. FetchContent
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
@@ -224,8 +226,9 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
target_compile_options(driver_skip_list PRIVATE ${TEST_FLAGS})
target_link_libraries(driver_skip_list PRIVATE skip_list)
# enable to test skip list
if(0)
# Optional cross-check of SkipList.cpp against ReferenceImpl. Opt-in via
# -DTEST_SKIP_LIST=ON, since these are slow.
if(TEST_SKIP_LIST)
foreach(TEST ${CORPUS_TESTS})
get_filename_component(hash ${TEST} NAME)
add_test(NAME skip_list_${hash} COMMAND driver_skip_list ${TEST})
@@ -390,6 +393,24 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
PROPERTIES CXX_STANDARD_REQUIRED ON)
add_test(NAME conflict_set_cxx_api_test COMMAND conflict_set_cxx_api_test)
# SHOW_MEMORY instrumentation, which the rest of the test suite doesn't
# compile with
add_executable(conflict_set_show_memory_test ConflictSet.cpp
ShowMemoryTest.cpp)
target_compile_options(conflict_set_show_memory_test PRIVATE ${TEST_FLAGS})
target_compile_definitions(conflict_set_show_memory_test
PRIVATE SHOW_MEMORY=1)
target_include_directories(conflict_set_show_memory_test
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
if(NOT CMAKE_CROSSCOMPILING)
target_compile_options(conflict_set_show_memory_test
PRIVATE -fsanitize=address,undefined)
target_link_options(conflict_set_show_memory_test PRIVATE
-fsanitize=address,undefined)
endif()
add_test(NAME conflict_set_show_memory_test
COMMAND conflict_set_show_memory_test)
# symbol visibility tests
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
if(APPLE)
+151 -21
View File
@@ -80,17 +80,19 @@ using namespace weaselab;
#define assume assert
#endif
#if SHOW_MEMORY
// The SHOW_MEMORY instrumentation. Declared unconditionally so that each of
// these has a single definition in every configuration (see the definitions at
// the end of this file), rather than a no-op stub standing in for the real
// thing.
void addNode(struct Node *);
void removeNode(struct Node *);
void addKey(struct Node *);
void removeKey(struct Node *);
#else
constexpr void addNode(struct Node *) {}
constexpr void removeNode(struct Node *) {}
constexpr void addKey(struct Node *) {}
constexpr void removeKey(struct Node *) {}
#endif
// Like `removeKey`, but with a caller-provided search path length. Used when
// the node's ancestors may already have been released, which makes walking
// `parent` pointers invalid.
void removeKey(struct Node *, int64_t searchPathLength);
int64_t getSearchPathLength(struct Node *);
// ==================== BEGIN IMPLEMENTATION ====================
@@ -116,13 +118,40 @@ struct InternalVersionT {
constexpr InternalVersionT() = default;
constexpr explicit InternalVersionT(int64_t value) : value(value) {}
constexpr int64_t toInt64() const { return value; } // GCOVR_EXCL_LINE
constexpr auto operator<=>(const InternalVersionT &rhs) const {
constexpr bool operator<(const InternalVersionT &rhs) const {
#if USE_64_BIT
return value <=> rhs.value;
return value < rhs.value;
#else
// Maintains ordering after overflow, as long as the full-precision versions
// are within `kMaxCorrectVersionWindow` of eachother.
return int32_t(value - rhs.value) <=> 0;
return int32_t(value - rhs.value) < 0;
#endif
}
constexpr bool operator<=(const InternalVersionT &rhs) const {
#if USE_64_BIT
return value <= rhs.value;
#else
// Maintains ordering after overflow, as long as the full-precision versions
// are within `kMaxCorrectVersionWindow` of eachother.
return int32_t(value - rhs.value) <= 0;
#endif
}
constexpr bool operator>=(const InternalVersionT &rhs) const {
#if USE_64_BIT
return value >= rhs.value;
#else
// Maintains ordering after overflow, as long as the full-precision versions
// are within `kMaxCorrectVersionWindow` of eachother.
return int32_t(value - rhs.value) >= 0;
#endif
}
constexpr bool operator>(const InternalVersionT &rhs) const {
#if USE_64_BIT
return value > rhs.value;
#else
// Maintains ordering after overflow, as long as the full-precision versions
// are within `kMaxCorrectVersionWindow` of eachother.
return int32_t(value - rhs.value) > 0;
#endif
}
constexpr bool operator==(const InternalVersionT &) const = default;
@@ -307,7 +336,9 @@ struct Node {
uint8_t *partialKey();
Type getType() const { return type; }
int32_t getCapacity() const {
assert(!releaseDeferred);
// `partialKeyCapacity` is not repurposed by `deferRelease`, so this is also
// valid for nodes with `releaseDeferred` set. It's needed to account for
// the capacity of a deferred node when it's finally released.
return partialKeyCapacity;
}
@@ -2280,6 +2311,8 @@ bool checkMaxBetweenExclusiveImpl(Node3 *n, int begin, int end,
assert(!(begin == -1 && end == 256));
auto *self = static_cast<Node3 *>(n);
assume(1 <= self->numChildren);
assume(self->numChildren <= 3);
++begin;
@@ -2753,10 +2786,39 @@ checkMaxBetweenExclusiveImpl<true>(Node256 *n, int begin, int end,
return self;
}
#if SHOW_MEMORY
// Length of the key leading to `child`, given the length of the key leading to
// its parent. Consistent with `getSearchPathLength`.
int64_t childSearchPathLength(int64_t parentSearchPathLength, Node *child) {
return parentSearchPathLength + 1 + child->partialKeyLen;
}
#endif
void eraseTree(Node *root, WriteContext *writeContext) {
Arena arena;
auto toFree = vector<Node *>(arena);
toFree.push_back(root);
// Length of the key leading to each node in `toFree`, on a parallel stack.
// A node is released before its children are visited, so the `parent`
// pointers that `getSearchPathLength` walks are dangling by the time a queued
// node is popped. Carry the lengths along instead. Only the SHOW_MEMORY
// accounting needs them.
#if SHOW_MEMORY
auto searchPathLengths = vector<int64_t>(arena);
searchPathLengths.push_back(getSearchPathLength(root));
#endif
// Queue `child` for release, where `parentSearchPathLength` is the length of
// the key leading to its parent.
auto queueChild = [&](Node *child, int64_t parentSearchPathLength) {
toFree.push_back(child);
#if SHOW_MEMORY
searchPathLengths.push_back(
childSearchPathLength(parentSearchPathLength, child));
#else
(void)parentSearchPathLength;
#endif
};
while (toFree.size() > 0) {
auto *n = toFree.back();
@@ -2764,7 +2826,13 @@ void eraseTree(Node *root, WriteContext *writeContext) {
writeContext->accum.entries_erased += n->entryPresent;
++writeContext->accum.nodes_released;
removeKey(n);
#if SHOW_MEMORY
const int64_t searchPathLength = searchPathLengths.back();
searchPathLengths.pop_back();
#else
const int64_t searchPathLength = 0;
#endif
removeKey(n, searchPathLength);
switch (n->getType()) {
case Type_Node0: {
@@ -2774,28 +2842,39 @@ void eraseTree(Node *root, WriteContext *writeContext) {
case Type_Node3: {
auto *n3 = static_cast<Node3 *>(n);
for (int i = 0; i < n3->numChildren; ++i) {
toFree.push_back(n3->children[i]);
queueChild(n3->children[i], searchPathLength);
}
writeContext->release(n3);
} break;
case Type_Node16: {
auto *n16 = static_cast<Node16 *>(n);
for (int i = 0; i < n16->numChildren; ++i) {
toFree.push_back(n16->children[i]);
queueChild(n16->children[i], searchPathLength);
}
writeContext->release(n16);
} break;
case Type_Node48: {
auto *n48 = static_cast<Node48 *>(n);
for (int i = 0; i < n48->numChildren; ++i) {
toFree.push_back(n48->children[i]);
queueChild(n48->children[i], searchPathLength);
}
writeContext->release(n48);
} break;
case Type_Node256: {
auto *n256 = static_cast<Node256 *>(n);
auto *out = toFree.unsafePrepareAppend(n256->numChildren).data();
n256->bitSet.forEachSet([&](int i) { *out++ = n256->children[i]; });
#if SHOW_MEMORY
auto *outSearchPathLengths =
searchPathLengths.unsafePrepareAppend(n256->numChildren).data();
#endif
n256->bitSet.forEachSet([&](int i) {
Node *child = n256->children[i];
*out++ = child;
#if SHOW_MEMORY
*outSearchPathLengths++ =
childSearchPathLength(searchPathLength, child);
#endif
});
assert(out == toFree.end());
writeContext->release(n256);
} break;
@@ -2907,9 +2986,13 @@ void fixupMaxVersion(Node *node, WriteContext *writeContext) {
break;
case Type_Node3: {
auto *self3 = static_cast<Node3 *>(node);
max = std::max(max,
horizontalMaxUpTo16(self3->childMaxVersion,
writeContext->zero, self3->numChildren));
assume(1 <= self3->numChildren);
assume(self3->numChildren <= 3);
InternalVersionT m = self3->childMaxVersion[0];
for (int i = 1; i < self3->numChildren; ++i) {
m = std::max(m, self3->childMaxVersion[i]);
}
max = std::max(max, m);
} break;
case Type_Node16: {
auto *self16 = static_cast<Node16 *>(node);
@@ -5463,8 +5546,9 @@ void internal_addWrites(ConflictSet::Impl *impl,
impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY
// Testing-only cross-check of the accounting.
if (impl->totalBytes != mallocBytes) {
abort();
abort(); // GCOVR_EXCL_LINE
}
#endif
}
@@ -5475,8 +5559,9 @@ void internal_setOldestVersion(ConflictSet::Impl *impl, int64_t oldestVersion) {
impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY
// Testing-only cross-check of the accounting.
if (impl->totalBytes != mallocBytes) {
abort();
abort(); // GCOVR_EXCL_LINE
}
#endif
}
@@ -5942,6 +6027,14 @@ checkMaxVersion(Node *root, Node *node, InternalVersionT oldestVersion,
} // namespace
// The SHOW_MEMORY instrumentation. Each of these functions is defined exactly
// once, with the accounting compiled out when SHOW_MEMORY is disabled. Defining
// them once (rather than defining no-op stubs when SHOW_MEMORY is disabled)
// keeps every configuration of this file reporting each function on the same
// line, which merging coverage from objects compiled with different options
// requires: gcovr, merging the SHOW_MEMORY test with the rest of the build,
// otherwise aborts with "Got function ... on multiple lines". The optimizer
// removes the resulting empty calls.
#if SHOW_MEMORY
int64_t nodeBytes = 0;
@@ -5970,7 +6063,10 @@ int64_t getNodeSize(struct Node *n) {
}
}
#endif
int64_t getSearchPathLength(Node *n) {
#if SHOW_MEMORY
assert(n != nullptr);
int64_t result = 0;
for (;;) {
@@ -5982,9 +6078,14 @@ int64_t getSearchPathLength(Node *n) {
n = n->parent;
}
return result;
#else
(void)n;
return 0;
#endif
}
void addNode(Node *n) {
#if SHOW_MEMORY
nodeBytes += getNodeSize(n);
partialCapacityBytes += n->getCapacity();
if (nodeBytes > peakNodeBytes) {
@@ -5993,14 +6094,22 @@ void addNode(Node *n) {
if (partialCapacityBytes > peakPartialCapacityBytes) {
peakPartialCapacityBytes = partialCapacityBytes;
}
#else
(void)n;
#endif
}
void removeNode(Node *n) {
#if SHOW_MEMORY
nodeBytes -= getNodeSize(n);
partialCapacityBytes -= n->getCapacity();
#else
(void)n;
#endif
}
void addKey(Node *n) {
#if SHOW_MEMORY
if (!n->entryPresent) {
++totalKeys;
keyBytes += getSearchPathLength(n);
@@ -6011,15 +6120,36 @@ void addKey(Node *n) {
peakKeyBytes = keyBytes;
}
}
#else
(void)n;
#endif
}
void removeKey(Node *n) {
#if SHOW_MEMORY
if (n->entryPresent) {
--totalKeys;
keyBytes -= getSearchPathLength(n);
}
#else
(void)n;
#endif
}
void removeKey(Node *n, int64_t searchPathLength) {
#if SHOW_MEMORY
if (n->entryPresent) {
--totalKeys;
keyBytes -= searchPathLength;
}
#else
(void)n;
(void)searchPathLength;
#endif
}
#if SHOW_MEMORY
struct __attribute__((visibility("default"))) PeakPrinter {
~PeakPrinter() {
printf("--- radix_tree ---\n");
+7
View File
@@ -23,7 +23,9 @@ using namespace weaselab;
#include <callgrind.h>
#define DEBUG_VERBOSE 0
#ifndef SHOW_MEMORY
#define SHOW_MEMORY 0
#endif
// std::span is not trivially constructible. We want a span that leaves its
// members uninitialized for performance reasons.
@@ -96,6 +98,11 @@ static_assert(std::is_trivial_v<TrivialSpan>);
#if SHOW_MEMORY
inline int64_t mallocBytes = 0;
inline int64_t peakMallocBytes = 0;
// Defined in ConflictSet.cpp
extern int64_t nodeBytes;
extern int64_t partialCapacityBytes;
extern int64_t totalKeys;
extern int64_t keyBytes;
#endif
inline thread_local int64_t mallocBytesDelta = 0;
+71 -3
View File
@@ -96,7 +96,7 @@ inline int firstNeqStride(const uint8_t *ap, const uint8_t *bp) {
// GCOVR_EXCL_START
#if defined(HAS_AVX) && !defined(__SANITIZE_THREAD__)
__attribute__((target("avx512f,avx512bw"))) inline int
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
longestCommonPrefixLong(const uint8_t *ap, const uint8_t *bp, int cl) {
int i = 0;
int end = cl & ~63;
while (i < end) {
@@ -123,7 +123,7 @@ __attribute__((target("default")))
// GCOVR_EXCL_STOP
inline int
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
longestCommonPrefixLong(const uint8_t *ap, const uint8_t *bp, int cl) {
if (!(cl >= 0)) {
__builtin_unreachable(); // GCOVR_EXCL_LINE
}
@@ -182,4 +182,72 @@ longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
}
return i;
}
}
// Use in-bounds native-word loads for short common lengths. The second load
// overlaps the first; because the first load matched, any mismatch found in
// the overlap is impossible and the returned absolute index is still correct.
// Precondition: cl <= 2 * sizeof(uint64_t)
__attribute__((always_inline)) static inline int
longestCommonPrefixShort(const uint8_t *ap, const uint8_t *bp, int cl) {
assert(cl <= 2 * int(sizeof(uint64_t)));
if (cl >= 8) {
uint64_t a0;
uint64_t b0;
memcpy(&a0, ap, sizeof(a0));
memcpy(&b0, bp, sizeof(b0));
if (uint64_t mismatched = a0 ^ b0) {
return std::countr_zero(mismatched) / 8;
}
if (cl == 8) {
return 8;
}
uint64_t a1;
uint64_t b1;
memcpy(&a1, ap + cl - 8, sizeof(a1));
memcpy(&b1, bp + cl - 8, sizeof(b1));
if (uint64_t mismatched = a1 ^ b1) {
return cl - 8 + std::countr_zero(mismatched) / 8;
}
return cl;
}
if (cl >= 4) {
uint32_t a0;
uint32_t b0;
memcpy(&a0, ap, sizeof(a0));
memcpy(&b0, bp, sizeof(b0));
if (uint32_t mismatched = a0 ^ b0) {
return std::countr_zero(mismatched) / 8;
}
if (cl == 4) {
return 4;
}
uint32_t a1;
uint32_t b1;
memcpy(&a1, ap + cl - 4, sizeof(a1));
memcpy(&b1, bp + cl - 4, sizeof(b1));
if (uint32_t mismatched = a1 ^ b1) {
return cl - 4 + std::countr_zero(mismatched) / 8;
}
return cl;
}
int i = 0;
while (i < cl && ap[i] == bp[i]) {
++i;
}
return i;
}
__attribute__((always_inline)) static inline int
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
if (!(cl >= 0)) {
__builtin_unreachable(); // GCOVR_EXCL_LINE
}
if (cl <= 2 * int(sizeof(uint64_t))) {
return longestCommonPrefixShort(ap, bp, cl);
}
return longestCommonPrefixLong(ap, bp, cl);
}
+21 -21
View File
@@ -19,30 +19,30 @@ InstalledDir: /usr/lib/llvm-21/bin
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
| 164.29 | 6,086,873.38 | 0.0% | 3,107.03 | 604.19 | 5.142 | 558.59 | 0.0% | 1.96 | `point reads`
| 161.05 | 6,209,395.38 | 0.1% | 3,036.76 | 592.21 | 5.128 | 539.35 | 0.0% | 1.93 | `prefix reads`
| 239.55 | 4,174,539.38 | 0.1% | 3,722.71 | 880.68 | 4.227 | 692.00 | 0.0% | 2.86 | `range reads`
| 354.75 | 2,818,919.14 | 0.7% | 4,523.64 | 1,304.75 | 3.467 | 720.22 | 2.0% | 4.23 | `point writes`
| 345.32 | 2,895,878.47 | 0.1% | 4,484.57 | 1,270.31 | 3.530 | 705.00 | 1.8% | 4.12 | `prefix writes`
| 193.48 | 5,168,547.42 | 0.1% | 2,224.10 | 711.72 | 3.125 | 377.17 | 3.3% | 2.32 | `range writes`
| 404.89 | 2,469,777.50 | 2.4% | 6,855.96 | 1,489.70 | 4.602 | 1,227.82 | 1.3% | 0.05 | `monotonic increasing point writes`
| 134,231.80 | 7,449.80 | 1.9% | 812,045.25 | 495,770.40 | 1.638 | 151,246.50 | 0.9% | 0.01 | `worst case for radix tree`
| 37.80 | 26,454,311.17 | 0.4% | 701.00 | 139.14 | 5.038 | 102.00 | 0.0% | 0.01 | `create and destroy`
| 165.93 | 6,026,520.04 | 0.1% | 3,107.03 | 610.42 | 5.090 | 558.59 | 0.0% | 1.98 | `point reads`
| 163.08 | 6,132,046.57 | 0.1% | 3,036.76 | 599.68 | 5.064 | 539.35 | 0.0% | 1.95 | `prefix reads`
| 250.26 | 3,995,834.21 | 0.1% | 3,722.71 | 920.25 | 4.045 | 692.00 | 0.0% | 2.99 | `range reads`
| 359.64 | 2,780,581.63 | 0.1% | 4,523.64 | 1,322.63 | 3.420 | 720.22 | 2.1% | 4.30 | `point writes`
| 358.20 | 2,791,726.93 | 0.1% | 4,484.57 | 1,317.23 | 3.405 | 705.00 | 2.0% | 4.28 | `prefix writes`
| 193.91 | 5,157,081.29 | 0.1% | 2,224.10 | 713.33 | 3.118 | 377.17 | 3.3% | 2.32 | `range writes`
| 422.03 | 2,369,513.38 | 1.1% | 6,855.96 | 1,551.55 | 4.419 | 1,227.82 | 1.4% | 0.05 | `monotonic increasing point writes`
| 142,467.00 | 7,019.17 | 3.8% | 812,047.33 | 508,535.40 | 1.597 | 151,246.50 | 1.0% | 0.01 | `worst case for radix tree`
| 37.66 | 26,554,217.74 | 0.8% | 701.00 | 138.80 | 5.050 | 102.00 | 0.0% | 0.01 | `create and destroy`
## Radix tree (this implementation)
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
| 12.89 | 77,565,115.56 | 0.1% | 244.55 | 47.43 | 5.155 | 34.21 | 0.6% | 0.15 | `point reads`
| 15.11 | 66,162,047.76 | 0.1% | 297.79 | 55.60 | 5.356 | 43.23 | 0.4% | 0.18 | `prefix reads`
| 36.29 | 27,559,358.29 | 0.1% | 783.16 | 133.44 | 5.869 | 109.52 | 0.2% | 0.43 | `range reads`
| 20.53 | 48,719,405.55 | 0.1% | 381.81 | 75.51 | 5.057 | 51.04 | 0.5% | 0.25 | `point writes`
| 39.37 | 25,402,042.40 | 0.1% | 685.00 | 144.83 | 4.730 | 106.72 | 0.3% | 0.47 | `prefix writes`
| 43.78 | 22,843,841.63 | 0.1% | 800.40 | 161.06 | 4.970 | 127.36 | 0.1% | 0.53 | `range writes`
| 78.37 | 12,760,008.75 | 1.0% | 1,452.61 | 288.24 | 5.040 | 278.69 | 0.1% | 0.01 | `monotonic increasing point writes`
| 322,885.50 | 3,097.07 | 1.5% | 4,362,382.00 | 1,183,852.00 | 3.685 | 765,301.00 | 0.1% | 0.01 | `worst case for radix tree`
| 99.99 | 10,000,718.79 | 0.4% | 1,775.00 | 367.93 | 4.824 | 288.00 | 0.0% | 0.01 | `create and destroy`
| 12.25 | 81,664,669.29 | 0.4% | 233.49 | 44.94 | 5.196 | 31.23 | 0.7% | 0.15 | `point reads`
| 13.35 | 74,903,950.49 | 0.1% | 271.75 | 49.10 | 5.534 | 40.21 | 0.4% | 0.16 | `prefix reads`
| 31.37 | 31,874,583.62 | 0.1% | 700.39 | 115.43 | 6.067 | 100.83 | 0.2% | 0.37 | `range reads`
| 19.55 | 51,153,057.80 | 0.1% | 366.51 | 71.90 | 5.098 | 47.92 | 0.5% | 0.23 | `point writes`
| 36.92 | 27,088,344.17 | 0.1% | 645.11 | 135.72 | 4.753 | 100.31 | 0.4% | 0.44 | `prefix writes`
| 36.84 | 27,143,553.08 | 0.1% | 691.04 | 135.42 | 5.103 | 114.01 | 0.1% | 0.45 | `range writes`
| 74.62 | 13,400,467.72 | 0.9% | 1,418.37 | 274.20 | 5.173 | 273.39 | 0.1% | 0.01 | `monotonic increasing point writes`
| 352,671.00 | 2,835.50 | 15.3% | 3,687,535.00 | 1,287,119.00 | 2.865 | 712,030.00 | 0.1% | 0.01 | :wavy_dash: `worst case for radix tree` (Unstable with ~1.0 iters. Increase `minEpochIterations` to e.g. 10)
| 96.63 | 10,348,729.42 | 1.2% | 1,766.00 | 355.53 | 4.967 | 283.00 | 0.0% | 0.01 | `create and destroy`
# "Real data" test
@@ -51,13 +51,13 @@ Point queries only. Gc ratio is the ratio of time spent doing garbage collection
## skip list
```
Check: 4.62967 seconds, 352.195 MB/s, Add: 3.34177 seconds, 167.771 MB/s, Gc ratio: 37.9399%, Peak idle memory: 5.51852e+06
Check: 4.69875 seconds, 347.017 MB/s, Add: 3.39475 seconds, 165.153 MB/s, Gc ratio: 38.7414%, Peak idle memory: 5.51852e+06
```
## radix tree
```
Check: 1.00477 seconds, 1622.8 MB/s, Add: 1.21142 seconds, 462.808 MB/s, Gc ratio: 39.4716%, Peak idle memory: 2.0226e+06
Check: 0.976745 seconds, 1669.37 MB/s, Add: 1.20464 seconds, 465.411 MB/s, Gc ratio: 39.4158%, Peak idle memory: 2.0226e+06
```
## hash table
@@ -65,6 +65,6 @@ Check: 1.00477 seconds, 1622.8 MB/s, Add: 1.21142 seconds, 462.808 MB/s, Gc rati
(The hash table implementation doesn't work on range queries, and its purpose is to provide an idea of how fast point queries can be)
```
Check: 0.854254 seconds, 1908.74 MB/s, Add: 0.632626 seconds, 886.232 MB/s, Gc ratio: 41.0827%, Peak idle memory: 0
Check: 0.864589 seconds, 1885.92 MB/s, Add: 0.634604 seconds, 883.47 MB/s, Gc ratio: 41.184%, Peak idle memory: 0
```
+115
View File
@@ -0,0 +1,115 @@
// Exercises the SHOW_MEMORY instrumentation, which the other tests don't
// compile with (this target passes -DSHOW_MEMORY=1). The instrumentation has
// its own consistency checks, which abort on failure, and asserts are enabled
// by TEST_FLAGS. Only one ConflictSet may exist at a time, since the
// accounting is process-wide.
#include "ConflictSet.h"
#include "Internal.h"
#include <cassert>
#include <cstdint>
#include <string>
using namespace weaselab;
namespace {
void writePoint(ConflictSet &cs, const std::string &key, int64_t writeVersion) {
ConflictSet::WriteRange w{{(const uint8_t *)key.data(), int(key.size())},
{nullptr, 0}};
cs.addWrites(&w, 1, writeVersion);
}
ConflictSet::Result checkPoint(const ConflictSet &cs, const std::string &key,
int64_t readVersion) {
ConflictSet::ReadRange r{{(const uint8_t *)key.data(), int(key.size())},
{nullptr, 0},
readVersion};
ConflictSet::Result result;
cs.check(&r, &result, 1);
return result;
}
// Everything a ConflictSet allocated must have been accounted for and released
// by the time it's destroyed.
void checkFullyReleased() {
assert(nodeBytes == 0);
assert(partialCapacityBytes == 0);
assert(totalKeys == 0);
assert(keyBytes == 0);
assert(mallocBytes == 0);
}
} // namespace
int main() {
// Splitting a node replaces it, which defers the release of the old node.
// Releasing a deferred node must account for its capacity, and the key it
// held must be accounted for without walking `parent` pointers of nodes that
// have already been released.
{
ConflictSet cs{0};
writePoint(cs, "a", 1);
writePoint(cs, "ab", 2);
assert(checkPoint(cs, "a", 0) == ConflictSet::Conflict);
assert(checkPoint(cs, "ab", 0) == ConflictSet::Conflict);
assert(checkPoint(cs, "b", 0) == ConflictSet::Commit);
assert(nodeBytes > 0);
assert(keyBytes > 0);
}
checkFullyReleased();
// Exceeding the version window erases the tree mid-workload.
{
ConflictSet cs{0};
writePoint(cs, "abc", 1);
const int64_t version = int64_t(1) << 40;
writePoint(cs, "abc", version);
assert(checkPoint(cs, "abc", 0) == ConflictSet::TooOld);
assert(checkPoint(cs, "abc", version) == ConflictSet::Commit);
}
checkFullyReleased();
// Erase a tree with many nodes, both mid-workload and at destruction.
{
ConflictSet cs{0};
for (int i = 0; i < 1000; ++i) {
writePoint(cs, "prefix/" + std::to_string(i), i + 1);
}
const int64_t version = int64_t(1) << 40;
writePoint(cs, "prefix/1", version);
assert(checkPoint(cs, "prefix/1", version) == ConflictSet::Commit);
for (int i = 0; i < 1000; ++i) {
writePoint(cs, "other/" + std::to_string(i), version + i + 1);
}
assert(checkPoint(cs, "other/999", version) == ConflictSet::Conflict);
}
checkFullyReleased();
// Grow a node through every node type, so that the accounting for each node
// type's size is exercised, then erase the entries one at a time so that
// nodes are released individually rather than as part of a whole tree.
{
ConflictSet cs{0};
std::string key(1, '\0');
for (int i = 0; i < 256; ++i) {
key[0] = char(i);
writePoint(cs, key, i + 1);
}
for (int i = 0; i < 256; ++i) {
key[0] = char(i);
assert(checkPoint(cs, key, i) == ConflictSet::Conflict);
}
// Garbage collection erases entries that can only fail with TooOld anyway.
// Doing it one version at a time walks the tree, erasing, downsizing and
// releasing nodes as it goes.
for (int i = 0; i < 256; ++i) {
cs.setOldestVersion(i + 1);
}
key[0] = '\0';
assert(checkPoint(cs, key, 0) == ConflictSet::TooOld);
}
checkFullyReleased();
}
+8 -1
View File
@@ -819,10 +819,17 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
}
void setOldestVersion(int64_t oldestVersion) {
// oldestVersion becomes the max of its previous value and the argument, so
// an older version is a no-op. We still fall through in the equal case so
// that garbage collection can make progress without advancing the oldest
// version.
if (oldestVersion < this->oldestVersion) {
return;
}
// This isn't 100% accurate. It overcounts if you hit the end
gc_iterations_total.add(keyUpdates);
assert(oldestVersion >= this->oldestVersion);
this->oldestVersion = oldestVersion;
SkipList::Finger finger;
int temp;
+39 -5
View File
@@ -68,6 +68,35 @@ def test_hash_table_getBytes():
assert cs.check(read(0, b"key")) == [Result.CONFLICT]
def test_setOldestVersion_does_not_decrease():
# Regression test for issue #87: `setOldestVersion` sets oldestVersion to
# the maximum of its previous value and its argument, so passing an older
# version is valid input and must be a no-op. skip_list used to assert in
# Debug builds and, in Release builds, lower its oldest version, which
# resurrected a window of versions that callers were told resulted in
# TooOld.
with DebugConflictSet() as cs:
cs.addWrites(5, write(b"k"))
cs.setOldestVersion(5)
# Valid: `2 >= 0` and `2 <= newestVersion == 5`. oldestVersion stays 5,
# so 3 and 4 are still TooOld.
cs.setOldestVersion(2)
assert cs.check(read(3, b"k")) == [Result.TOO_OLD]
assert cs.check(read(4, b"k")) == [Result.TOO_OLD]
assert cs.check(read(5, b"k")) == [Result.COMMIT]
# Same, but with the entries at the old oldestVersion already garbage
# collected, so lowering oldestVersion would report Commit for a version
# that the caller was told resulted in TooOld.
with DebugConflictSet() as cs:
cs.addWrites(1, write(b"a"))
cs.addWrites(2) # No writes; advances newestVersion to 2
cs.setOldestVersion(2) # Garbage collects the write at version 1
cs.setOldestVersion(1)
assert cs.check(read(1, b"a")) == [Result.TOO_OLD]
assert cs.check(read(2, b"a")) == [Result.COMMIT]
def test_hash_table_setOldestVersion_empties_map():
# Regression test for issue #78: setOldestVersion must terminate when
# its garbage-collection pass erases every entry in the map. Each write
@@ -222,13 +251,18 @@ def test_merge_child_node48():
cs.addWrites(1, write(b"\x00" * 8, b"\x00" * 10))
def test_fixup_256():
def test_fixup_256_heterogeneous_versions():
# Regression test for issue #89: recomputing the maxVersion of a Node256
# must consider all of its children, not just those with an index < 16.
# Here the highest-version child ("\x10\x50") has an index >= 16, and the
# range write ends at the Node256, which erases that child from the range
# and recomputes the Node256's maxVersion.
with DebugConflictSet() as cs:
cs.addWrites(0, write(bytes([1])))
for i in range(256):
cs.addWrites(1, write(bytes([1, i])))
cs.addWrites(2, write(bytes([0]), bytes([1])))
cs.check(read(0, bytes([1]), bytes([2])))
cs.addWrites(1, write(bytes([0x10, i])))
cs.addWrites(10, write(bytes([0x10, 0x50])))
cs.addWrites(11, write(bytes([0x05]), bytes([0x10])))
assert cs.check(read(5, bytes([0x10, 0x50]))) == [Result.CONFLICT]
def test_large_removal_buffer():
+2 -1
View File
@@ -1,7 +1,8 @@
#!/bin/bash
# Disable frequency scaling
for i in $(seq "$(nproc)") ; do sudo cat /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_max_freq | sudo tee /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_min_freq >/dev/null ; done
# shellcheck disable=SC2002
for i in $(seq "$(nproc)") ; do cat /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_max_freq | sudo tee /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_min_freq >/dev/null ; done
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_FLAGS=-DNVALGRIND > /dev/null
cmake --build build > /dev/null