Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 16 Apr 00:02

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_at now return owned values (not &T). Required by the LRU cache migration — lru invalidates references on any cache touch.
  • New set_header_loader / set_score_loader / has_header_loader / has_score_loader / set_cache_capacity + HeaderLoader / ScoreLoader type 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 combining best_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