Skip to content

perf: reuse compact filter file handles - #7574

Merged
PastaPastaPasta merged 1 commit into
dashpay:developfrom
PastaPastaPasta:perf/reuse-cfilter-file-handles
Aug 11, 2026
Merged

perf: reuse compact filter file handles#7574
PastaPastaPasta merged 1 commit into
dashpay:developfrom
PastaPastaPasta:perf/reuse-cfilter-file-handles

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 11, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Serving a normal BIP157 getcfilters range caused BlockFilterIndex::LookupFilterRange to open and close the compact-filter flat file once per filter. A 1,000-filter request therefore performed up to 1,000 file-open operations even when the filters were contiguous in the same underlying file.

This adds avoidable filesystem overhead to mobile/SPV sync, where peers commonly request long contiguous ranges.

What was done?

  • Keep one AutoFile open while reading consecutive filters from the same flat file.
  • Seek the reused handle to each recorded filter position.
  • Open a new handle only when the range crosses into another flat file.
  • Preserve checksum verification and BlockFilter construction in a shared ReadFilterFromFile helper.
  • Add focused benchmarks for single-filter lookup, 100/1,000-filter ranges, encoded-byte throughput, serialization, and combined lookup plus serialization.

The wire protocol and individual cfilter messages are unchanged.

How Has This Been Tested?

Tested on Apple Silicon using a depends-based release build:

  • Full make build
  • Full make check
  • test/functional/test_runner.py p2p_blockfilters.py rpc_getblockfilter.py
  • test/lint/lint-whitespace.py
  • test/lint/lint-circular-dependencies.py

In-tree benchmark

For a contiguous 1,000-filter range containing approximately 2.64 MB of encoded filters:

Benchmark Before After Improvement
Range lookup 21.21 ms 5.96 ms 3.56x / 71.9% less time
Lookup + serialization 21.26 ms 6.19 ms 3.43x / 70.9% less time
Single-filter lookup 21.23 µs 23.91 µs Effectively unchanged

The current branch rerun measured approximately 180,000 filters/s for the 1,000-filter range.

End-to-end sync measurement note

Exploratory rust-dashcore testnet sync runs confirmed that this path is exercised during full SPV sync, but their absolute before/after timings are not presented as proof here. The runs were not sufficiently controlled for server warm-cache state and background compact-filter-index I/O, so cross-condition timing differences were confounded. The deterministic in-tree benchmark above isolates the changed lookup path.

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone

This pull request was created by Codex.

@thepastaclaw

thepastaclaw commented Aug 11, 2026

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit 51f4049)

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The index now deserializes filters through a helper that accepts an open file. Range lookups reuse files and seek between filter positions. A new benchmark suite builds a test chain, synchronizes a BlockFilterIndex, and measures filter lookup and serialization operations. The benchmark source is added to the benchmark build and non-backported file list.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Benchmark
  participant BlockFilterIndex
  participant AutoFile
  Benchmark->>BlockFilterIndex: request filter range
  BlockFilterIndex->>AutoFile: open or seek filter file
  BlockFilterIndex->>BlockFilterIndex: deserialize filter
  BlockFilterIndex-->>Benchmark: return filters
  Benchmark->>Benchmark: serialize filters and measure work
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: reusing compact filter file handles.
Description check ✅ Passed The description directly explains the file-handle reuse, benchmarks, testing, performance results, and unchanged protocol behavior.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

End-to-end testnet sync benchmark using the current clean rust-dashcore development client.

Setup

  • Host: Apple M4 Pro, 14 logical CPUs, Darwin 25.5.0.
  • Dash Core before: clean worktree at develop@e19dfeaacae96e7ecda5467edb5e24f2c54d2cb2.
  • Dash Core after: separate clean worktree at this PR's 2032dee123049d00d72343cb1d6a942dfca1522d.
  • Both Dash Core revisions were configured and built in fresh worktrees with the same depends prefix and flags: --without-gui --disable-tests --disable-bench, then make -j13.
  • Client: clean dashpay/rust-dashcore dev@5a80bd71f6ba13a5055780d644f0aa724a34ca04, built with cargo build --release -p dash-spv-bench.
  • Dataset: unpruned testnet at height 1,531,863, tip 0000005308ffe3a6d9ed59a9dd24a7c0a33b0df7b798376acb275ec4a8576a12; the basic compact-filter index was fully synced at the same height.
  • The before/after daemons used distinct copy-on-write datadir clones of the same stopped dataset. Each accepted only a single localhost client (BENCH_MAX_PEERS=1) and had public peer discovery/connectivity disabled (-connect=0 -dnsseed=0).
  • Four full syncs were run per revision with a fresh SPV storage directory each time. Run 1 was treated as warm-up and excluded; median and arithmetic mean use runs 2–4.

Raw full-sync times

Run Before After
1 (warm-up) 48.153 s 26.045 s
2 45.147 s 26.294 s
3 42.376 s 24.453 s
4 44.328 s 25.865 s

Aggregates over runs 2–4

Metric Before After Change
Full sync, median 44.328 s 25.865 s 1.714x faster / 41.65% less time
Full sync, mean 43.950 s 25.537 s 1.721x faster / 41.90% less time
Block-header completion, median 11.296 s 11.504 s effectively unchanged
Filter-header completion (elapsed), median 40.787 s 19.803 s 51.45% less time
Filter-header completion (elapsed), mean 41.548 s 19.340 s 53.45% less time

