Skip to content

Commit ae3daa1

Browse files
committed
chaos: add memory-pressure governor, restore Xmx to 2.5GiB
Throttles alloc-storm/direct-memory/dump-storm via cgroup memory watermarks (85%/70%) instead of trimming heap to make off-heap headroom. Validated: the jemalloc/2560m/profiler+tracer config that OOMKilled in CI (job 1858271921) now completes cleanly under a real 6GiB-capped cgroup.
1 parent be3c3bf commit ae3daa1

6 files changed

Lines changed: 137 additions & 0 deletions

File tree

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/AllocStormAntagonist.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ private void javaLoop() {
101101
buf[0] = (byte) idx;
102102
acc += buf[buf.length - 1];
103103
idx = (idx + 1) % SIZES.length;
104+
MemoryGovernor.pace();
104105
}
105106
sink.addAndGet(acc);
106107
}
@@ -116,6 +117,7 @@ private void nativeLoop() {
116117
long addr = (long) ALLOCATE_MEMORY.invoke(UNSAFE, NATIVE_BLOCK_SIZE);
117118
acc += addr;
118119
FREE_MEMORY.invoke(UNSAFE, addr);
120+
MemoryGovernor.pace();
119121
}
120122
} catch (Throwable t) {
121123
// reflective failure; JVM crash is the signal we watch for

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/DirectMemoryAntagonist.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ private void ringLoop() {
8888
}
8989
slot = (slot + 1) % RING_SIZE;
9090
sizeIdx = (sizeIdx + 1) % RING_SIZES_BYTES.length;
91+
MemoryGovernor.pace();
9192
}
9293
for (int i = 0; i < RING_SIZE; i++) {
9394
ring[i] = null;

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/DumpStormAntagonist.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ private void loop() {
7979
return;
8080
}
8181
}
82+
MemoryGovernor.pace();
8283
}
8384
}
8485

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/Main.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public static void main(String[] args) throws Exception {
4242
Args parsed = Args.parse(args);
4343
log("starting duration=" + parsed.duration + " antagonists=" + parsed.antagonists);
4444

45+
MemoryGovernor.start();
46+
4547
List<Antagonist> running = new ArrayList<>();
4648
try {
4749
for (String name : parsed.antagonists) {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*/
10+
package com.datadoghq.profiler.chaos;
11+
12+
import java.io.IOException;
13+
import java.nio.charset.StandardCharsets;
14+
import java.nio.file.Files;
15+
import java.nio.file.Path;
16+
import java.nio.file.Paths;
17+
18+
/**
19+
* Shared memory-pressure gate for antagonists that burst allocations fast
20+
* enough to threaten the container's cgroup ceiling before GC/Cleaner
21+
* catches up (alloc-storm, direct-memory, dump-storm).
22+
*
23+
* <p>A single background thread samples the cgroup memory usage/limit (v2
24+
* {@code memory.current}/{@code memory.max}, falling back to v1 {@code
25+
* memory.usage_in_bytes}/{@code memory.limit_in_bytes}) and flips a shared
26+
* {@code throttled} flag with hysteresis: entering throttle above {@link
27+
* #HIGH_WATERMARK} of the limit, clearing it below {@link #LOW_WATERMARK}.
28+
* Antagonists poll {@link #pace()} once per loop iteration — a volatile read
29+
* plus, only while throttled, a short sleep. If no cgroup limit is readable
30+
* (e.g. running outside a container), the governor stays permanently
31+
* disabled rather than guessing at a ceiling.
32+
*/
33+
final class MemoryGovernor {
34+
35+
private static final double HIGH_WATERMARK = 0.85;
36+
private static final double LOW_WATERMARK = 0.70;
37+
private static final long SAMPLE_INTERVAL_MS = 500;
38+
private static final long THROTTLE_SLEEP_MS = 5;
39+
40+
private static final Path[] USAGE_PATHS = {
41+
Paths.get("/sys/fs/cgroup/memory.current"),
42+
Paths.get("/sys/fs/cgroup/memory/memory.usage_in_bytes"),
43+
};
44+
private static final Path[] LIMIT_PATHS = {
45+
Paths.get("/sys/fs/cgroup/memory.max"),
46+
Paths.get("/sys/fs/cgroup/memory/memory.limit_in_bytes"),
47+
};
48+
49+
private static volatile boolean throttled;
50+
51+
private MemoryGovernor() {
52+
}
53+
54+
/** Starts the background sampler if a cgroup memory limit is readable. Idempotent-ish: call once from Main. */
55+
static void start() {
56+
Path usagePath = firstReadable(USAGE_PATHS);
57+
long limitBytes = firstLimit(LIMIT_PATHS);
58+
if (usagePath == null || limitBytes <= 0) {
59+
return; // no container ceiling visible; never throttle
60+
}
61+
Thread sampler = new Thread(() -> sampleLoop(usagePath, limitBytes), "chaos-memory-governor");
62+
sampler.setDaemon(true);
63+
sampler.start();
64+
}
65+
66+
/** Called from an antagonist's hot allocation loop. No-op unless throttled. */
67+
static void pace() {
68+
if (throttled) {
69+
try {
70+
Thread.sleep(THROTTLE_SLEEP_MS);
71+
} catch (InterruptedException e) {
72+
Thread.currentThread().interrupt();
73+
}
74+
}
75+
}
76+
77+
private static void sampleLoop(Path usagePath, long limitBytes) {
78+
while (true) {
79+
try {
80+
Thread.sleep(SAMPLE_INTERVAL_MS);
81+
long usage = readLong(usagePath);
82+
double fraction = (double) usage / (double) limitBytes;
83+
if (fraction >= HIGH_WATERMARK) {
84+
throttled = true;
85+
} else if (fraction <= LOW_WATERMARK) {
86+
throttled = false;
87+
}
88+
} catch (InterruptedException e) {
89+
Thread.currentThread().interrupt();
90+
return;
91+
} catch (IOException | NumberFormatException e) {
92+
// Transient read failure; keep last known state and retry next tick.
93+
}
94+
}
95+
}
96+
97+
private static Path firstReadable(Path[] candidates) {
98+
for (Path p : candidates) {
99+
if (Files.isReadable(p)) {
100+
return p;
101+
}
102+
}
103+
return null;
104+
}
105+
106+
private static long firstLimit(Path[] candidates) {
107+
for (Path p : candidates) {
108+
if (Files.isReadable(p)) {
109+
try {
110+
return readLong(p);
111+
} catch (IOException | NumberFormatException e) {
112+
// "max" (v2, unlimited) or unreadable; try the next candidate.
113+
}
114+
}
115+
}
116+
return -1;
117+
}
118+
119+
private static long readLong(Path path) throws IOException {
120+
String s = new String(Files.readAllBytes(path), StandardCharsets.US_ASCII).trim();
121+
return Long.parseLong(s);
122+
}
123+
}

utils/run-chaos-harness.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ echo "LD_PRELOAD=${LD_PRELOAD:-}"
188188
CGROUP_MEM_LIMIT=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || cat /sys/fs/cgroup/memory.max 2>/dev/null || echo "unknown")
189189
echo "cgroup memory limit: ${CGROUP_MEM_LIMIT}"
190190

191+
# Container ceiling is 6GiB (see logged cgroup limit above). Job 1858271921
192+
# (profiler+tracer, jemalloc, 21.0.3-tem, aarch64) OOMKilled within seconds of
193+
# all 13 antagonists starting: several of them (alloc-storm, direct-memory,
194+
# dump-storm) deliberately burst off-heap/native memory hard and fast, sharing
195+
# the same cgroup limit as the heap. Reproduced under a 6GiB-capped cgroup and
196+
# confirmed MemoryGovernor (see chaos/MemoryGovernor.java, wired into the
197+
# antagonists' hot loops) keeps that same config under the limit, so the heap
198+
# no longer needs to be trimmed to make room for off-heap bursts.
191199
HEAP_MB=2560
192200

193201
HS_ERR="${CHAOS_ERROR_FILE:-${WORK_DIR}/hs_err.log}"

0 commit comments

Comments
 (0)