Skip to content

perf(history migration): reduce allocs, avoid decode, make resumable, tune batch size - #4070

Open
EgeCaner wants to merge 2 commits into
maksym/statehistory-migrationfrom
perf/history-migration
Open

EgeCaner wants to merge 2 commits into
maksym/statehistory-migrationfrom
perf/history-migration

Conversation

@EgeCaner

@EgeCaner EgeCaner commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

User description

Reworks the contract history migration for speed and resumability: the per-contract DeleteRange becomes one bucket wipe per phase (interleaved tombstones were quadratic in pebble, −40% on mainnet), rows move as raw bytes through reusable per-worker key buffers instead of a decode/encode round-trip, and the batch target drops to 32 MB to stay under pebble's large-batch threshold (−25% memtable stall). On cancellation the source records the first address it never handed out and Before resumes there, skipping completed phases. Output is verified byte-identical to the previous implementation on a seed covering every branch of all three phases.

Mainnet migration (377 GB snapshot, same machine):

before after
total 2636.5 s 1551.0 s / 1583.8 s (−40%)
class-hash 402.4 s 110.2 s
nonce 369.7 s 259.4 s
storage 1797.9 s 1117.8 s
memtable stall 1282.9 s 949.5 s (−25%)

Attribution from isolating each change: the bucket wipe is ≈ −1050 s, the 32 MB batch ≈ −73 s; the allocation work removes GC pressure but did not move wall-clock, which was memtable-stall bound.

Micro-benchmark (per contract; storage seeds 1024 entries, nonce 16, class-hash 17):

allocs bytes time
storage 7366 → 4 386 KB → 230 B 318 → 83 µs (−74%, p=0.000)
nonce 71 → 5 2963 → 211 B n.s. (±43–66%)
class-hash 74 → 6 3074 → 226 B −29% (p=0.015, ±57%)

Methodology. Micro: go test -bench against pebble in a temp dir, one seeded contract, 500 iterations × 8 runs, parent commit vs HEAD run sequentially and compared with benchstat. Mainnet: the full migration on a snapshot restored to the same pristine state before every run, phase times from the migration log, stall from db_write_stall{type="memtable"}, each configuration run twice.


PR Type

Enhancement, Tests


Description

  • Optimize migration with zero-allocation byte buffers.

    • Implement reusable historyScratch buffers.
    • Add UncopiedKey to DB iterators.
  • Enable migration resumability across phases.

    • Persist current phase and last unprocessed address.
    • Skip completed phases on resume.
  • Reduce Pebble batch target size to 32MB.

    • Mitigates memtable stalls.
  • Wipe deprecated buckets per-phase instead of per-contract.

    • Greatly reduces the number of tombstones.

File Walkthrough

Relevant files
Enhancement
8 files
iterator.go
Add `UncopiedKey` method to Iterator interface                     
+9/-0     
iterator.go
Implement `UncopiedKey` in memory iterator                             
+18/-0   
iterator.go
Implement `UncopiedKey` in pebble iterator                             
+6/-0     
iterator.go
Implement `UncopiedKey` in pebblev2 iterator                         
+6/-0     
iterator.go
Implement `UncopiedKey` in remote iterator                             
+6/-0     
keys.go
Introduce `historyScratch` buffers and byte manipulators 
+141/-0 
migrator.go
Implement resumability and per-phase bucket wiping             
+154/-82
migrator.go
Add timing logs for overall migration and phases                 
+12/-0   
Configuration changes
1 files
constants.go
Reduce batch target sizes to avoid memtable stalls             
+10/-2   
Refactoring
4 files
ingestor.go
Remove context awareness from `BaseIngestor` flush             
+11/-19 
ingestor.go
Update `newIngestor` signature to remove context                 
+1/-3     
migrator.go
Update `newIngestor` call to reflect signature change       
+1/-1     
parse.go
Remove deprecated parse functions in favor of raw bytes   
+0/-28   
Performance
3 files
class_hash_ingestor.go
Optimize class-hash migration using raw byte buffers         
+63/-88 
nonce_ingestor.go
Optimize nonce migration using raw byte buffers                   
+40/-35 
storage_ingestor.go
Optimize storage migration using raw byte buffers               
+86/-114
Tests
3 files
keys_test.go
Add tests for history key operations and record reading   
+138/-0 
migrator_test.go
Update and expand history migrator tests for resumability
+100/-28
resume_test.go
Add tests to validate migration interruption and resume logic
+167/-0 

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Review in progress

  • Gather context (PR description, diff, changed files)
  • Read repo CLAUDE.md for conventions
  • Review db/iterator interface changes (UncopiedKey etc.)
  • Review migration/state/newstate/internal/history changes (migrator, ingestors, keys, resume)
  • Review tests (keys_test, migrator_test, resume_test)
  • Post findings as inline comments + summary

View job run

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.66667% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.21%. Comparing base (ca4ebb2) to head (f0322e9).

Files with missing lines Patch % Lines
...tate/newstate/internal/history/storage_ingestor.go 69.09% 17 Missing ⚠️
...e/newstate/internal/history/class_hash_ingestor.go 80.00% 8 Missing ⚠️
.../state/newstate/internal/history/nonce_ingestor.go 72.41% 8 Missing ⚠️
...ration/state/newstate/internal/history/migrator.go 92.85% 4 Missing ⚠️
db/memory/iterator.go 66.66% 2 Missing ⚠️
db/pebble/iterator.go 0.00% 1 Missing ⚠️
db/pebblev2/iterator.go 0.00% 1 Missing ⚠️
db/remote/iterator.go 0.00% 1 Missing ⚠️
...gration/state/newstate/internal/common/ingestor.go 50.00% 1 Missing ⚠️
migration/state/newstate/internal/history/keys.go 97.29% 1 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                       @@
##           maksym/statehistory-migration    #4070   +/-   ##
==============================================================
  Coverage                          79.21%   79.21%           
==============================================================
  Files                                472      471    -1     
  Lines                              36089    36130   +41     
==============================================================
+ Hits                               28587    28620   +33     
- Misses                              7493     7501    +8     
  Partials                               9        9           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 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.

@NazariiDenha NazariiDenha left a 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.

One more thing is resumability in case of error

For example, if we encounter error during nonce phase - the intermediate state will be nil and migrator will start again from first phase class-hash. deprecated bucket will be wiped at that time and nothing happens for that phase, but still it doesn't store actual state.

Probably, there is the same problem in outer state migration with headstate, history and trie phases

deprecatedPrefix := db.DeprecatedContractClassHashHistoryKey(addrFelt)
contract, err := state.GetContract(i.Database, addrFelt)
contractKey := fillAddressKey(scratch.contractKey[:], db.Contract, &addr)
headClassHash, deployHeight, err := readHeadClassHash(i.Database, contractKey)

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.

we already read head contract data when producing input in addressSeq and could pass some kind of headRecord instead of addr from addressSeq here and in nonce_ingestor. This could eliminate 2 random point reads per contract which gave significant improve on headstate migration, but maybe here it will not give that much

@NazariiDenha

Copy link
Copy Markdown
Contributor

My results

phase before after improve
total 1896.7 s 1674.8 s  −11.7%
class-hash 117 s 48,3 s -59%
nonce 302.7 s 144.4 s -52%
storage 1477 s 1482 s 0

Strange that I didn't get any improve on storage phase

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.

2 participants