bench: add index benchmarks #35827

pull arejula27 wants to merge 3 commits into bitcoin:master from arejula27:index-sync-bench-infra changing 7 files +365 −0
  1. arejula27 commented at 7:53 PM on July 27, 2026: contributor

    There is a fair amount of work going into the indices at the moment, and no cheap way to tell whether a change actually improves what it set out to improve: checking end to end means a full IBD. The only index benchmark in the tree, BlockFilterIndexSync, syncs coinbase-only blocks, so it cannot show anything that scales with what a block contains, and there is none at all for TxIndex or TxoSpenderIndex.

    This adds shared helpers in src/bench/index_sync_util.{h,cpp} that build a chain of blocks with chained, validly-signed transactions (~2 inputs and ~2 outputs each, every output in a block paying a distinct key), and on top of them, for txindex, txospenderindex and blockfilterindex:

    • <Index>SyncDisk / <Index>SyncMem — full sync with the index database on the filesystem, and in memory.
    • <Index>Lookup — one query per indexed key (FindTx, FindSpender).

    How far apart Disk and Mem land depends on the data directory, which defaults to a tmpfs on many Linux systems, where both variants write to RAM and report about the same number. Pointing -testdatadir at real storage makes Disk pay real writes. The index stays in LevelDB's write buffer at this chain size, so the difference is mostly the synced writes that create the database (Linux, btrfs on NVMe, default 50x50 chain, median of 3 runs):

    Benchmark tmpfs (default) btrfs via -testdatadir
    TxIndexSyncDisk 6.1 ms 34.8 ms
    TxIndexSyncMem 5.4 ms 4.5 ms
    TxIndexLookup 5.9 us 5.9 us
    TxoSpenderIndexSyncDisk 5.5 ms 36.7 ms
    TxoSpenderIndexSyncMem 5.4 ms 5.4 ms
    TxoSpenderIndexLookup 6.8 us 6.3 us
    BlockFilterIndexSyncRealisticDisk 8.4 ms 41.8 ms
    BlockFilterIndexSyncRealisticMem 7.8 ms 15.6 ms

    The lookups have a single variant, and barely move between the two columns: the database answers them from the write buffer either way, and the block file they read lives in the data directory whatever f_memory says.

    The Mem rows barely moving is the check that MemEnv really keeps the database off the filesystem. Blockfilter is the exception, because its filters go to the data directory through m_filter_fileseq whatever f_memory says, so both of its variants pay that write.

    BenchIndexSync() also forces a chainstate flush, without which BaseIndex::Commit() returns early and the commit path stays out of the measurement.

    The existing BlockFilterIndexSync is left untouched: it scales with the number of blocks over near-empty filters, the new one with what a block contains. The suite runs in about 15 s.

    A last commit documents in doc/benchmarking.md how to measure syscalls, on-disk size and peak memory around bench_bitcoin, which nanobench does not report.

    I have used this on those two pull requests, and in both the interesting number was one the existing benchmark could not produce: #35531 (5-byte siphash keys) trades a 49% smaller index on disk for a lookup regression that grows with index size, and #34489 (batch db writes) cuts write syscalls by 91% with CPU unchanged. The numbers are in those threads.

    Disclosure: I used claude to help me in some parts, however all code has been verified by me and I ran the bench manually on the scenarios mentioned.

  2. DrahtBot added the label Tests on Jul 27, 2026
  3. DrahtBot commented at 7:53 PM on July 27, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK l0rinc

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. arejula27 commented at 8:04 PM on July 27, 2026: contributor

    I think this PR can be splited into two, one for the doc and another for the index benchmarks

  5. in src/bench/index_blockfilter.cpp:70 in 6bf3b13e53 outdated
      65 | +// Returns a fresh, not-yet-initialized BASIC BlockFilterIndex. `f_memory=false`
      66 | +// exercises the real disk write path, true isolates CPU cost from I/O.
      67 | +static std::unique_ptr<BlockFilterIndex> MakeBlockFilterIndex(TestChain100Setup& test_setup, bool f_memory)
      68 | +{
      69 | +    return std::make_unique<BlockFilterIndex>(interfaces::MakeChain(test_setup.m_node), BlockFilterType::BASIC,
      70 | +                                              /*n_cache_size=*/1 << 20, f_memory, /*f_wipe=*/true);
    


    l0rinc commented at 8:10 PM on July 27, 2026:

    nit: you can use 1_MiB

  6. in src/bench/index_sync_util.cpp:49 in 6bf3b13e53 outdated
      44 | +    // Coinbase from block 1 matures exactly when spent at height 101 (chain
      45 | +    // tip is at height 100 right after TestChain100Setup).
      46 | +    auto& coinbase_to_spend{test_setup.m_coinbase_txns[0]};
      47 | +    size_t k0{0};
      48 | +    size_t k1{1};
      49 | +    const CMutableTransaction first_tx{test_setup.CreateValidTransaction({coinbase_to_spend}, {COutPoint(coinbase_to_spend->GetHash(), 0)},
    


    l0rinc commented at 8:11 PM on July 27, 2026:

    I don't usually mind long lines but formatting is a bit off on GitHub

  7. in src/bench/index_sync_util.h:25 in 6bf3b13e53 outdated
      20 | +/**
      21 | + * Size of the chain built by the index benchmarks. Building it is not timed but
      22 | + * dominates the wall time of a run, so keep these just large enough for the
      23 | + * timed part to stand out of the noise.
      24 | + */
      25 | +static constexpr size_t BENCH_INDEX_NUM_BLOCKS{50};
    


    l0rinc commented at 8:12 PM on July 27, 2026:

    we should avoid adding new arch dependent types if possible


    arejula27 commented at 5:11 PM on July 28, 2026:

    mb

  8. in src/bench/index_sync_util.h:59 in 6bf3b13e53 outdated
      54 | +        assert(!index->BlockUntilSyncedToCurrentChain());
      55 | +        index->Sync();
      56 | +
      57 | +        IndexSummary summary{index->GetSummary()};
      58 | +        assert(summary.synced);
      59 | +        assert(summary.best_block_hash == WITH_LOCK(::cs_main, return test_setup.m_node.chainman->ActiveTip()->GetBlockHash()));
    


    l0rinc commented at 8:13 PM on July 27, 2026:

    locking inside the measured block will likely distort the results. Consider precalculating and maybe using setup block

  9. in src/bench/index_sync_util.h:97 in 6bf3b13e53 outdated
      92 | +    assert(!keys.empty());
      93 | +    bench.batch(keys.size()).unit("lookup").run([&] {
      94 | +        for (const Key& key : keys) {
      95 | +            const bool ok{lookup_one(key)};
      96 | +            assert(ok);
      97 | +            ankerl::nanobench::doNotOptimizeAway(ok);
    


    l0rinc commented at 8:14 PM on July 27, 2026:

    either assert or doNotOptimizeAway, no need for both. Alternatively have a bool at the begining, and each value and assert at the end

  10. in src/bench/index_txindex.cpp:38 in 6bf3b13e53 outdated
      33 | +}
      34 | +
      35 | +static void TxIndexSyncDisk(benchmark::Bench& bench) { TxIndexSync(bench, /*f_memory=*/false); }
      36 | +static void TxIndexSyncMem(benchmark::Bench& bench) { TxIndexSync(bench, /*f_memory=*/true); }
      37 | +BENCHMARK(TxIndexSyncDisk);
      38 | +BENCHMARK(TxIndexSyncMem);
    


    l0rinc commented at 8:15 PM on July 27, 2026:

    I'd prefer having all of these at the very end instead

  11. in src/bench/index_txindex.cpp:49 in 6bf3b13e53 outdated
      44 | +{
      45 | +    const auto test_setup = MakeNoLogFileContext<TestChain100Setup>();
      46 | +    ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK);
      47 | +
      48 | +    // Build and fully sync a persistent txindex, kept alive for the lookups.
      49 | +    auto index{MakeTxIndex(*test_setup, /*f_memory=*/false)};
    


    l0rinc commented at 8:15 PM on July 27, 2026:

    these are likely small, so even if they're not in memory, the OS will likely make sure they're still actually in memory...


    arejula27 commented at 5:04 PM on July 28, 2026:

    Thats a problem i am having with this PR, based on my results looks like Im not using the disk, not sure if it is only the size or am i doing something wrong elsewhere


    l0rinc commented at 5:19 PM on July 28, 2026:

    You can try invalidating the OS file caches


    arejula27 commented at 5:57 PM on July 28, 2026:

    I didn't find a way to do it from inside the benchmark itself, only things the user can do outside the process, but I don't know this area well enough. Do you know a simpler way?

    Also I noticed the writes are not fsynced (fSync=false in BaseIndex::Commit) and the index is smaller than the write buffer, so it never leaves the memtable. I'll try with a bigger chain and see what happens


    l0rinc commented at 10:37 AM on November 2, 2026:

    Thanks for checking, please resolve.

  12. in src/bench/index_txospender.cpp:56 in 6bf3b13e53 outdated
      51 | +
      52 | +    const std::vector<COutPoint> outpoints{CollectChainSpentOutpoints(*test_setup, BENCH_INDEX_FIRST_SPEND_HEIGHT)};
      53 | +
      54 | +    BenchIndexLookup(bench, outpoints, [&](const COutPoint& txo) {
      55 | +        const auto result{index->FindSpender(txo)};
      56 | +        return result && result->has_value();
    


    l0rinc commented at 8:16 PM on July 27, 2026:

    aren't these two conditions exactly the same?


    arejula27 commented at 5:34 PM on July 28, 2026:
     /**
         * Search the index for a transaction that spends the given outpoint.
         *
         * [@param](/bitcoin-bitcoin/contributor/param/)[in] txo  The outpoint to search for.
         *
         * [@return](/bitcoin-bitcoin/contributor/return/)  std::nullopt               if the outpoint has not been spent on-chain.
         *          std::optional{TxoSpender}  if the output has been spent on-chain. Contains the spending transaction
         *                                     and the block it was confirmed in.
         *          util::Unexpected{error}    if something unexpected happened (i.e. disk or deserialization error).
         */
        util::Expected<std::optional<TxoSpender>, std::string> FindSpender(const COutPoint& txo) const;
    

    FindSpender returns util::Expected<std::optional<TxoSpender>, std::string>, which might be complex read. My understanding:

    • The first check is "no read error".
    • The second check is "a spender was found".

    How do you understand it?


    l0rinc commented at 10:36 AM on November 2, 2026:

    Indeed, I missed the nested optional it seems, thanks for clarifying, please resolve.

  13. l0rinc commented at 8:17 PM on July 27, 2026: contributor

    Concept ACK

    Thanks for taking care of these, I will review them later in more detail, just quickly glanced at the structure.

  14. arejula27 force-pushed on Jul 27, 2026
  15. DrahtBot added the label CI failed on Jul 29, 2026
  16. maflcko removed the label CI failed on Jul 30, 2026
  17. DrahtBot added the label CI failed on Jul 30, 2026
  18. DrahtBot commented at 7:52 PM on July 30, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/30384384298/job/90985587575</sub> <sub>LLM reason (✨ experimental): CI failed because IWYU detected missing/incorrect #includes and generated a failure (Failure generated from IWYU) after applying include changes.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  19. arejula27 force-pushed on Jul 30, 2026
  20. DrahtBot removed the label CI failed on Aug 17, 2026
  21. arejula27 force-pushed on Sep 6, 2026
  22. arejula27 commented at 4:25 PM on September 6, 2026: contributor

    Invalidating the OS caches wasn't the real issue but the data directory defaults to a tmpfs. So f_memory=false was writing to RAM either way. With -testdatadir on real storage the Disk/Mem split shows up on its own, around 2.7x for txindex. The helper documents that now, since the default hides it.

    Digging into that I also found the benchmarks never reached BaseIndex::Commit(), a test setup never flushes the chainstate, so its guard always returned early and the commit path stayed out of the measurement. ExtendChainWithSpends() forces a flush now.

    The rest of your comments are applied, thank you for the review @l0rinc

  23. arejula27 force-pushed on Sep 6, 2026
  24. DrahtBot added the label CI failed on Sep 18, 2026
  25. DrahtBot commented at 2:26 PM on September 18, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/34046169078/job/105615382042</sub> <sub>LLM reason (✨ experimental): CI failed because IWYU reported an include/headers issue (it auto-fixed includes and then intentionally failed the job).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  26. arejula27 force-pushed on Sep 18, 2026
  27. arejula27 force-pushed on Sep 18, 2026
  28. DrahtBot removed the label CI failed on Sep 18, 2026
  29. sedited requested review from andrewtoth on Oct 31, 2026
  30. sedited requested review from l0rinc on Oct 31, 2026
  31. in doc/benchmarking.md:87 in 2c97a9ad93
      82 | +The data directory defaults to a temporary directory, often a RAM-backed
      83 | +`tmpfs` where nothing reaches a real disk. Point it elsewhere first:
      84 | +
      85 | +    build/bin/bench_bitcoin -filter='<benchmark>' -testdatadir=/path/on/disk
      86 | +
      87 | +Write syscalls. A run covers several iterations, so compare between builds
    


    l0rinc commented at 10:24 AM on November 2, 2026:

    2c97a9a doc: describe measuring I/O and memory cost of benchmarks:

    nanobench chooses iteration counts liberally -the aggregate syscall count can otherwise change just because one build ran more syncs: -sanity-check runs one iteration in each build.

    I'd also prefer if we used angle brackets consistently for the fake test data directory values.

    <details> <summary>Trace one iteration in each build</summary>

    diff --git a/doc/benchmarking.md b/doc/benchmarking.md
    index 2e763dffee..24db57c32d 100644
    --- a/doc/benchmarking.md
    +++ b/doc/benchmarking.md
    @@ -82,18 +82,19 @@ tools around `bench_bitcoin`. The commands below are Linux/GNU specific.
     The data directory defaults to a temporary directory, often a RAM-backed
     `tmpfs` where nothing reaches a real disk. Point it elsewhere first:
     
    -    build/bin/bench_bitcoin -filter='<benchmark>' -testdatadir=/path/on/disk
    +    build/bin/bench_bitcoin -filter='<benchmark>' -testdatadir='<testdatadir>'
     
    -Write syscalls. A run covers several iterations, so compare between builds
    -rather than reading the absolute count:
    +Write syscalls. Use `-sanity-check` to keep the traced work fixed at one
    +iteration in each build:
     
         strace -f -c -e trace=write,pwrite64,fsync,fdatasync \
    -        build/bin/bench_bitcoin -filter='<benchmark>' -testdatadir=/path/on/disk
    +        build/bin/bench_bitcoin -sanity-check -filter='<benchmark>' \
    +        -testdatadir='<testdatadir>'
     
     Peak resident memory:
     
         /usr/bin/time -v build/bin/bench_bitcoin -filter='<benchmark>' \
    -        -testdatadir=/path/on/disk 2>&1 | grep 'Maximum resident set size'
    +        -testdatadir='<testdatadir>' 2>&1 | grep 'Maximum resident set size'
     
     On-disk size. This one needs `-testdatadir`: without it the setup deletes the
     data directory on teardown, leaving nothing to measure. Each benchmark gets its
    

    </details>

  32. in src/bench/index_sync_util.cpp:113 in 2c97a9ad93
     108 | +    // CreateAndProcessBlock ignores the result of ProcessNewBlock.
     109 | +    Assert(WITH_LOCK(::cs_main, return test_setup.m_node.chainman->ActiveHeight()) == next_height - 1);
     110 | +
     111 | +    // BaseIndex::Commit() writes nothing until the index tip is an ancestor of
     112 | +    // the last flushed block, and a test setup never flushes on its own.
     113 | +    test_setup.m_node.chainman->ActiveChainstate().ForceFlushStateToDisk();
    


    l0rinc commented at 10:25 AM on November 2, 2026:

    2c97a9a doc: describe measuring I/O and memory cost of benchmarks:

    Could we move the chainstate flush into BenchIndexSync()? BaseIndex::Commit() needs it, while chain construction and lookup-only setup do not. I also corrected the Disk/Mem note to describe the synced database-creation writes without assuming the default temporary directory is RAM-backed.

    <details> <summary>Flush only for sync benchmarks</summary>

    diff --git a/src/bench/index_sync_util.cpp b/src/bench/index_sync_util.cpp
    index c2d9029164..b8c6a242b2 100644
    --- a/src/bench/index_sync_util.cpp
    +++ b/src/bench/index_sync_util.cpp
    @@ -108,10 +108,6 @@ int ExtendChainWithSpends(TestChain100Setup& test_setup, uint32_t num_blocks, ui
         // CreateAndProcessBlock ignores the result of ProcessNewBlock.
         Assert(WITH_LOCK(::cs_main, return test_setup.m_node.chainman->ActiveHeight()) == next_height - 1);
     
    -    // BaseIndex::Commit() writes nothing until the index tip is an ancestor of
    -    // the last flushed block, and a test setup never flushes on its own.
    -    test_setup.m_node.chainman->ActiveChainstate().ForceFlushStateToDisk();
    -
         return start_height + 1;
     }
     
    diff --git a/src/bench/index_sync_util.h b/src/bench/index_sync_util.h
    index 0e269a633a..ef74b56182 100644
    --- a/src/bench/index_sync_util.h
    +++ b/src/bench/index_sync_util.h
    @@ -30,8 +30,7 @@ static constexpr uint32_t BENCH_INDEX_TXS_PER_BLOCK{50};
      * coinbase, then `num_blocks` of `num_txs_per_block` chained transactions, each
      * spending the two outputs of the previous one. Returns the first height added.
      *
    - * Only callable once per setup: it always spends m_coinbase_txns[0]. Flushes
    - * the chainstate, without which BaseIndex::Commit() writes nothing.
    + * Only callable once per setup: it always spends m_coinbase_txns[0].
      */
     int ExtendChainWithSpends(TestChain100Setup& test_setup, uint32_t num_blocks, uint32_t num_txs_per_block);
     
    @@ -40,20 +39,16 @@ int ExtendChainWithSpends(TestChain100Setup& test_setup, uint32_t num_blocks, ui
      * `test_setup`. `make_index` must return a fresh index, wiped and constructed
      * inside the timed region: a second Sync() on a synced index is a no-op.
      *
    - * Disk vs Mem: f_memory=false keeps the index database on the filesystem, true
    - * keeps it in memory. The data directory defaults to fs::temp_directory_path(),
    - * a tmpfs on many Linux systems where Disk writes to RAM too, so the Disk
    - * figure only means what its name says when the directory is on real storage:
    - *
    - *     build/bin/bench_bitcoin -filter='<benchmark>' -testdatadir=/path/on/disk
    - *
    - * The chain is small enough that the index never leaves LevelDB's write buffer,
    - * so what Disk adds over Mem is page cache traffic. See doc/benchmarking.md.
    + * `f_memory=false` keeps the index database on the filesystem.
    + * The index stays in LevelDB's write buffer here, so Disk mostly adds the synced writes that create the database.
    + * The default data directory may be a RAM-backed tmpfs. See doc/benchmarking.md.
      */
     template <typename MakeIndex>
     void BenchIndexSync(benchmark::Bench& bench, TestChain100Setup& test_setup, MakeIndex make_index)
     {
    -    // The tip is fixed for the whole run, and cs_main stays out of the loop.
    +    // BaseIndex::Commit() writes nothing until the index tip is an ancestor of
    +    // the last flushed block, and a test setup never flushes on its own.
    +    test_setup.m_node.chainman->ActiveChainstate().ForceFlushStateToDisk();
         const auto expected_tip{WITH_LOCK(::cs_main, return test_setup.m_node.chainman->ActiveTip()->GetBlockHash())};
     
         bench.minEpochIterations(5).run([&] {
    

    </details>

  33. in src/bench/index_sync_util.h:69 in 2c97a9ad93
      64 | +        const IndexSummary summary{index->GetSummary()};
      65 | +        Assert(summary.synced);
      66 | +        Assert(summary.best_block_hash == expected_tip);
      67 | +
      68 | +        // Shutdown sequence (c.f. Shutdown() in init.cpp)
      69 | +        index->Stop();
    


    l0rinc commented at 10:25 AM on November 2, 2026:

    2c97a9a doc: describe measuring I/O and memory cost of benchmarks:

    Could we stop each temporary index with the existing StopIndex() helper? It drains queued validation callbacks before unregistering the index, matching the pre-existing block-filter benchmark and index tests.

    <details> <summary>Drain callbacks before stopping indexes</summary>

    diff --git a/src/bench/index_sync_util.h b/src/bench/index_sync_util.h
    index ef74b56182..b00fcc5c16 100644
    --- a/src/bench/index_sync_util.h
    +++ b/src/bench/index_sync_util.h
    @@ -11,6 +11,7 @@
     #include <primitives/transaction.h>
     #include <sync.h>
     #include <test/util/setup_common.h>
    +#include <test/util/validation.h>
     #include <util/check.h>
     #include <validation.h>
     
    @@ -60,8 +61,7 @@ void BenchIndexSync(benchmark::Bench& bench, TestChain100Setup& test_setup, Make
             Assert(summary.synced);
             Assert(summary.best_block_hash == expected_tip);
     
    -        // Shutdown sequence (c.f. Shutdown() in init.cpp)
    -        index->Stop();
    +        StopIndex(*index, test_setup.m_node);
         });
     }
     
    diff --git a/src/bench/index_txindex.cpp b/src/bench/index_txindex.cpp
    index d41ac1e827..6d596e9225 100644
    --- a/src/bench/index_txindex.cpp
    +++ b/src/bench/index_txindex.cpp
    @@ -9,6 +9,7 @@
     #include <interfaces/chain.h>
     #include <primitives/transaction.h>
     #include <test/util/setup_common.h>
    +#include <test/util/validation.h>
     #include <util/byte_units.h> // IWYU pragma: keep
     #include <util/check.h>
     
    @@ -54,7 +55,7 @@ static void TxIndexLookup(benchmark::Bench& bench)
     
         BenchIndexLookup(bench, txids, [&](const Txid& id) { return index->FindTx(id).has_value(); });
     
    -    index->Stop();
    +    StopIndex(*index, test_setup->m_node);
     }
     
     BENCHMARK(TxIndexSyncDisk);
    diff --git a/src/bench/index_txospender.cpp b/src/bench/index_txospender.cpp
    index 01fa3bf291..331154bf95 100644
    --- a/src/bench/index_txospender.cpp
    +++ b/src/bench/index_txospender.cpp
    @@ -9,6 +9,7 @@
     #include <interfaces/chain.h>
     #include <primitives/transaction.h>
     #include <test/util/setup_common.h>
    +#include <test/util/validation.h>
     #include <util/byte_units.h> // IWYU pragma: keep
     #include <util/check.h>
     #include <util/expected.h>
    @@ -60,7 +61,7 @@ static void TxoSpenderIndexLookup(benchmark::Bench& bench)
             return result && result->has_value();
         });
     
    -    index->Stop();
    +    StopIndex(*index, test_setup->m_node);
     }
     
     BENCHMARK(TxoSpenderIndexSyncDisk);
    

    </details>

  34. in src/bench/index_sync_util.h:87 in 2c97a9ad93
      82 | + * and returns whether it hit, which is asserted. What the number measures is
      83 | + * mostly the block file read and deserialization the lookup APIs do on top of
      84 | + * the database query, which at this chain size is answered from memory.
      85 | + */
      86 | +template <typename Key, typename LookupOne>
      87 | +void BenchIndexLookup(benchmark::Bench& bench, const std::vector<Key>& keys, LookupOne lookup_one)
    


    l0rinc commented at 10:26 AM on November 2, 2026:

    2c97a9a doc: describe measuring I/O and memory cost of benchmarks:

    The two lookup benchmarks have the same setup, synced check, and shutdown sequence, and keeping it beside the timed loop makes them harder to drift.

    <details> <summary>Share the lookup index lifecycle</summary>

    diff --git a/src/bench/index_sync_util.h b/src/bench/index_sync_util.h
    index b00fcc5c16..33a2f249af 100644
    --- a/src/bench/index_sync_util.h
    +++ b/src/bench/index_sync_util.h
    @@ -72,16 +72,21 @@ std::vector<Txid> CollectChainTxids(TestChain100Setup& test_setup, int from_heig
     std::vector<COutPoint> CollectChainSpentOutpoints(TestChain100Setup& test_setup, int from_height);
     
     /**
    - * Times `lookup_one(key)` over every key in `keys` on an already synced index,
    + * Syncs `index`, then times `lookup_one(key)` over every key in `keys`,
      * reporting time per lookup via `bench.batch()`. `lookup_one` runs one query
      * and returns whether it hit, which is asserted. What the number measures is
      * mostly the block file read and deserialization the lookup APIs do on top of
      * the database query, which at this chain size is answered from memory.
      */
     template <typename Key, typename LookupOne>
    -void BenchIndexLookup(benchmark::Bench& bench, const std::vector<Key>& keys, LookupOne lookup_one)
    +void BenchIndexLookup(benchmark::Bench& bench, TestChain100Setup& test_setup, BaseIndex& index,
    +                      const std::vector<Key>& keys, LookupOne lookup_one)
     {
         Assert(!keys.empty());
    +    Assert(index.Init());
    +    index.Sync();
    +    Assert(index.GetSummary().synced);
    +
         bench.batch(keys.size()).minEpochIterations(5).unit("lookup").run([&] {
             // Accumulated without short-circuiting, so every key is queried and the
             // reported per-lookup time really divides by keys.size().
    @@ -89,6 +94,8 @@ void BenchIndexLookup(benchmark::Bench& bench, const std::vector<Key>& keys, Loo
             for (const Key& key : keys) all_found &= lookup_one(key);
             Assert(all_found);
         });
    +
    +    StopIndex(index, test_setup.m_node);
     }
     
     #endif // BITCOIN_BENCH_INDEX_SYNC_UTIL_H
    diff --git a/src/bench/index_txindex.cpp b/src/bench/index_txindex.cpp
    index 6d596e9225..c651f3f54f 100644
    --- a/src/bench/index_txindex.cpp
    +++ b/src/bench/index_txindex.cpp
    @@ -4,14 +4,11 @@
     
     #include <bench/bench.h>
     #include <bench/index_sync_util.h>
    -#include <index/base.h>
     #include <index/txindex.h>
     #include <interfaces/chain.h>
     #include <primitives/transaction.h>
     #include <test/util/setup_common.h>
    -#include <test/util/validation.h>
     #include <util/byte_units.h> // IWYU pragma: keep
    -#include <util/check.h>
     
     #include <memory>
     #include <optional>
    @@ -47,15 +44,9 @@ static void TxIndexLookup(benchmark::Bench& bench)
             ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK)};
     
         auto index{MakeTxIndex(*test_setup, /*f_memory=*/false)};
    -    Assert(index->Init());
    -    index->Sync();
    -    Assert(index->GetSummary().synced);
    -
         const std::vector<Txid> txids{CollectChainTxids(*test_setup, first_spend_height)};
     
    -    BenchIndexLookup(bench, txids, [&](const Txid& id) { return index->FindTx(id).has_value(); });
    -
    -    StopIndex(*index, test_setup->m_node);
    +    BenchIndexLookup(bench, *test_setup, *index, txids, [&](const Txid& id) { return index->FindTx(id).has_value(); });
     }
     
     BENCHMARK(TxIndexSyncDisk);
    diff --git a/src/bench/index_txospender.cpp b/src/bench/index_txospender.cpp
    index 331154bf95..dbbc7544a8 100644
    --- a/src/bench/index_txospender.cpp
    +++ b/src/bench/index_txospender.cpp
    @@ -4,14 +4,11 @@
     
     #include <bench/bench.h>
     #include <bench/index_sync_util.h>
    -#include <index/base.h>
     #include <index/txospenderindex.h>
     #include <interfaces/chain.h>
     #include <primitives/transaction.h>
     #include <test/util/setup_common.h>
    -#include <test/util/validation.h>
     #include <util/byte_units.h> // IWYU pragma: keep
    -#include <util/check.h>
     #include <util/expected.h>
     
     #include <memory>
    @@ -50,18 +47,12 @@ static void TxoSpenderIndexLookup(benchmark::Bench& bench)
             ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK)};
     
         auto index{MakeTxoSpenderIndex(*test_setup, /*f_memory=*/false)};
    -    Assert(index->Init());
    -    index->Sync();
    -    Assert(index->GetSummary().synced);
    -
         const std::vector<COutPoint> outpoints{CollectChainSpentOutpoints(*test_setup, first_spend_height)};
     
    -    BenchIndexLookup(bench, outpoints, [&](const COutPoint& txo) {
    +    BenchIndexLookup(bench, *test_setup, *index, outpoints, [&](const COutPoint& txo) {
             const auto result{index->FindSpender(txo)};
             return result && result->has_value();
         });
    -
    -    StopIndex(*index, test_setup->m_node);
     }
     
     BENCHMARK(TxoSpenderIndexSyncDisk);
    

    </details>

  35. in src/bench/index_sync_util.cpp:118 in 2c97a9ad93
     113 | +    test_setup.m_node.chainman->ActiveChainstate().ForceFlushStateToDisk();
     114 | +
     115 | +    return start_height + 1;
     116 | +}
     117 | +
     118 | +std::vector<Txid> CollectChainTxids(TestChain100Setup& test_setup, int from_height)
    


    l0rinc commented at 10:28 AM on November 2, 2026:

    2c97a9a doc: describe measuring I/O and memory cost of benchmarks:

    The current helpers re-read all 51 blocks from disk while holding cs_main, even though the benchmark just built those blocks.

    We could return the blocks from ExtendChainWithSpends() and collect lookup keys from them directly - which would let us chain construction use one uniform loop, validate each block as it is submitted, and remove the duplicate readers.

    <details> <summary>Reuse the constructed blocks</summary>

    diff --git a/src/bench/index_blockfilter.cpp b/src/bench/index_blockfilter.cpp
    index 9f62301a5a..4ab34520c9 100644
    --- a/src/bench/index_blockfilter.cpp
    +++ b/src/bench/index_blockfilter.cpp
    @@ -72,13 +72,12 @@ static std::unique_ptr<BlockFilterIndex> MakeBlockFilterIndex(TestChain100Setup&
                                                   /*n_cache_size=*/1_MiB, f_memory, /*f_wipe=*/true);
     }
     
    -// Same sync as BlockFilterIndexSync above, but over blocks carrying
    -// transactions paying to distinct scripts, so the filters scale with what a
    -// block contains rather than with the number of blocks.
    +// Sync blocks containing transactions that pay distinct scripts.
    +// Their filters scale with block contents rather than block count.
     static void BlockFilterIndexSyncRealistic(benchmark::Bench& bench, bool f_memory)
     {
         const auto test_setup = MakeNoLogFileContext<TestChain100Setup>();
    -    ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK);
    +    ExtendChainWithSpends(*test_setup);
     
         BenchIndexSync(bench, *test_setup, [&] { return MakeBlockFilterIndex(*test_setup, f_memory); });
     }
    diff --git a/src/bench/index_sync_util.cpp b/src/bench/index_sync_util.cpp
    index b8c6a242b2..3784647504 100644
    --- a/src/bench/index_sync_util.cpp
    +++ b/src/bench/index_sync_util.cpp
    @@ -5,35 +5,35 @@
     #include <bench/index_sync_util.h>
     
     #include <addresstype.h>
    -#include <chain.h>
     #include <consensus/amount.h>
     #include <key.h>
    -#include <node/blockstorage.h>
     #include <policy/feerate.h>
     #include <primitives/block.h>
     #include <primitives/transaction.h>
     #include <script/script.h>
    -#include <sync.h>
    +#include <test/util/mining.h>
     #include <test/util/setup_common.h>
     #include <test/util/time.h>
     #include <util/check.h>
    -#include <validation.h>
     
     #include <chrono>
     #include <cstddef>
     #include <cstdint>
    +#include <memory>
     #include <optional>
     #include <utility>
     #include <vector>
     
     using namespace std::chrono_literals;
     
    -int ExtendChainWithSpends(TestChain100Setup& test_setup, uint32_t num_blocks, uint32_t num_txs_per_block)
    +std::vector<CBlock> ExtendChainWithSpends(TestChain100Setup& test_setup)
     {
    -    Assert(num_txs_per_block > 0);
    +    // Building the chain is not timed, but it is most of the wall time of a run
    +    constexpr uint32_t num_blocks{50};
    +    constexpr uint32_t num_txs_per_block{50};
     
         // Block filter elements are deduplicated, so only distinct scripts add to a
    -    // filter. Keys are reused across blocks, distinct within one.
    +    // filter. Key pairs are reused across blocks, distinct within one.
         const size_t num_keys{2 * num_txs_per_block};
         std::vector<CKey> keys;
         std::vector<CTxOut> outs;
    @@ -45,99 +45,38 @@ int ExtendChainWithSpends(TestChain100Setup& test_setup, uint32_t num_blocks, ui
         }
     
         const CScript coinbase_spk{GetScriptForDestination(WitnessV0KeyHash{test_setup.coinbaseKey.GetPubKey()})};
    -    const int start_height{WITH_LOCK(::cs_main, return test_setup.m_node.chainman->ActiveHeight())};
     
         // The coinbase of block 1 matures exactly at the first height added here.
    -    auto& coinbase_to_spend{test_setup.m_coinbase_txns[0]};
    -    size_t k0{0};
    -    size_t k1{1};
    -    const CMutableTransaction first_tx{
    -        test_setup.CreateValidTransaction(
    -            /*input_transactions=*/{coinbase_to_spend},
    -            /*inputs=*/{COutPoint(coinbase_to_spend->GetHash(), 0)},
    -            /*input_height=*/1,
    -            /*input_signing_keys=*/{test_setup.coinbaseKey},
    -            /*outputs=*/{outs[k0], outs[k1]},
    -            /*feerate=*/{},
    -            /*fee_output=*/{})
    -            .first};
    -    test_setup.CreateAndProcessBlock({first_tx}, coinbase_spk);
    -    test_setup.m_clock += 1s;
    +    CTransactionRef tx_to_spend{test_setup.m_coinbase_txns[0]};
    +    std::vector<COutPoint> inputs{{tx_to_spend->GetHash(), 0}};
    +    std::vector<CKey> signing_keys{test_setup.coinbaseKey};
    +    size_t output_pair{0};
     
    -    // first_tx went into start_height + 1, so the loop starts one above that.
    -    int next_height{start_height + 2};
    -    CTransactionRef tx_to_spend{MakeTransactionRef(first_tx)};
    -    size_t next_key{2 % num_keys};
    +    std::vector<CBlock> blocks;
    +    blocks.reserve(num_blocks);
         for (uint32_t b{0}; b < num_blocks; ++b) {
             std::vector<CMutableTransaction> txs;
             txs.reserve(num_txs_per_block);
             for (uint32_t i{0}; i < num_txs_per_block; ++i) {
    -            const std::vector<COutPoint> inputs{
    -                COutPoint(tx_to_spend->GetHash(), 0),
    -                COutPoint(tx_to_spend->GetHash(), 1),
    -            };
    -            const std::vector<CKey> signing_keys{keys[k0], keys[k1]};
    -            // The first transaction of a block spends the last one of the
    -            // previous block; the rest spend within this one.
    -            const int input_height{i == 0 ? next_height - 1 : next_height};
    -            k0 = next_key;
    -            next_key = (next_key + 1) % num_keys;
    -            k1 = next_key;
    -            next_key = (next_key + 1) % num_keys;
    -            const CMutableTransaction tx{
    -                test_setup.CreateValidTransaction(
    -                    /*input_transactions=*/{tx_to_spend},
    -                    /*inputs=*/inputs,
    -                    /*input_height=*/input_height,
    -                    /*input_signing_keys=*/signing_keys,
    -                    /*outputs=*/{outs[k0], outs[k1]},
    -                    /*feerate=*/{},
    -                    /*fee_output=*/{})
    -                    .first};
    -            txs.emplace_back(tx);
    +            auto tx{test_setup.CreateValidTransaction(
    +                                  /*input_transactions=*/{tx_to_spend},
    +                                  /*inputs=*/inputs,
    +                                  /*input_height=*/0, // Height is unused outside the scratch signing view
    +                                  /*input_signing_keys=*/signing_keys,
    +                                  /*outputs=*/{outs[2 * output_pair], outs[2 * output_pair + 1]},
    +                                  /*feerate=*/{},
    +                                  /*fee_output=*/{})
    +                        .first};
                 tx_to_spend = MakeTransactionRef(tx);
    +            txs.push_back(std::move(tx));
    +            inputs = {{tx_to_spend->GetHash(), 0}, {tx_to_spend->GetHash(), 1}};
    +            signing_keys = {keys[2 * output_pair], keys[2 * output_pair + 1]};
    +            output_pair = (output_pair + 1) % num_txs_per_block;
             }
    -        test_setup.CreateAndProcessBlock(txs, coinbase_spk);
    -        // UpdateTime() clamps a block's time to at least MTP+1, so a frozen
    -        // clock drifts a second per block and eventually trips
    -        // MAX_FUTURE_BLOCK_TIME.
    +        auto& block{blocks.emplace_back(test_setup.CreateBlock(txs, coinbase_spk))};
    +        Assert(!ProcessBlock(test_setup.m_node, std::make_shared<CBlock>(block)).IsNull());
    +        // UpdateTime() clamps block times to at least MTP+1, so advance the mock clock like mineBlocks()
             test_setup.m_clock += 1s;
    -        ++next_height;
         }
    -
    -    // CreateAndProcessBlock ignores the result of ProcessNewBlock.
    -    Assert(WITH_LOCK(::cs_main, return test_setup.m_node.chainman->ActiveHeight()) == next_height - 1);
    -
    -    return start_height + 1;
    -}
    -
    -std::vector<Txid> CollectChainTxids(TestChain100Setup& test_setup, int from_height)
    -{
    -    Assert(from_height >= 0);
    -    std::vector<Txid> txids;
    -    LOCK(::cs_main);
    -    const CChain& chain{test_setup.m_node.chainman->ActiveChain()};
    -    for (int h{from_height}; h <= chain.Height(); ++h) {
    -        CBlock block;
    -        Assert(test_setup.m_node.chainman->m_blockman.ReadBlock(block, *chain[h]));
    -        for (const auto& tx : block.vtx) txids.push_back(tx->GetHash());
    -    }
    -    return txids;
    -}
    -
    -std::vector<COutPoint> CollectChainSpentOutpoints(TestChain100Setup& test_setup, int from_height)
    -{
    -    Assert(from_height >= 0);
    -    std::vector<COutPoint> outpoints;
    -    LOCK(::cs_main);
    -    const CChain& chain{test_setup.m_node.chainman->ActiveChain()};
    -    for (int h{from_height}; h <= chain.Height(); ++h) {
    -        CBlock block;
    -        Assert(test_setup.m_node.chainman->m_blockman.ReadBlock(block, *chain[h]));
    -        for (const auto& tx : block.vtx) {
    -            if (tx->IsCoinBase()) continue;
    -            for (const auto& in : tx->vin) outpoints.push_back(in.prevout);
    -        }
    -    }
    -    return outpoints;
    +    return blocks;
     }
    diff --git a/src/bench/index_sync_util.h b/src/bench/index_sync_util.h
    index 33a2f249af..300293efa1 100644
    --- a/src/bench/index_sync_util.h
    +++ b/src/bench/index_sync_util.h
    @@ -8,32 +8,24 @@
     #include <bench/bench.h>
     #include <chain.h>
     #include <index/base.h>
    -#include <primitives/transaction.h>
    +#include <primitives/block.h>
     #include <sync.h>
     #include <test/util/setup_common.h>
     #include <test/util/validation.h>
     #include <util/check.h>
     #include <validation.h>
     
    -#include <cstdint>
     #include <memory>
     #include <vector>
     
    -/** Blocks of chained spends the benchmarks build. ExtendChainWithSpends() mines
    - *  one more on top of these to spend a coinbase, over TestChain100Setup's 100.
    - *  Building the chain is not timed, but it is most of the wall time of a run. */
    -static constexpr uint32_t BENCH_INDEX_NUM_BLOCKS{50};
    -/** Chained transactions in each of those blocks. */
    -static constexpr uint32_t BENCH_INDEX_TXS_PER_BLOCK{50};
    -
     /**
    - * Extends the active chain with `num_blocks + 1` blocks: one spending a mature
    - * coinbase, then `num_blocks` of `num_txs_per_block` chained transactions, each
    - * spending the two outputs of the previous one. Returns the first height added.
    + * Extends the active chain with blocks of chained transactions.
    + * The first transaction spends a mature coinbase, and each later transaction spends the two outputs of its predecessor.
    + * Returns the blocks added.
      *
      * Only callable once per setup: it always spends m_coinbase_txns[0].
      */
    -int ExtendChainWithSpends(TestChain100Setup& test_setup, uint32_t num_blocks, uint32_t num_txs_per_block);
    +std::vector<CBlock> ExtendChainWithSpends(TestChain100Setup& test_setup);
     
     /**
      * Times `make_index()` plus a full Init -> Sync -> Stop cycle over the chain in
    @@ -65,12 +57,6 @@ void BenchIndexSync(benchmark::Bench& bench, TestChain100Setup& test_setup, Make
         });
     }
     
    -/** All txids in the active chain from `from_height` (>= 0) to the tip, in block/tx order. */
    -std::vector<Txid> CollectChainTxids(TestChain100Setup& test_setup, int from_height);
    -
    -/** All outpoints spent by non-coinbase transactions from `from_height` (>= 0) to the tip. */
    -std::vector<COutPoint> CollectChainSpentOutpoints(TestChain100Setup& test_setup, int from_height);
    -
     /**
      * Syncs `index`, then times `lookup_one(key)` over every key in `keys`,
      * reporting time per lookup via `bench.batch()`. `lookup_one` runs one query
    diff --git a/src/bench/index_txindex.cpp b/src/bench/index_txindex.cpp
    index c651f3f54f..fe83b2a08d 100644
    --- a/src/bench/index_txindex.cpp
    +++ b/src/bench/index_txindex.cpp
    @@ -6,6 +6,7 @@
     #include <bench/index_sync_util.h>
     #include <index/txindex.h>
     #include <interfaces/chain.h>
    +#include <primitives/block.h>
     #include <primitives/transaction.h>
     #include <test/util/setup_common.h>
     #include <util/byte_units.h> // IWYU pragma: keep
    @@ -27,7 +28,7 @@ static std::unique_ptr<TxIndex> MakeTxIndex(TestChain100Setup& test_setup, bool
     static void TxIndexSync(benchmark::Bench& bench, bool f_memory)
     {
         const auto test_setup = MakeNoLogFileContext<TestChain100Setup>();
    -    ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK);
    +    ExtendChainWithSpends(*test_setup);
     
         BenchIndexSync(bench, *test_setup, [&] { return MakeTxIndex(*test_setup, f_memory); });
     }
    @@ -40,12 +41,14 @@ static void TxIndexSyncMem(benchmark::Bench& bench) { TxIndexSync(bench, /*f_mem
     static void TxIndexLookup(benchmark::Bench& bench)
     {
         const auto test_setup = MakeNoLogFileContext<TestChain100Setup>();
    -    const int first_spend_height{
    -        ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK)};
    +    std::vector<Txid> txids;
    +    for (const auto& block : ExtendChainWithSpends(*test_setup)) {
    +        for (const auto& tx : block.vtx) {
    +            txids.push_back(tx->GetHash());
    +        }
    +    }
     
         auto index{MakeTxIndex(*test_setup, /*f_memory=*/false)};
    -    const std::vector<Txid> txids{CollectChainTxids(*test_setup, first_spend_height)};
    -
         BenchIndexLookup(bench, *test_setup, *index, txids, [&](const Txid& id) { return index->FindTx(id).has_value(); });
     }
     
    diff --git a/src/bench/index_txospender.cpp b/src/bench/index_txospender.cpp
    index dbbc7544a8..3ea8dfb5a7 100644
    --- a/src/bench/index_txospender.cpp
    +++ b/src/bench/index_txospender.cpp
    @@ -6,6 +6,7 @@
     #include <bench/index_sync_util.h>
     #include <index/txospenderindex.h>
     #include <interfaces/chain.h>
    +#include <primitives/block.h>
     #include <primitives/transaction.h>
     #include <test/util/setup_common.h>
     #include <util/byte_units.h> // IWYU pragma: keep
    @@ -17,8 +18,7 @@
     #include <vector>
     
     // Returns a fresh, not-yet-initialized TxoSpenderIndex. See BenchIndexSync()
    -// for what f_memory changes. f_wipe empties the database, so the constructor
    -// regenerates and writes the siphash key on every iteration.
    +// for what f_memory changes.
     static std::unique_ptr<TxoSpenderIndex> MakeTxoSpenderIndex(TestChain100Setup& test_setup, bool f_memory)
     {
         return std::make_unique<TxoSpenderIndex>(interfaces::MakeChain(test_setup.m_node),
    @@ -30,7 +30,7 @@ static std::unique_ptr<TxoSpenderIndex> MakeTxoSpenderIndex(TestChain100Setup& t
     static void TxoSpenderIndexSync(benchmark::Bench& bench, bool f_memory)
     {
         const auto test_setup = MakeNoLogFileContext<TestChain100Setup>();
    -    ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK);
    +    ExtendChainWithSpends(*test_setup);
     
         BenchIndexSync(bench, *test_setup, [&] { return MakeTxoSpenderIndex(*test_setup, f_memory); });
     }
    @@ -43,12 +43,17 @@ static void TxoSpenderIndexSyncMem(benchmark::Bench& bench) { TxoSpenderIndexSyn
     static void TxoSpenderIndexLookup(benchmark::Bench& bench)
     {
         const auto test_setup = MakeNoLogFileContext<TestChain100Setup>();
    -    const int first_spend_height{
    -        ExtendChainWithSpends(*test_setup, BENCH_INDEX_NUM_BLOCKS, BENCH_INDEX_TXS_PER_BLOCK)};
    +    std::vector<COutPoint> outpoints;
    +    for (const auto& block : ExtendChainWithSpends(*test_setup)) {
    +        for (const auto& tx : block.vtx) {
    +            if (tx->IsCoinBase()) continue;
    +            for (const auto& in : tx->vin) {
    +                outpoints.push_back(in.prevout);
    +            }
    +        }
    +    }
     
         auto index{MakeTxoSpenderIndex(*test_setup, /*f_memory=*/false)};
    -    const std::vector<COutPoint> outpoints{CollectChainSpentOutpoints(*test_setup, first_spend_height)};
    -
         BenchIndexLookup(bench, *test_setup, *index, outpoints, [&](const COutPoint& txo) {
             const auto result{index->FindSpender(txo)};
             return result && result->has_value();
    

    </details>

  36. l0rinc changes_requested
  37. l0rinc commented at 10:38 AM on November 2, 2026: contributor

    Thanks for addressing my previous suggestions. I’ve now done another AI-assisted review round and left the findings inline. There are still a few points I’d like to see addressed, so I expect to do another manual round after the next update, but I’m posting these in the meantime so you have them now.

  38. bench: add reusable index sync benchmark helpers
    The existing BlockFilterIndexSync syncs coinbase-only blocks, which
    cannot exercise anything that scales with the number of inputs or
    outputs per block.
    
    Add src/bench/index_sync_util.{h,cpp} with the chain setup and timing
    loops shared by index benchmarks. ExtendChainWithSpends() builds blocks
    of validly-signed transactions with ~2 inputs each (the average ratio
    observed on mainnet), paying every output to a distinct key: block
    filter elements are deduplicated, so reusing a couple of scripts would
    keep every filter tiny no matter how many transactions the block holds.
    
    BenchIndexSync() also flushes the chainstate before timing. BaseIndex::Commit
    returns early unless the index tip is an ancestor of the last flushed
    block, and a test setup never flushes on its own, so without this the
    benchmarks would write the index entries but never the best-block
    locator, leaving CustomCommit() outside the measurement.
    
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    a340b0db35
  39. bench: add TxoSpenderIndex, TxIndex and BlockFilterIndex sync benchmarks
    Add, on top of the shared helpers:
    - TxoSpenderIndexSync{Disk,Mem} and TxoSpenderIndexLookup (FindSpender)
    - TxIndexSync{Disk,Mem} and TxIndexLookup (FindTx)
    - BlockFilterIndexSyncRealistic{Disk,Mem}
    
    The Disk variant exercises the file-backed write path, the Mem variant
    keeps the database in memory. Reading the pair as the cost of that path
    requires the data directory to be on real storage: it defaults to
    fs::temp_directory_path(), which on many Linux systems is a tmpfs, and
    there both variants write to RAM and report the same number. Pointing
    -testdatadir at a real filesystem is what makes the Disk figures mean
    what their name says.
    
    The pre-existing coinbase-only BlockFilterIndexSync is left untouched.
    It scales with the number of blocks over near-empty filters, where the
    new one scales with what a block contains, so the two are kept as
    separate axes.
    
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    c7887c7ad6
  40. doc: describe measuring I/O and memory cost of benchmarks
    The framework reports wall-clock time and, where available, cycles and
    instructions, but not syscalls, bytes written, on-disk size or peak
    memory. Document how to measure those around bench_bitcoin with strace,
    /usr/bin/time and du, and the -testdatadir knob needed to avoid tmpfs.
    
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    e8d3a6e485
  41. arejula27 force-pushed on Nov 4, 2026
  42. arejula27 commented at 10:13 PM on November 4, 2026: contributor

    Thanks @l0rinc! I applied all of your suggestions and added you as a co-author, since I used your patches exactly as you wrote them. I also updated the PRs description, I ran the bench on a different machine.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-11-08 19:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me