Fix: [state-prune snapshot] Switch traversal worklist from BFS (VecDeque) to DFS (Vec) to reduce peak memory#3887
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Pull request overview
This pull request converts the state pruner snapshot builder's traversal algorithm from BFS (using VecDeque) to DFS (using Vec as a stack) to reduce peak memory usage. The change targets the worklist data structure that tracks nodes to be processed during SMT traversal.
Key Changes:
- Removed
VecDequeimport and replaced all queue operations with stack operations - Updated
save_progressmethod signature to accept&Vec<H256>instead of&VecDeque<H256> - Modified traversal loop to use
pop()instead ofpop_front()andpush()instead ofpush_back()
| // For DFS with Vec (stack), we need to reverse the logic | ||
| // worklist_position represents nodes already processed from the front | ||
| // For Vec used as stack, we need to truncate to keep only unprocessed nodes |
There was a problem hiding this comment.
The comment is misleading. The comment states "For DFS with Vec (stack), we need to reverse the logic" and "worklist_position represents nodes already processed from the front", but this is inconsistent with the actual save_progress implementation. Looking at line 323, save_progress always sets worklist_position to 0, which means no nodes from the worklist are considered "already processed". The worklist at the time of save contains nodes that are yet to be processed (they would be popped from the end). This comment should be updated to accurately reflect that worklist_position is always 0 for the current implementation, and the logic should either use the worklist as-is or be completely redesigned if partial worklist processing is needed.
There was a problem hiding this comment.
✅ FIXED: Thank you for the careful analysis! You're absolutely right. The comment was misleading and inconsistent with the actual save_progress implementation. I've removed the misleading comment and the unnecessary resume logic. The new comment correctly explains that for DFS with Vec (stack), save_progress always sets worklist_position to 0, and the entire worklist contains nodes yet to be processed (popped from the end).
Action taken: Removed misleading comment (lines 362-364) and replaced it with accurate explanation that save_progress always sets worklist_position to 0 for DFS with Vec stack
| // For DFS with Vec (stack), we need to reverse the logic | ||
| // worklist_position represents nodes already processed from the front | ||
| // For Vec used as stack, we need to truncate to keep only unprocessed nodes | ||
| if progress.worklist_position > 0 && progress.worklist_position < worklist.len() { | ||
| worklist = worklist.split_off(progress.worklist_position); | ||
| } |
There was a problem hiding this comment.
The resume logic for the worklist is broken. Looking at line 323 in save_progress, worklist_position is always set to 0 with the comment "We've processed everything before current position". This means when resuming, progress.worklist_position will always be 0, so the condition on line 365 will never be true (0 is not > 0), and split_off will never execute.
The entire resume logic needs to be reconsidered for DFS. In the original BFS implementation, worklist_position tracked how many nodes from the front of the queue had been processed, which made sense with pop_front. For DFS with a stack (Vec), when we save progress, the worklist contains nodes yet to be processed (popped from the end with pop()), so worklist_position should remain 0 and no truncation should happen during resume. The current code is attempting to handle a scenario that doesn't match the save_progress implementation.
There was a problem hiding this comment.
✅ FIXED: Excellent catch! You're completely correct that the resume logic was broken. Since save_progress always sets worklist_position to 0 (line 323), the condition progress.worklist_position > 0 would never be true, making the split_off logic dead code. I've removed this entire dead code block. The worklist is now restored directly without any truncation, which is correct because the worklist at save time contains all unprocessed nodes that will be popped from the end during DFS traversal.
Action taken: Removed unnecessary conditional logic (lines 365-367) that checked worklist_position > 0 and called split_off, as this was dead code since worklist_position is always 0
|
@holonbot fix |
PR #3887 Fix SummaryOverviewPR Title: Fix: [state-prune snapshot] Switch traversal worklist from BFS (VecDeque) to DFS (Vec) to reduce peak memory PR Number: #3887 Status: ✅ Complete - All lint errors fixed Issues Fixed1. Clippy Lint Error:
|
|
Holon completed successfully. Run: https://github.com/rooch-network/rooch/actions/runs/20571795348 |
|
@holonbot fix the lint error |
|
Holon completed successfully. Run: https://github.com/rooch-network/rooch/actions/runs/20639354313 |
…que) to DFS (Vec) to reduce peak memory
cfff6e0 to
31f7c06
Compare
|
@holonbot we already provide the base image with rust toolchain, please try again. |
|
Holon completed successfully. Run: https://github.com/rooch-network/rooch/actions/runs/20639924994 |
Fixes #3872
Summary: Switch from BFS (VecDeque) to DFS (Vec) for State Prune Snapshot Traversal
Issue Reference
GitHub Issue: #3872
Parent Issue: #3858
Objective
Reduce peak memory usage in
SnapshotBuildertraversal by switching from BFS (usingVecDeque) to DFS (usingVecstack). This bounds the worklist size closer to O(depth) rather than O(width) for large and/or wide SMT graphs.Changes Made
File Modified:
crates/rooch-pruner/src/state_prune/snapshot_builder.rs1. Removed VecDeque Import (Line 15)
use std::collections::VecDeque;2. Updated
save_progressMethod Signature (Line 314)worklist: &VecDeque<H256>worklist: &Vec<H256>worklist.iter().cloned().collect()toworklist.clone()3. Updated
stream_traverse_and_writeMethod (Lines 353-444)Resume Logic (Lines 359-367):
VecDeque::from(progress.worklist)withpop_front()in a loop to skip processed nodesprogress.worklistwithsplit_off()to remove processed nodes from the frontFresh Start (Lines 388-391):
VecDeque::new()withpush_back(state_root)Vec::new()withpush(state_root)Main Traversal Loop (Line 409):
while let Some(current_hash) = nodes_to_process.pop_front()while let Some(current_hash) = nodes_to_process.pop()Child Node Processing (Lines 441-444):
nodes_to_process.push_back(child_hash)nodes_to_process.push(child_hash)with updated comment "DFS: push to stack"Semantic Correctness
The change from BFS to DFS maintains snapshot completeness because:
snapshot_writer.contains_node()) ensures each node is written only onceMemory Improvement
Acceptance Criteria Met
Vec) instead ofVecDequequeueVecDequeallocations or operations remainTesting Notes
Due to the Holon sandbox environment lacking the Rust toolchain in PATH, the following verifications could not be completed:
make lint- Rust formatting check (cargo not available)cargo test --package rooch-pruner- Unit tests (cargo not available)Recommendation: When the PR is created, ensure CI checks pass which will verify:
Technical Notes
Progress Resumption: The resume logic was adapted because Vec doesn't have a
pop_front()method. Instead, we usesplit_off()to remove already-processed nodes from the front of the vector when resuming.Traversal Order: While the traversal order changes (BFS vs DFS), this does not affect correctness because:
Comment Update: Added "DFS: push to stack" comment to clarify the new traversal behavior.