Skip to content

Commit a38d084

Browse files
jbachorikclaude
andcommitted
test(PROF-15098): add reapply benchmark and chaos antagonist
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 13a9fdc commit a38d084

4 files changed

Lines changed: 192 additions & 0 deletions

File tree

ddprof-stresstest/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ dependencies {
6767
// dd-trace-api: annotations only at compile time. The patched dd-java-agent
6868
// provides the (relocated) runtime classes and intercepts @Trace.
6969
"chaosCompileOnly"(libs.dd.trace.api)
70+
// ddprof-lib public API: compile-only; the patched dd-java-agent provides the
71+
// classes at runtime for antagonists that call JavaProfiler/ThreadContext directly.
72+
"chaosCompileOnly"(project(mapOf("path" to ":ddprof-lib", "configuration" to "debug")))
7073
}
7174

7275
tasks.register<Jar>("chaosJar") {

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
@@ -90,6 +90,8 @@ private static Antagonist create(String name) {
9090
return new WeakRefWaveAntagonist();
9191
case "dump-storm":
9292
return new DumpStormAntagonist();
93+
case "reapply-context":
94+
return new ReapplyContextAntagonist();
9395
// Deferred: dlopen-churn (needs per-arch dummy .so built in CI prep).
9496
default:
9597
throw new IllegalArgumentException("unknown antagonist: " + name);
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 com.datadoghq.profiler.JavaProfiler;
13+
import com.datadoghq.profiler.ThreadContext;
14+
import java.nio.charset.StandardCharsets;
15+
import java.time.Duration;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.TimeUnit;
19+
import java.util.concurrent.atomic.AtomicLong;
20+
21+
/**
22+
* Drives the reapply-by-id-and-bytes hot path continuously from multiple threads while the
23+
* profiler's wall-clock signal fires, racing the {@code detach}/{@code attach} window.
24+
*
25+
* <p>Each worker thread loops: span-activate (wiping slots) → reapply snapshot. This mirrors
26+
* dd-trace-java's {@code reapplyAppContext} pattern and exercises the single-window invariant
27+
* (no partial publish visible to a signal handler) under high thread count and signal pressure.
28+
*
29+
* <p>The only failure signal is a JVM crash or a reapply returning {@code false} while the record
30+
* is active — the latter is logged and counted but does not abort the run (false positives from
31+
* genuine attrs_data overflow are possible with many threads).
32+
*/
33+
public final class ReapplyContextAntagonist implements Antagonist {
34+
35+
private static final String[] ROUTES = {
36+
"GET /api/users",
37+
"POST /api/orders",
38+
"GET /api/health",
39+
"PUT /api/users/{id}",
40+
"DELETE /api/sessions"
41+
};
42+
43+
private final int workerCount;
44+
private final ExecutorService pool;
45+
private volatile boolean running;
46+
private final AtomicLong sink = new AtomicLong();
47+
private final AtomicLong reapplyFailures = new AtomicLong();
48+
49+
public ReapplyContextAntagonist() {
50+
this(8);
51+
}
52+
53+
public ReapplyContextAntagonist(int workerCount) {
54+
this.workerCount = workerCount;
55+
this.pool =
56+
Executors.newFixedThreadPool(
57+
workerCount,
58+
r -> {
59+
Thread t = new Thread(r, "chaos-reapply-context");
60+
t.setDaemon(true);
61+
return t;
62+
});
63+
}
64+
65+
@Override
66+
public String name() {
67+
return "reapply-context";
68+
}
69+
70+
@Override
71+
public void start() {
72+
running = true;
73+
for (int i = 0; i < workerCount; i++) {
74+
pool.submit(this::workerLoop);
75+
}
76+
}
77+
78+
@Override
79+
public void stopGracefully(Duration timeout) {
80+
running = false;
81+
pool.shutdown();
82+
try {
83+
pool.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS);
84+
} catch (InterruptedException e) {
85+
Thread.currentThread().interrupt();
86+
}
87+
long failures = reapplyFailures.get();
88+
if (failures > 0) {
89+
System.out.println(
90+
"[chaos] reapply-context: " + failures + " unexpected reapply failures");
91+
}
92+
}
93+
94+
private void workerLoop() {
95+
JavaProfiler profiler;
96+
try {
97+
profiler = JavaProfiler.getInstance();
98+
} catch (Exception e) {
99+
System.err.println("[chaos] reapply-context: failed to get profiler: " + e);
100+
return;
101+
}
102+
ThreadContext ctx = profiler.getThreadContext();
103+
104+
// Prime the per-thread encoding cache and capture a stable snapshot.
105+
long spanId = Thread.currentThread().getId() + 1;
106+
long localRootSpanId = spanId * 31L;
107+
ctx.put(localRootSpanId, spanId, 0, spanId);
108+
for (int i = 0; i < ROUTES.length; i++) {
109+
ctx.setContextAttribute(i, ROUTES[i]);
110+
}
111+
int[] constantIds = new int[10];
112+
ctx.copyCustoms(constantIds);
113+
byte[][] utf8 = new byte[10][];
114+
for (int i = 0; i < ROUTES.length; i++) {
115+
utf8[i] = ROUTES[i].getBytes(StandardCharsets.UTF_8);
116+
}
117+
118+
long r = spanId;
119+
while (running) {
120+
// Span activation wipes all custom slots.
121+
ctx.put(localRootSpanId, spanId, 0, spanId);
122+
// Reapply restores them — this is the hot path under test.
123+
if (!ctx.setContextAttributesByIdAndBytes(constantIds, utf8)) {
124+
reapplyFailures.incrementAndGet();
125+
}
126+
// Burn a little CPU so wall-clock signals have something to sample.
127+
r = r * 1103515245L + 12345L;
128+
sink.addAndGet(r);
129+
}
130+
}
131+
}

ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/ThreadContextBenchmark.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.datadoghq.profiler.JavaProfiler;
1919
import com.datadoghq.profiler.ThreadContext;
20+
import java.nio.charset.StandardCharsets;
2021
import java.nio.file.Files;
2122
import java.nio.file.Path;
2223
import java.util.concurrent.ThreadLocalRandom;
@@ -81,6 +82,34 @@ public void setup(ProfilerState ps) {
8182
}
8283
}
8384

