1010package com .datadoghq .profiler .chaos ;
1111
1212import java .io .IOException ;
13+ import java .lang .management .ManagementFactory ;
14+ import java .lang .management .MemoryUsage ;
1315import java .nio .charset .StandardCharsets ;
1416import java .nio .file .Files ;
1517import java .nio .file .Path ;
1618import java .nio .file .Paths ;
1719
1820/**
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).
21+ * Shared memory-pressure gate for antagonists whose allocation/thread-churn
22+ * rate can outrun GC/cleanup and threaten either the container's cgroup
23+ * ceiling or the JVM heap itself (alloc-storm, direct-memory, dump-storm,
24+ * weakref-wave, hidden-class-churn, context-hop, bounded-pool,
25+ * consumer-group).
2226 *
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.
27+ * <p>A single background thread samples two independent signals every tick:
28+ * cgroup memory usage/limit (v2 {@code memory.current}/{@code memory.max},
29+ * falling back to v1 {@code memory.usage_in_bytes}/{@code
30+ * memory.limit_in_bytes}) and JVM heap usage ({@link
31+ * java.lang.management.MemoryMXBean#getHeapMemoryUsage()}). Either signal
32+ * crossing its high watermark sets the shared {@code throttled} flag;
33+ * clearing it requires both signals to be back below their low watermark
34+ * (hysteresis, so a single antagonist bouncing near one ceiling doesn't
35+ * flap the gate for antagonists driving the other). A signal that has no
36+ * readable ceiling (no cgroup limit, or a JVM without {@code -Xmx}) is
37+ * treated as permanently low rather than guessed at.
38+ *
39+ * <p>Antagonists poll {@link #pace()} once per loop iteration — a volatile
40+ * read plus, only while throttled, a short sleep.
3241 */
3342final class MemoryGovernor {
3443
35- private static final double HIGH_WATERMARK = 0.85 ;
36- private static final double LOW_WATERMARK = 0.70 ;
44+ private static final double CGROUP_HIGH_WATERMARK = 0.85 ;
45+ private static final double CGROUP_LOW_WATERMARK = 0.70 ;
46+ // Heap-space OOME is less recoverable than a cgroup kill (no headroom to
47+ // shed off-heap first), so back off earlier and require more slack.
48+ private static final double HEAP_HIGH_WATERMARK = 0.80 ;
49+ private static final double HEAP_LOW_WATERMARK = 0.65 ;
3750 private static final long SAMPLE_INTERVAL_MS = 500 ;
3851 private static final long THROTTLE_SLEEP_MS = 5 ;
3952
@@ -51,14 +64,17 @@ final class MemoryGovernor {
5164 private MemoryGovernor () {
5265 }
5366
54- /** Starts the background sampler if a cgroup memory limit is readable. Idempotent-ish: call once from Main. */
67+ /** Starts the background sampler. Cgroup and/or heap signals no-op individually if unreadable. Call once from Main. */
5568 static void start () {
5669 Path usagePath = firstReadable (USAGE_PATHS );
5770 long limitBytes = firstLimit (LIMIT_PATHS );
5871 if (usagePath == null || limitBytes <= 0 ) {
59- return ; // no container ceiling visible; never throttle
72+ usagePath = null ;
73+ limitBytes = -1 ;
6074 }
61- Thread sampler = new Thread (() -> sampleLoop (usagePath , limitBytes ), "chaos-memory-governor" );
75+ Path finalUsagePath = usagePath ;
76+ long finalLimitBytes = limitBytes ;
77+ Thread sampler = new Thread (() -> sampleLoop (finalUsagePath , finalLimitBytes ), "chaos-memory-governor" );
6278 sampler .setDaemon (true );
6379 sampler .start ();
6480 }
@@ -78,20 +94,37 @@ private static void sampleLoop(Path usagePath, long limitBytes) {
7894 while (true ) {
7995 try {
8096 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- }
8897 } catch (InterruptedException e ) {
8998 Thread .currentThread ().interrupt ();
9099 return ;
91- } catch (IOException | NumberFormatException e ) {
92- // Transient read failure; keep last known state and retry next tick.
93100 }
101+ try {
102+ update (usagePath , limitBytes );
103+ } catch (IOException | NumberFormatException | OutOfMemoryError e ) {
104+ // Transient read/allocation failure; keep last known state and retry next tick.
105+ }
106+ }
107+ }
108+
109+ private static void update (Path usagePath , long limitBytes ) throws IOException {
110+ double cgroupFraction = 0.0 ;
111+ if (usagePath != null ) {
112+ cgroupFraction = (double ) readLong (usagePath ) / (double ) limitBytes ;
113+ }
114+ double heapFraction = 0.0 ;
115+ MemoryUsage heap = ManagementFactory .getMemoryMXBean ().getHeapMemoryUsage ();
116+ if (heap .getMax () > 0 ) {
117+ heapFraction = (double ) heap .getUsed () / (double ) heap .getMax ();
118+ }
119+
120+ boolean high = cgroupFraction >= CGROUP_HIGH_WATERMARK || heapFraction >= HEAP_HIGH_WATERMARK ;
121+ boolean low = cgroupFraction <= CGROUP_LOW_WATERMARK && heapFraction <= HEAP_LOW_WATERMARK ;
122+ if (high ) {
123+ throttled = true ;
124+ } else if (low ) {
125+ throttled = false ;
94126 }
127+ // else: in the dead zone between watermarks — keep current state.
95128 }
96129
97130 private static Path firstReadable (Path [] candidates ) {
0 commit comments