Repository navigation
fix(debug-trace-server): run on jemalloc so cache churn stops inflating RSS - #231
Merged
Merged
Conversation
…ng RSS Under response- and block-data-cache churn, glibc malloc keeps freed memory in its per-thread arenas: production RSS sat at 2.5-3x the caches' accounted bytes and kept rising while the live heap stayed flat. In a side-by-side lab run (same binary, same load, 512 MB + 512 MB caches churned over 12k blocks) glibc ended ~957 MiB above the accounted bytes and still climbing, while jemalloc stayed within ~25 MiB of them. - Set tikv-jemallocator 0.7 (jemalloc 5.3.1, matching mega-reth) as the global allocator on unix, with override_allocator_on_supported_platforms so the C dependencies (zstd, aws-lc) use it too. - Document the allocator choice in README and AGENTS.md, and note that RSS growth with only chain sync running is redb's page cache (1 GiB default), which is bounded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RSS alone cannot tell a leak from allocator retention; the glibc diagnosis
had to infer it from RSS minus the caches' accounted bytes. Publish
jemalloc's own accounting so the two separate on the dashboard.
- Add tikv-jemalloc-ctl (stats) and sample stats.{allocated,active,
metadata,resident,mapped,retained} every 10s into
debug_trace_jemalloc_*_bytes gauges, matching the set mega-reth exports.
- Sample on a dedicated thread rather than a runtime task, so inline EVM
tracing saturating the workers cannot stall it.
- Test that the sample carries real, consistently ordered values, which
guards the stats feature and jemalloc being the active allocator.
- Document the gauges in README and AGENTS.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ler docs - Drop the cfg(unix) gates around the allocator, the jemalloc sampler and its test, and move the two jemalloc crates into the plain dependency list: the binary is already unix-only (timing.rs calls clock_gettime(CLOCK_THREAD_CPUTIME_ID) ungated), so the gates guarded a build that cannot exist. Hoist the sampler's imports accordingly. - Correct the stated reason for the dedicated sampling thread: the Prometheus exporter is spawned on the main runtime, so the thread cannot keep the gauges readable under saturation; it keeps jemalloc's epoch refresh, which merges arena stats under their locks, off async workers. - Name the thread in its spawn error, as admin.rs does. - Trim doc text that repeated the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…threads on Linux Without background threads a jemalloc arena returns freed pages only while it keeps allocating, so memory freed by a burst stays resident in every arena that then goes quiet (32 cores -> 128 arenas). On tmp-0, the same load (~500k callTracer + 50k prestateTracer requests over 1000 cached blocks) left RSS at ~1.8 GiB after a 2-minute idle with the live heap at ~630 MiB; with background_thread:true RSS fell back to ~0.7 GiB. - Enable tikv-jemallocator's `background_threads` in a Linux-only target table: macOS jemalloc has no background threads and only prints a warning at every startup. MALLOC_CONF=background_thread:false still turns them off. - Pin it with a Linux-only unit test, and document it in README/AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit d636173)
Troublor
force-pushed
the
liquan/fix/dts-jemalloc-allocator
branch
from
October 19, 2026 13:13
8f905ce to
ca49039
Compare
…c sampler Merge origin/main to resolve the README conflict: keep main's witness-accounting lines alongside the jemalloc lines, and keep the redb page-cache note in the combined-memory line. Harden and simplify the jemalloc sampler: - Resolve the stat MIBs once and hold the stats as (gauge, MIB) pairs. - Take the first sample at startup; a jemalloc that cannot report statistics disables sampling with one warning instead of logging up to six WARNs every 10s. Later failures log at debug. - Check the compiled-in malloc_conf in jemalloc_purges_in_the_background, so MALLOC_CONF=background_thread:false no longer fails it, and move the test into main.rs next to the allocator setup. - Drop the stat ordering assertions, which a concurrent allocation in another test can briefly violate. - Log the effective allocator config (version, compiled-in conf, background_thread, narenas) at startup, regardless of --metrics-enabled. - Strip jemalloc's NUL terminator through one jemalloc_str helper. - Drop the redundant background_threads_runtime_support feature, which tikv-jemalloc-ctl's default sys features already enable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Claude review status
🛠️ Review did not finish Attempted This round did not publish: MODEL_SESSION_LIMIT: Claude session limit reached (5-hour quota).. Anything listed below is from the last round that did. Workflow run: https://github.com/megaeth-labs/stateless-validator/actions/runs/39753317204 |
flyq
marked this pull request as ready for review
October 22, 2026 08:56
clippy::double_ended_iterator_last fires on Linux, where the test compiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vincent-k2026
approved these changes
Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Switch
debug-trace-serverfrom glibc malloc to jemalloc so that response- and block-data-cache churn no longer inflates RSS.On Linux, jemalloc's background purge threads are also turned on, so memory freed by a load burst goes back to the OS.
jemalloc's own heap accounting is exported as metrics, so live heap and allocator-retained memory can be told apart on dashboards.
In production on ORE, with both caches full and evicting, RSS now sits at 1.02× the caches' accounted bytes (36.75 GiB vs 35.88 GiB) and has stayed flat for about 1.5 days.
The glibc build on the same host reached 2.53× (90.9 GiB vs 36.0 GiB) and was still climbing.
Under cache churn, glibc malloc keeps freed memory in its per-thread arenas instead of returning it to the OS.
In production, RSS sat at 2.5–3× the caches' accounted bytes (ORE 90 GiB vs 36.5 GiB, FRA 85 GiB vs 28.5 GiB) and kept rising, while the live heap stayed flat.
A side-by-side lab run on v2.0.18 used the same binary and the same load, with only the allocator swapped (jemalloc 5.3.0 via
LD_PRELOAD): 512 MB response cache plus 512 MB block-data cache, churned over 12k historical blocks for about 470k executions each, at a ~57% hit rate.Heap profiling ruled out a code leak.
Cache hits, transaction execution, R2 witness fetches and connections all hold steady.
The only live-object growth comes from chain sync, and it is redb's page cache, which is bounded.
Changes
Allocator
tikv-jemallocator0.7 (jemalloc 5.3.1, the same version mega-reth uses) the binary's#[global_allocator].override_allocator_on_supported_platformsso the C dependencies (zstd, aws-lc) also allocate through jemalloc on Linux.cfg(unix)gating: the binary is already unix-only (timing.rscallsclock_gettime(CLOCK_THREAD_CPUTIME_ID)ungated).Allocator: jemallocline with the jemalloc version, the compiled-inmalloc_conf,background_threadandnarenas, whether or not metrics are enabled, so aMALLOC_CONFoverride shows up in the logs.Background purge threads (Linux)
background_threadsin a Linux-only target table.macOS jemalloc has no background threads and only prints a warning at every startup, so the feature is not enabled there.
So memory freed by a burst stays resident in every arena that then goes quiet; a 32-core host has 128 arenas.
MALLOC_CONF=background_thread:false.jemalloc_purges_in_the_background, pins the feature.It reads the compiled-in
malloc_confrather than the live setting, so aMALLOC_CONF=background_thread:falseenvironment does not fail it.Metrics
--metrics-enabled, sample jemalloc'sstats.{allocated,active,metadata,resident,mapped,retained}every 10s intodebug_trace_jemalloc_*_bytesgauges, the same set mega-reth exports.This adds
tikv-jemalloc-ctlwith itsstatsfeature, which builds jemalloc with statistics enabled.allocatedis the live heap andresidentthe physical memory jemalloc holds, so a gap that widens whileallocatedstays flat is allocator retention rather than a leak.If jemalloc cannot report statistics, sampling is disabled with a single warning instead of failing every 10s.
The
/metricsexporter itself still runs on the main runtime, so this does not keep the gauges readable while tracing saturates it.Docs
Database::createdefaults to 1 GiB): bounded, not a leak.Testing
Local
cargo fmt --all --check,cargo sort --check …andcargo clippy --workspace --all-targets --all-featurespass with no warnings.cargo test -p debug-trace-server: all 207 unit tests pass on macOS.allocator_metrics_sample_the_live_heapguards thestatsfeature and jemalloc being the active allocator.Allocator: jemallocstartup line and all sixdebug_trace_jemalloc_*_bytesgauges on/metrics.tmp-0 (Ubuntu 24.04, release build,
--locked)The binary exports
malloc/free/calloc/realloc, andLD_DEBUG=bindingsshows glibc's ownmalloc/freebinding to them.Four
jemalloc_bg_thdthreads start without anyMALLOC_CONF.consistency_test.rs:callTracer,trace_block,prestateTracer, by-hash,debug_traceTransactionandtrace_transactionwere all identical, with no dts errors.4byteTracer(mega-reth answers with struct logs; the gateway rejects this tracer anyway),prestateTracerdiffMode(empty{}post entries, same in v2.0.20), and theindex/"refund":0fields that chore: upgrade to reth v2.3.0 / revm 40 / alloy 2.0.5 #184 already brought to main.Each run was ~500k
callTracerplus 50kprestateTracerrequests over 1000 cached blocks, at 25–37k req/s, with 0 errors:allocated)background_threadsProduction (ORE)
mnet-ore-compute-0: 48 cores, 182 GiB RAM, no swap, shared withrpc-node(48.4 GiB RSS).Once full, the response cache sits just under 32 GiB and the block-data cache just under 4 GiB.
Snapshots below were taken from
/proc,/metricsanddebug_getCacheStatus(times in UTC):d636173)The growth is the caches filling, not retention.
That fits glibc's growth coming from eviction churn, which only starts once a cache is full.
Since then RSS has stayed flat at about 36.7 GiB for roughly 39 hours, while both caches evict.
allocated35.2 GiB,active36.5 GiB,resident36.9 GiB,metadata0.33 GiB,mapped37.1 GiB.residentis 1.67 GiB (4.7%) aboveallocated.retained(22.1 GiB) is address space kept for reuse, not resident memory.It has served 1.11M requests, with 517k response-cache and 509k block-data misses, against 10.18M requests and 3.17M / 3.11M misses for the glibc run.
Rollout
liquan/fix/dts-jemalloc-v2.0.20atd636173: v2.0.20 plus this PR's allocator, metrics and background-thread commits, without chore: upgrade to reth v2.3.0 / revm 40 / alloy 2.0.5 #184 and without the later sampler and startup-log cleanups.debug_trace_jemalloc_*.🤖 Generated with Claude Code