v0.3.0
Highlights
Peak RSS on mainnet sync: 14.95 GB → ~9.5 GB (−37%), no observable impact on sync throughput, no on-disk state format changes, no resync required.
Two orthogonal wins compose to deliver it: a memory-aware flush dial on the validator sweep, and a lazy header store in the chain crate. Added observability via /debug/memory so the dial can be tuned against real measurements.
Memory
Memory-aware flush dial (ergo-sync)
The validator sweep used to flush the redb write transaction every 100 blocks (hardcoded). Profiling identified RedbAVLStorage::update_internal's write-tx dirty-page Arc<[u8]> cache as the dominant live-heap allocator during sync — those Arcs accumulate between flushes and drive peak RSS.
Flush cadence is now a runtime-configurable dial against jemalloc's live-heap counter:
[node]
flush_heap_threshold_mb = 4096 # flush when live heap >= 4 GB
flush_max_blocks = 100 # upper guardrail (crash-recovery bound)
flush_min_blocks = 5 # lower guardrail (prevents storm flushing)Defaults preserve the prior 100-block cadence as the upper bound. Under mimalloc or other non-jemalloc allocators the probe returns None and the policy degenerates cleanly to count-based via flush_max_blocks.
Lazy header store (enr-chain)
HeaderChain's in-memory Vec<Header> is retired. Headers live in a bounded LRU cache (default capacity 16 384 — sized to cover the mainnet difficulty walk and a full finalization-depth reorg with slack), with cache misses falling through to a HeaderLoader wired by the main crate against enr-store::read_header_at.
At 1.76 M mainnet headers that's a straight -1.4 GB of live heap.
Observability
GET /debug/memory
REST endpoint for live memory composition, always on:
{
"process": { rssAnonBytes, rssFileBytes, rssTotalBytes,
rssPeakBytes, vmSizeBytes, pssBytes },
"jemalloc": { allocatedBytes, activeBytes, residentBytes,
retainedBytes, metadataBytes }, // null under mimalloc
"components": { chainHeaderEstimateBytes, chainHeaderCount,
mempoolTxCount }
}
Sample it periodically to track peak RSS vs live heap, watch flush-dial behavior, or compare jemalloc.allocated against process.rssAnon to quantify allocator overhead without spinning up jeprof/heaptrack/dhat.
jemalloc-prof cargo feature
New opt-in build (cargo build --release --no-default-features --features jemalloc-prof) enables jemalloc's built-in heap profiler. Activate at runtime via _RJEM_MALLOC_CONF=prof:true,prof_prefix:/var/log/.../jeprof,... and read dumps with jeprof. For field profiling when /debug/memory's aggregate numbers aren't enough.
Contract changes
enr-chain
header_at/tip/headers_from/score_atnow return owned values (not&T). Required by the LRU cache migration —lruinvalidates references on any cache touch.- New
set_header_loader/set_score_loader/has_header_loader/has_score_loader/set_cache_capacity+HeaderLoader/ScoreLoadertype aliases +DEFAULT_CACHE_CAPACITY. - Write-through cache invariant documented:
push_header,pop_header,restore_header,install_from_nipopow_proof, and deep-reorg drain/restore paths all update the cache in lockstep with the canonical state.
enr-store
- New
ModifierStore::read_header_at(height)— single-read-transaction combiningbest_header_at+get(101, id). Wired by the main crate to the chain's header loader.
Compatibility
- No consensus changes.
- No on-disk state format changes. Existing nodes resume against their persisted state. No resync required.
- Default config values preserve prior behavior as upper bounds; enabling the new flush dial is the default (threshold 4 GB), but the effective cadence only tightens vs the prior hardcoded 100-block flush.
Full Changelog: v0.2.0...v0.3.0