perf(migration): merge-join the headstate source instead of point reads - #4063
NazariiDenha wants to merge 2 commits into
Conversation
|
Claude encountered an error after 26s —— View job I'll analyze this and get back to you. |
7c0aedf to
06f5857
Compare
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## maksym/statehistory-migration #4063 +/- ##
=================================================================
- Coverage 79.21% 79.19% -0.02%
=================================================================
Files 472 471 -1
Lines 36089 36126 +37
=================================================================
+ Hits 28587 28610 +23
- Misses 7493 7507 +14
Partials 9 9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@NazariiDenha when writing a PR as perf, please be sure to include why is it a perf |
| batch := database.NewBatchWithSize(common.BatchByteSize) | ||
| completed := 0 | ||
|
|
||
| write := func() error { |
There was a problem hiding this comment.
Can be inlined, last step does not need to rotate the batch as well
There was a problem hiding this comment.
last step does not create new batch now, renamed to flush()
There was a problem hiding this comment.
I'd still inline this. With the batch reopen moved out, flush is just Size → Write → Log, and the two call sites genuinely differ, the tail needs neither the reset nor a new batch, so the closure hides that rather than helping.
At exit, neither of the last two statements does anything useful. completed = 0 is dead, nothing reads it again. And counter.Log doesn't help either: it only prints when more than TimeLogRate has elapsed since the previous line, so on the final call it almost always returns silently, and the last batch's bytes and contract count are dropped along with the counter. Whether that batch ever shows up in the log is timing luck.
There was a problem hiding this comment.
Add a completion log before returning, with the elapsed time since the phase started, nothing currently records how long this takes
| } | ||
| rec.classHash.SetBytes(raw) | ||
|
|
||
| // A missing nonce means the contract was never updated. |
There was a problem hiding this comment.
If I'm not mistaken every deployed contract must have nonce 0 written to head state. see
There was a problem hiding this comment.
Previous headstate migration had same check for non-existing nonce, so I left it here. I made only performance changes here
There was a problem hiding this comment.
It gives the impression that it might not exists, but if Im not mistaken it must exists. If that is the case we should make it required, either here or in the original PR
Standalone PR ReviewPR-Agent could not safely update the persistent review. This standalone result will not replace the canonical review. PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
5b4fc22 to
564f2ce
Compare
|
Claude finished @NazariiDenha's task in 12m 6s —— View job Review of PR #4063
This PR replaces the pipelined point-read ingestor with a sequential merge-join over the four address-keyed buckets ( Verified the code builds ( Important
Nit
Everything else looks solid: the |
Standalone PR ReviewPR-Agent could not safely update the persistent review. This standalone result will not replace the canonical review. PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
| for driver.addr != nil { | ||
| addr := driver.addr | ||
|
|
||
| if migrated.advanceTo(addr) { | ||
| driver.next() | ||
| continue | ||
| } | ||
|
|
||
| rec, err := resolve(driver, nonces, heights, addr) | ||
| if err != nil { | ||
| iterErr = err | ||
| return | ||
| } | ||
| if !yield(rec) { | ||
| return | ||
| } | ||
| driver.next() | ||
| } | ||
|
|
||
| iterErr = errors.Join(driver.err, nonces.err, heights.err, migrated.err) |
There was a problem hiding this comment.
Important — deferred error detection on the migrated cursor still wastes a full pass on corruption.
migrated.err (set inside advanceTo → next → set when a Contract-bucket key is malformed) is only surfaced in the final errors.Join at line 155, after the driver loop has already run to completion. Once migrated.err is set, migrated.addr becomes nil permanently, so migrated.advanceTo(addr) (line 139) returns false for every remaining address — every remaining contract is treated as "not yet migrated," resolved, and written to the batch, before the corruption is finally reported and the whole migration is failed anyway.
This was already flagged in the previous automated review pass and doesn't look like it's been addressed. Not data-corrupting (writes are idempotent/re-runnable), but it turns a cheap early-exit into a full wasted pass whenever the Contract bucket has a malformed key. Consider checking migrated.err right after the advanceTo call (same pattern already used for heights.err at lines 93-95) so the failure surfaces immediately instead of after redoing the remaining work.
| if err != nil { | ||
| return rec, fmt.Errorf("reading deployment height for %s: %w", &rec.addr, err) | ||
| } | ||
| rec.height = binary.BigEndian.Uint64(raw) |
There was a problem hiding this comment.
Nit — potential panic on a malformed deployment-height value.
binary.BigEndian.Uint64(raw) will panic (index out of range) if raw is shorter than 8 bytes. cursor.set already validates that keys are the expected length before use, but there's no equivalent length check on the value read here (or in driver.it.UncopiedValue() / nonces.it.UncopiedValue() above). In the current codebase this mirrors the existing core.GetContractDeploymentHeight pattern, so it's not a regression, but since this migration already hardens against malformed keys, it might be worth doing the same for malformed values rather than trusting raw blindly — a corrupted/truncated value here would crash the migration instead of surfacing a clean error like the rest of this function.

Uh oh!
There was an error while loading. Please reload this page.