85+
@State(Scope.Thread)
86+
public static class ReapplyState {
87+
ThreadContext ctx;
88+
long spanId;
89+
long localRootSpanId;
90+
int[] constantIds;
91+
byte[][] utf8;
92+
93+
@Setup(Level.Trial)
94+
public void setup(ProfilerState ps) {
95+
ctx = ps.profiler.getThreadContext();
96+
spanId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE);
97+
localRootSpanId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE);
98+
99+
// Prime the normal path to obtain constant IDs, then snapshot for reapply.
100+
ctx.put(localRootSpanId, spanId, 0, spanId);
101+
for (int i = 0; i < ROUTES.length && i < 10; i++) {
102+
ctx.setContextAttribute(i % ROUTES.length, ROUTES[i % ROUTES.length]);
103+
}
104+
constantIds = new int[10];
105+
ctx.copyCustoms(constantIds);
106+
utf8 = new byte[10][];
107+
for (int i = 0; i < ROUTES.length && i < 10; i++) {
108+
utf8[i] = ROUTES[i % ROUTES.length].getBytes(StandardCharsets.UTF_8);
109+
}
110+
}
111+
}
112+
84113
@Benchmark
85114
public void setContextFull(ThreadState ts) {
86115
ts.ctx.put(ts.localRootSpanId, ts.spanId, 0, ts.spanId);
@@ -132,4 +161,31 @@ public long getSpanId(ThreadState ts) {
132161
public void clearContext(ThreadState ts) {
133162
ts.ctx.put(0, 0, 0, 0);
134163
}
164+
165+
/** Bare reapply cost with constant IDs and bytes already in hand — no Dictionary lookup. */
166+
@Benchmark
167+
public boolean reapplyByIdAndBytes(ReapplyState rs) {
168+
return rs.ctx.setContextAttributesByIdAndBytes(rs.constantIds, rs.utf8);
169+
}
170+
171+
/** Full reapply cycle: span activation wipes slots, then reapply restores them. */
172+
@Benchmark
173+
public boolean reapplyCycle(ReapplyState rs) {
174+
rs.ctx.put(rs.localRootSpanId, rs.spanId, 0, rs.spanId);
175+
return rs.ctx.setContextAttributesByIdAndBytes(rs.constantIds, rs.utf8);
176+
}
177+
178+
@Benchmark
179+
@Threads(2)
180+
public boolean reapplyCycle_2t(ReapplyState rs) {
181+
rs.ctx.put(rs.localRootSpanId, rs.spanId, 0, rs.spanId);
182+
return rs.ctx.setContextAttributesByIdAndBytes(rs.constantIds, rs.utf8);
183+
}
184+
185+
@Benchmark
186+
@Threads(4)
187+
public boolean reapplyCycle_4t(ReapplyState rs) {
188+
rs.ctx.put(rs.localRootSpanId, rs.spanId, 0, rs.spanId);
189+
return rs.ctx.setContextAttributesByIdAndBytes(rs.constantIds, rs.utf8);
190+
}
135191
}

0 commit comments

Comments
 (0)