The unchanged block-header milestone and the large reduction in later compact-filter sync work make this a strong end-to-end signal for the range-read optimization. On this fixed local testnet dataset, the PR reduced overall rust-dashcore full-sync time by about 42%.


🤖 Posted autonomously by Codex on behalf of pasta.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Codex/Sol only (Phase 2 disabled)

The compact-filter file-handle reuse is correct, but the new Dash-authored benchmark is missing from the Dash-only cppcheck inventory. The two corrective follow-up commits should also be folded into the commit that introduced the affected implementation and benchmarks; no blocking correctness issue remains at the reviewed head.
Source: reviewer backend model gpt-5.6-sol (Codex general and dash-core-commit-history); final verifier backend model gpt-5.6-sol (Codex); orchestration-only openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 3 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/bench/blockfilter_index.cpp`:
- [SUGGESTION] src/bench/blockfilter_index.cpp:1: Register the new Dash-specific benchmark for cppcheck
  This newly authored Dash benchmark has no counterpart in `bitcoin/master`, but `test/util/data/non-backported.txt` does not include it. `lint-cppcheck-dash.py` constructs its entire analysis set from that inventory, and `git ls-files` with the current patterns omits this file. Add `src/bench/blockfilter_index.cpp` to `test/util/data/non-backported.txt` so the additional Dash-only static analysis covers it.
- [SUGGESTION] src/bench/blockfilter_index.cpp:103: Fold the benchmark corrections into the benchmark introduction
  Commit `2032dee1230` revises benchmarks introduced by `d934d2a885e`, replacing their shared static fixture with isolated per-benchmark setup and changing all six registrations from HIGH to LOW priority. These are corrections to the benchmarks' initial lifecycle and execution policy rather than an independent feature. Fold this commit into `d934d2a885e` so the benchmarks enter history in their intended form.

In `src/index/blockfilterindex.cpp`:
- [SUGGESTION] src/index/blockfilterindex.cpp:473: Squash the compact-filter guard into its introducing commit
  Commit `4ba4242d799` corrects the reuse loop introduced immediately beforehand by `d934d2a885e`. `FlatFilePos::nFile` defaults to `-1`, so the original condition could match the initial `file_num{-1}` and enter the seek branch before `file` was opened; the added `!file` guard belongs to the correctness of the introduced loop. Fold this one-line correction into `d934d2a885e` so the implementation is complete at every commit boundary.

@@ -0,0 +1,184 @@
// Copyright (c) 2026 The Dash Core developers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Register the new Dash-specific benchmark for cppcheck

This newly authored Dash benchmark has no counterpart in bitcoin/master, but test/util/data/non-backported.txt does not include it. lint-cppcheck-dash.py constructs its entire analysis set from that inventory, and git ls-files with the current patterns omits this file. Add src/bench/blockfilter_index.cpp to test/util/data/non-backported.txt so the additional Dash-only static analysis covers it.

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Register the new Dash-specific benchmark for cppcheck no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.


filters_out.resize(entries.size());
auto filter_pos_it = filters_out.begin();
std::unique_ptr<AutoFile> file;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Squash the compact-filter guard into its introducing commit

Commit 4ba4242d799 corrects the reuse loop introduced immediately beforehand by d934d2a885e. FlatFilePos::nFile defaults to -1, so the original condition could match the initial file_num{-1} and enter the seek branch before file was opened; the added !file guard belongs to the correctness of the introduced loop. Fold this one-line correction into d934d2a885e so the implementation is complete at every commit boundary.

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Squash the compact-filter guard into its introducing commit no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

}
return messages;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Fold the benchmark corrections into the benchmark introduction

Commit 2032dee1230 revises benchmarks introduced by d934d2a885e, replacing their shared static fixture with isolated per-benchmark setup and changing all six registrations from HIGH to LOW priority. These are corrections to the benchmarks' initial lifecycle and execution policy rather than an independent feature. Fold this commit into d934d2a885e so the benchmarks enter history in their intended form.

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Fold the benchmark corrections into the benchmark introduction no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

@PastaPastaPasta
PastaPastaPasta force-pushed the perf/reuse-cfilter-file-handles branch from 2032dee to 51f4049 Compare August 11, 2026 05:09
@PastaPastaPasta PastaPastaPasta changed the title perf(index): reuse compact filter file handles perf: reuse compact filter file handles Aug 11, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Codex/Sol only (Phase 2 disabled)

At exact head 51f4049, the compact-filter range lookup correctly reuses an open file, seeks before noncontiguous reads, changes handles across files, and preserves checksum verification and deserialization. All three prior suggestions are fixed in the single current commit: the benchmark is in the Dash cppcheck inventory, the unopened-file guard is present, and the benchmark setup and priorities are corrected; no in-scope defects remain.
Source: Codex reviewer backend gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier backend gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol was orchestration-only and is not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

@knst knst left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 51f4049

@PastaPastaPasta
PastaPastaPasta merged commit 1fb3399 into dashpay:develop Aug 11, 2026
50 of 53 checks passed
@PastaPastaPasta
PastaPastaPasta deleted the perf/reuse-cfilter-file-handles branch August 11, 2026 21:05
@UdjinM6 UdjinM6 added this to the 24 milestone Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants