Skip to content

Perf/cache epoch version in ClarityDatabase#6959

Draft
jacinta-stacks wants to merge 14 commits intostacks-network:developfrom
jacinta-stacks:perf/cache-epoch-per-block
Draft

Perf/cache epoch version in ClarityDatabase#6959
jacinta-stacks wants to merge 14 commits intostacks-network:developfrom
jacinta-stacks:perf/cache-epoch-per-block

Conversation

@jacinta-stacks
Copy link
Copy Markdown
Contributor

@jacinta-stacks jacinta-stacks commented Mar 5, 2026

Cache get_clarity_epoch_version() in a field on ClarityDatabase to eliminate redundant KV store lookups (2-3 times per map-set, map-get?, map-insert, map-delete, var-set, and NFT operation (for admits() type checks).

  • The epoch key is only written through set_clarity_epoch_version(), which updates the cache
  • roll_back() clears the cache defensively, so a rolled-back set_clarity_epoch_version() cannot leave a stale value
  • at-block also invalidates the store so it is reset there
  • ClarityDatabase instances are short-lived (created/destroyed per closure in with_clarity_db, as_transaction, etc.)

Benchmark Results (cargo bench --bench epoch_cache -p clarity)

Benchmark No cache Cached Change
map_set_get/50 455 µs 304 µs -33.2%
map_set_get/200 1.79 ms 1.18 ms -34.1%
var_set_get/50 229 µs 176 µs -23.1%
var_set_get/200 867 µs 668 µs -22.9%
call_heavy/50 78 µs 77 µs ~0% (no epoch lookups in this path)
call_heavy/200 273 µs 267 µs ~0% (no epoch lookups in this path)
map_insert_delete/50 585 µs 446 µs -23.8%
map_insert_delete/200 2.44 ms 1.84 ms -24.6%

Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
…epoch

Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.06%. Comparing base (995ebd0) to head (a92bda2).
⚠️ Report is 25 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff              @@
##           develop    #6959       +/-   ##
============================================
+ Coverage    68.63%   80.06%   +11.42%     
============================================
  Files          412      412               
  Lines       219013   219023       +10     
  Branches       338      338               
============================================
+ Hits        150330   175363    +25033     
+ Misses       68683    43660    -25023     
Files with missing lines Coverage Δ
clarity/src/vm/database/clarity_db.rs 80.98% <100.00%> (+1.96%) ⬆️

... and 257 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 995ebd0...a92bda2. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@cylewitruk-stacks cylewitruk-stacks left a comment

Choose a reason for hiding this comment

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

Makes sense to me. Just added a few benchmark suggestions.

b.iter_batched(
setup_store,
|marf| run(&parsed, marf),
BatchSize::SmallInput,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think that SmallInput may not be the best hint together with setup_store() since it tells Criterion that the values are small and it's okay to create thousands of them up-front (i.e. thousands of SQLite in-memory handles). Maybe use PerIteration or LargeInput?

But otoh if you don't see large memory usage when running the benchmarks then it might be small enough to not matter, I'm not sure off the top of my head how "resource hungry" the MemoryBackingStore + its SQLite in-mem instance is.

Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
@cylewitruk-stacks
Copy link
Copy Markdown
Contributor

Do you think we could add a couple of defensive regression tests specifically around the rollback and at-block cases asserting the cached value is cleared? The rollback case is clear in the diff, but not at-block (where I'm most insecure) -- and it's anyway cheap insurance that future changes don't mess this up?

jcnelson
jcnelson previously approved these changes Mar 11, 2026
@jcnelson jcnelson self-requested a review March 11, 2026 18:47
Copy link
Copy Markdown
Member

@jcnelson jcnelson left a comment

Choose a reason for hiding this comment

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

I think there could be a better way to do this: accept the epoch as an argument to ClarityDatabase::new() and ClarityDatabase::new_with_rollback_wrapper(). In all cases where we actually open the Clarity database on chainstate (i.e. not MemoryBackingStore), the epoch will be known to the caller. That could save you an additional MARF'ed read.

@jacinta-stacks
Copy link
Copy Markdown
Contributor Author

I think there could be a better way to do this: accept the epoch as an argument to ClarityDatabase::new() and ClarityDatabase::new_with_rollback_wrapper(). In all cases where we actually open the Clarity database on chainstate (i.e. not MemoryBackingStore), the epoch will be known to the caller. That could save you an additional MARF'ed read.

This is quite a substantial change. Not sure its worth the effort but I will give it a try and you can assess the result :)

Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
@jacinta-stacks jacinta-stacks force-pushed the perf/cache-epoch-per-block branch from e1d2a21 to 6bfa9fc Compare March 12, 2026 19:38
… read per transaction

Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
@federico-stacks federico-stacks self-assigned this Apr 8, 2026
Comment on lines 873 to 875
/// Since Clarity did not exist in stacks 1.0, the lowest valid epoch ID is stacks 2.0.
/// The instantiation of subsequent epochs may bump up the epoch version in the clarity DB if
/// Clarity is updated in that epoch.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: this doc string belongs to get_clarity_epoch_version and not to parse_epoch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

crc: 33bd8e3

Comment on lines 371 to 382
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This could benefit from using ClarityDatabase::read_epoch_from_store, as is done in other call sites such as read_only_connection_checked and eval_read_only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

crc: a2593d2

This also made burn_state_db arg unused and could be removed also from the call chain

/// `get_clarity_epoch_version()` call and updated by
/// `set_clarity_epoch_version()`. The epoch never changes
/// mid-block except through `set_clarity_epoch_version()`
cached_epoch: Option<StacksEpochId>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: improve docstring for methods involving cached_epoch (general comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

crc: 2cb8150

ClarityDatabase::new(self, headers_db, burn_state_db, None)
}

fn as_clarity_db_with_epoch<'b>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: missing docstring

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

crc: 9de52bb

store: &'a mut dyn ClarityBackingStore,
headers_db: &'a dyn HeadersDB,
burn_state_db: &'a dyn BurnStateDB,
cached_epoch: Option<StacksEpochId>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

improve test coverage for cache_epoch being set during construction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

crc: 38e54d7

@federico-stacks federico-stacks marked this pull request as ready for review April 8, 2026 14:18
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 24140301216

Coverage decreased (-38.9%) to 46.817%

Details

  • Coverage decreased (-38.9%) from the base build.
  • Patch coverage: 83 uncovered changes across 5 files (66 of 149 lines covered, 44.3%).
  • 85999 coverage regressions across 334 files.

Uncovered Changes

File Changed Covered %
clarity/src/vm/database/sqlite.rs 73 1 1.37%
stackslib/src/clarity_vm/clarity.rs 40 34 85.0%
clarity/src/vm/database/clarity_db.rs 29 27 93.1%
stackslib/src/clarity_vm/database/mod.rs 2 0 0.0%
clarity/src/vm/docs/mod.rs 1 0 0.0%

Coverage Regressions

85999 previously-covered lines in 334 files lost coverage.

Top 10 Files by Coverage Loss Lines Losing Coverage Coverage
stackslib/src/chainstate/stacks/db/transactions.rs 8051 7.28%
stackslib/src/chainstate/stacks/db/blocks.rs 4459 35.59%
stackslib/src/chainstate/stacks/transaction.rs 4311 9.87%
stackslib/src/chainstate/burn/db/sortdb.rs 3575 42.39%
stackslib/src/chainstate/stacks/boot/mod.rs 3429 12.78%
stackslib/src/net/chat.rs 3334 27.49%
stackslib/src/chainstate/burn/operations/leader_block_commit.rs 2465 16.34%
stackslib/src/net/db.rs 1942 29.17%
stacks-signer/src/signerdb.rs 1580 35.13%
stackslib/src/chainstate/stacks/index/test/node.rs 1317 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 217989
Covered Lines: 102056
Line Coverage: 46.82%
Coverage Strength: 12315981.16 hits per line

💛 - Coveralls

Copy link
Copy Markdown
Contributor

@federico-stacks federico-stacks left a comment

Choose a reason for hiding this comment

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

Actually moving the PR back to draft due this finding

Comment on lines +431 to +435
let mut clarity_db = datastore.as_clarity_db_with_epoch(
&NULL_HEADER_DB,
&NULL_BURN_STATE_DB,
epoch.epoch_id,
);
Copy link
Copy Markdown
Contributor

@federico-stacks federico-stacks Apr 13, 2026

Choose a reason for hiding this comment

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

the change from commit 6fd5f32, breaks some unit tests from chainstate::stacks::db::blocks::test (like process_transaction_fee_check)

I need to investigate this further.

@federico-stacks federico-stacks marked this pull request as draft April 13, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants