Uh oh!
There was an error while loading. Please reload this page.
Fix native OOM from unbounded CallTraceHashTable expansion - #670
Conversation
Benchmark Results (commit 75285e9)Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/125374352 Commit: ✅ Within expected boundariesNo significant runtime deltas (all within run-to-run noise) and no internal-counter outliers. Runtime details (per benchmark × JDK)
Internal counter details (ddprof)ddprof internal counters, latest / dev (✅ = 0, · = unavailable):
|
zhengyu123
commented
Jul 20, 2026
Could you rebase the PR? seems it has conflicts. |
CallTraceHashTable grows its backing LongHashTable by doubling capacity from a LinearAllocator whose chunks are a fixed 8MB. Once a table exceeds one chunk (capacity >= 512K entries), LinearAllocator::alloc() can never place it: the bump-allocator loops allocating fresh chunks that still cannot fit the oversized request, until mmap fails - a native OOM / vm.max_map_count exhaustion, not a heap-space error. This is reachable under live-heap profiling (memory=...:l) with a long, unrotated recording, where the active table grows past that threshold between processTraces() rotations. Cap expansion at the largest table that fits in one allocator chunk (LinearAllocator::maxAllocatableSize()); further growth degrades put() to a higher load factor and recovers on the next rotation. Deriving the bound from the chunk size keeps it correct if the chunk size ever changes. Also fix a per-thread native leak in LivenessTracker's subsampling RNG state: the gen/dis/skipped ThreadLocals were reclaimed only via pthread-key destructors, which never fire when a JNI-attached thread detaches on a reused OS thread. Release them explicitly from Profiler::onThreadEnd(), matching how the CPU/wall engines and ProfiledThread are already torn down. Environment: Datadog workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75285e9 to
c9d17a9CompareCI Test ResultsRun:#29814088134 | Commit:
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Summary: Total: 32 | Passed: 32 | Failed: 0 Updated: 2026-07-21 09:05:25 UTC |
…abot Dependabot bumped analyze to v4.37.1 without init, causing CodeQL's "Loaded a configuration file for version X, but running version Y" error.
Benchmark Results (commit d1d5076)Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/125698320 Commit:
|
| Benchmark | JDK | Latest | Dev | Δ (dev vs latest) | Issues L/D |
|---|---|---|---|---|---|
| akka-uct | 21 | ✅ 10276 ms (21 iters) | ✅ 10386 ms (21 iters) | ≈ +1.1% (±10.9%) | — / — |
| akka-uct | 25 | ✅ 8869 ms (24 iters) | ✅ 8909 ms (24 iters) | ≈ +0.5% (±10.4%) | — / — |
| finagle-chirper | 21 | ✅ 5960 ms (33 iters) | ✅ 5984 ms (33 iters) | ≈ +0.4% (±25.1%) | |
| finagle-chirper | 25 | ✅ 5462 ms (36 iters) | ✅ 5542 ms (36 iters) | ≈ +1.5% (±24.9%) | |
| fj-kmeans | 21 | ✅ 2663 ms (70 iters) | ✅ 2825 ms (66 iters) | 🔴 +6.1% | — / — |
| fj-kmeans | 25 | ✅ 2834 ms (66 iters) | ✅ 2818 ms (66 iters) | ≈ -0.6% (±2.6%) | — / — |
| future-genetic | 21 | ✅ 2058 ms (90 iters) | ✅ 2044 ms (90 iters) | ≈ -0.7% (±2.5%) | — / — |
| future-genetic | 25 | ✅ 2022 ms (91 iters) | ✅ 2124 ms (87 iters) | 🔴 +5% | — / — |
| naive-bayes | 21 | ✅ 1233 ms (138 iters) | ✅ 1245 ms (137 iters) | ≈ +1% (±33.1%) | — / — |
| naive-bayes | 25 | ✅ 1009 ms (169 iters) | ✅ 1010 ms (169 iters) | ≈ +0.1% (±31.5%) | — / — |
| reactors | 21 | ✅ 16270 ms (15 iters) | ✅ 15718 ms (15 iters) | ≈ -3.4% (±6%) | — / — |
| reactors | 25 | ✅ 19058 ms (15 iters) | ✅ 18294 ms (15 iters) | ≈ -4% (±4.9%) | — / — |
Internal counter details (ddprof)
ddprof internal counters, latest / dev (✅ = 0, · = unavailable):
| Benchmark | JDK | Dropped rec | Dropped jvmti | Dropped trace | Skipped WC | AGCT fail | Unwind fail |
|---|---|---|---|---|---|---|---|
| akka-uct | 21 | ✅ / ✅ | ✅ / ✅ | 2 / 3 | 2042 / 1952 | ✅ / ✅ | ✅ / ✅ |
| akka-uct | 25 | ✅ / ✅ | ✅ / ✅ | 2 / 4 | 2251 / 2204 | ✅ / ✅ | ✅ / ✅ |
| finagle-chirper | 21 | ✅ / ✅ | ✅ / ✅ | 4 / 3 | 8718 / 8771 | ✅ / ✅ | ✅ / ✅ |
| finagle-chirper | 25 | ✅ / ✅ | ✅ / ✅ | ✅ / 2 | 8527 / 8342 | ✅ / ✅ | ✅ / ✅ |
| fj-kmeans | 21 | ✅ / ✅ | ✅ / ✅ | ✅ / ✅ | ✅ / ✅ | ✅ / ✅ | ✅ / ✅ |
| fj-kmeans | 25 | ✅ / ✅ | ✅ / ✅ | 1 / 4 | 1270 / 1292 | ✅ / ✅ | ✅ / ✅ |
| future-genetic | 21 | ✅ / ✅ | ✅ / ✅ | 2 / 1 | 3042 / 2917 | ✅ / ✅ | ✅ / ✅ |
| future-genetic | 25 | ✅ / ✅ | ✅ / ✅ | ✅ / ✅ | 2833 / 2919 | ✅ / ✅ | ✅ / ✅ |
| naive-bayes | 21 | ✅ / ✅ | ✅ / ✅ | 7 / 7 | 3462 / 3491 | ✅ / ✅ | ✅ / ✅ |
| naive-bayes | 25 | ✅ / ✅ | ✅ / ✅ | 4 / 3 | 3463 / 3490 | ✅ / ✅ | ✅ / ✅ |
| reactors | 21 | ✅ / ✅ | ✅ / ✅ | 1 / ✅ | 1696 / 1521 | ✅ / ✅ | ✅ / ✅ |
| reactors | 25 | ✅ / ✅ | ✅ / ✅ | 2 / ✅ | 1978 / 1919 | ✅ / ✅ | ✅ / ✅ |
Reliability & Chaos Results✅ All reliability & chaos checks passed Pipeline: https://gitlab.ddbuild.io/DataDog/java-profiler/-/pipelines/125698277 |
kaahos
left a comment
There was a problem hiding this comment.
apart from the missing copyrights, it looks good to me!
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
🔄 Datadog auto-retried 2 jobs - 2 passed on retry 🔗 Commit SHA: b159f4e | Docs | Datadog PR Page | Give us feedback! |
Benchmark Results (commit b159f4e)Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/125887870 Commit:
|
| Benchmark | JDK | Latest | Dev | Δ (dev vs latest) | Issues L/D |
|---|---|---|---|---|---|
| akka-uct | 21 | ✅ 10319 ms (21 iters) | ✅ 10341 ms (21 iters) | ≈ +0.2% (±11.1%) | — / — |
| akka-uct | 25 | ✅ 8856 ms (24 iters) | ✅ 8805 ms (24 iters) | ≈ -0.6% (±10.3%) | — / — |
| finagle-chirper | 21 | ✅ 5940 ms (33 iters) | ✅ 5938 ms (33 iters) | ≈ -0% (±25.7%) | |
| finagle-chirper | 25 | ✅ 5533 ms (35 iters) | ✅ 5496 ms (36 iters) | ≈ -0.7% (±24.5%) | |
| fj-kmeans | 21 | ✅ 2784 ms (67 iters) | ✅ 2770 ms (67 iters) | ≈ -0.5% (±2.7%) | — / — |
| fj-kmeans | 25 | ✅ 2774 ms (67 iters) | ✅ 2819 ms (66 iters) | ≈ +1.6% (±2.7%) | — / — |
| future-genetic | 21 | ✅ 2095 ms (88 iters) | ✅ 2112 ms (87 iters) | ≈ +0.8% (±2.9%) | — / — |
| future-genetic | 25 | ✅ 2074 ms (90 iters) | ✅ 2008 ms (93 iters) | 🟢 -3.2% | — / — |
| naive-bayes | 21 | ✅ 1267 ms (135 iters) | ✅ 1231 ms (139 iters) | ≈ -2.8% (±32.3%) | — / — |
| naive-bayes | 25 | ✅ 1010 ms (169 iters) | ✅ 1015 ms (168 iters) | ≈ +0.5% (±31.7%) | — / — |
| reactors | 21 | ✅ 15746 ms (15 iters) | ✅ 16228 ms (15 iters) | ≈ +3.1% (±6.1%) | — / — |
| reactors | 25 | ✅ 17941 ms (15 iters) | ✅ 18765 ms (15 iters) | ≈ +4.6% (±5.6%) | — / — |
Internal counter details (ddprof)
ddprof internal counters, latest / dev (✅ = 0, · = unavailable):
| Benchmark | JDK | Dropped rec | Dropped jvmti | Dropped trace | Skipped WC | AGCT fail | Unwind fail |
|---|---|---|---|---|---|---|---|
| akka-uct | 21 | ✅ / ✅ | ✅ / ✅ | 1 / ✅ | 1925 / 2009 | ✅ / ✅ | ✅ / ✅ |
| akka-uct | 25 | ✅ / ✅ | ✅ / ✅ | 2 / 3 | 2253 / 2239 | ✅ / ✅ | ✅ / ✅ |
| finagle-chirper | 21 | ✅ / ✅ | ✅ / ✅ | 2 / 2 | 8371 / 8786 | ✅ / ✅ | ✅ / ✅ |
| finagle-chirper | 25 | ✅ / ✅ | ✅ / ✅ | 2 / 2 | 8047 / 8448 | ✅ / ✅ | ✅ / ✅ |
| fj-kmeans | 21 | ✅ / ✅ | ✅ / ✅ | 1 / 4 | 1252 / 1245 | ✅ / ✅ | ✅ / ✅ |
| fj-kmeans | 25 | ✅ / ✅ | ✅ / ✅ | 4 / ✅ | 1284 / 1261 | ✅ / ✅ | ✅ / ✅ |
| future-genetic | 21 | ✅ / ✅ | ✅ / ✅ | 1 / ✅ | 2935 / 2848 | ✅ / ✅ | ✅ / ✅ |
| future-genetic | 25 | ✅ / ✅ | ✅ / ✅ | 3 / 1 | 2984 / 2948 | ✅ / ✅ | ✅ / ✅ |
| naive-bayes | 21 | ✅ / ✅ | ✅ / ✅ | 3 / 6 | 3512 / 3484 | ✅ / ✅ | ✅ / ✅ |
| naive-bayes | 25 | ✅ / ✅ | ✅ / ✅ | 5 / 4 | 3469 / 3505 | ✅ / ✅ | ✅ / ✅ |
| reactors | 21 | ✅ / ✅ | ✅ / ✅ | 4 / 1 | 1562 / 1503 | ✅ / ✅ | ✅ / ✅ |
| reactors | 25 | ✅ / ✅ | ✅ / ✅ | 2 / ✅ | 1806 / 1940 | ✅ / ✅ | ✅ / ✅ |
Problem
Under live-heap profiling (
memory=...:l) with a long-running, unrotated recording, the profiler can exhaust native address space and crash withos::commit_memory ... failed; error='Not enough space' (errno=12)/pthread_create failed (EAGAIN). The failure isvm.max_map_count(VMA) exhaustion, not RAM: a captured memory map showed tens of thousands of 8 MB (and 8 MB-multiple) anonymous regions.Root cause
CallTraceHashTablegrows its backingLongHashTableby doubling capacity, allocating each generation from aLinearAllocatorwhose chunks are a fixed 8 MB (CALL_TRACE_CHUNK). Once a table's byte size exceeds one chunk (capacity ≥ ~512K entries),LinearAllocator::alloc()can never place it: the bump-allocator's inner loop only fitsoffs + size <= _chunk_size, so an oversized request falls through togetNextChunk(), which allocates a fresh 8 MB chunk that also can't fit it — looping and allocating chunks untilmmapfails.The active table only grows this large between
processTraces()rotations (which reset it to the initial capacity). In a single start-to-end JFR recording no rotation happens, so under the high distinct-trace volume that live-heap sampling sustains, the table crosses the one-chunk threshold and the allocator runs away.Fix
expandTableIfNeeded()now refuses to expand when the next generation'sgetSize()would exceedLinearAllocator::maxAllocatableSize()(one chunk minus its header). Instead of an unbounded allocation loop,put()keeps working on the current table at a higher load factor; the next rotation resets it. The bound is derived from the chunk size, so it stays correct ifCALL_TRACE_CHUNKchanges. A newcalltrace_storage_expansion_skippedcounter records when the cap engages.This also includes a related per-thread native leak fix in
LivenessTracker: its subsampling RNGThreadLocals (gen/dis/skipped) were reclaimed only via pthread-key destructors, which never fire when a JNI-attached thread detaches on a reused OS thread. They are now released explicitly fromProfiler::onThreadEnd(), matching how the CPU/wall engines andProfiledThreadare already torn down.Validation
Reproducer (
renaissance akka-uct -r 20undermemory=64:l) OOM'd at ~90 s / 13 iterations before the fix. With the fix it completes all 20 iterations with peak VMA count in the hundreds (was pinned at the 65530 limit). Compiles clean; verified the change is compatible with the existingexpandTableIfNeeded/overflow-guard unit tests by inspection (the new check sits alongside the existing guards and only engages for tables that exceed one chunk).🤖 Generated with Claude Code