Skip to content

Commit f2ae229

Browse files
committed
Add benchmark for the TLS priming mechanism
1 parent fd0d7a4 commit f2ae229

File tree

2 files changed

+239
-1
lines changed

2 files changed

+239
-1
lines changed

ddprof-stresstest/build.gradle

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,54 @@ jmh {
3636
timeOnIteration = '3s'
3737
warmup = '1s'
3838
warmupIterations = 3
39+
40+
// Add support for includes with multiple patterns
41+
if (project.hasProperty('jmh.includes')) {
42+
def patternsString = project.property('jmh.includes')
43+
includes = patternsString.split(',').collect { it.trim() }
44+
}
45+
46+
// Add support for excludes with multiple patterns
47+
if (project.hasProperty('jmh.excludes')) {
48+
def patternsString = project.property('jmh.excludes')
49+
excludes = patternsString.split(',').collect { it.trim() }
50+
}
51+
52+
// Support passing environment variables as system properties to the forked JVM
53+
// Usage: -Pjmh.env.VAR_NAME=value or -Pjmh.env="VAR1=val1,VAR2=val2"
54+
def jvmArgsList = []
55+
56+
// Option 1: Individual env vars via -Pjmh.env.VAR_NAME=value
57+
project.properties.each { key, value ->
58+
if (key.startsWith('jmh.env.')) {
59+
def envVarName = key.substring('jmh.env.'.length())
60+
jvmArgsList.add("-D${envVarName}=${value}")
61+
}
62+
}
63+
64+
// Option 2: Multiple env vars via -Pjmh.env="VAR1=val1,VAR2=val2"
65+
if (project.hasProperty('jmh.env')) {
66+
def envString = project.property('jmh.env')
67+
envString.split(',').each { pair ->
68+
def trimmedPair = pair.trim()
69+
if (trimmedPair.contains('=')) {
70+
def parts = trimmedPair.split('=', 2)
71+
jvmArgsList.add("-D${parts[0].trim()}=${parts[1].trim()}")
72+
}
73+
}
74+
}
75+
76+
// Option 3: Pass JVM args directly via -Pjmh.jvmArgs
77+
if (project.hasProperty('jmh.jvmArgs')) {
78+
def argsString = project.property('jmh.jvmArgs')
79+
argsString.split(',').each { arg ->
80+
jvmArgsList.add(arg.trim())
81+
}
82+
}
83+
84+
if (!jvmArgsList.isEmpty()) {
85+
jvmArgsAppend = jvmArgsList
86+
}
3987
}
4088

4189
// Configure all JMH-related JavaExec tasks to use the correct JDK
@@ -63,7 +111,72 @@ task runStressTests(type: Exec) {
63111
}
64112
group = 'Execution'
65113
description = 'Run JMH stresstests'
66-
commandLine "${javaHome}/bin/java", '-jar', 'build/libs/stresstests.jar', '-prof', 'com.datadoghq.profiler.stresstest.WhiteboxProfiler', 'counters.*'
114+
115+
// Build command line with support for passing JVM args
116+
def cmd = ["${javaHome}/bin/java"]
117+
118+
// Add JVM args if specified
119+
if (project.hasProperty('jmh.jvmArgs')) {
120+
project.property('jmh.jvmArgs').split(',').each { arg ->
121+
cmd.add(arg.trim())
122+
}
123+
}
124+
125+
// Add env vars as system properties (for Java code that uses System.getProperty)
126+
project.properties.each { key, value ->
127+
if (key.startsWith('jmh.env.')) {
128+
def envVarName = key.substring('jmh.env.'.length())
129+
cmd.add("-D${envVarName}=${value}")
130+
}
131+
}
132+
133+
if (project.hasProperty('jmh.env')) {
134+
def envString = project.property('jmh.env')
135+
envString.split(',').each { pair ->
136+
def trimmedPair = pair.trim()
137+
if (trimmedPair.contains('=')) {
138+
def parts = trimmedPair.split('=', 2)
139+
cmd.add("-D${parts[0].trim()}=${parts[1].trim()}")
140+
}
141+
}
142+
}
143+
144+
cmd.addAll([
145+
'-jar',
146+
'build/libs/stresstests.jar',
147+
'-prof',
148+
'com.datadoghq.profiler.stresstest.WhiteboxProfiler',
149+
'counters.*'
150+
])
151+
152+
commandLine cmd
153+
154+
// Set actual environment variables for the forked process (for native code that uses getenv())
155+
// This allows native C++ code to read them via std::getenv()
156+
def envVars = [:]
157+
158+
// Copy specific env vars from current process
159+
project.properties.each { key, value ->
160+
if (key.startsWith('jmh.env.')) {
161+
def envVarName = key.substring('jmh.env.'.length())
162+
envVars[envVarName] = value
163+
}
164+
}
165+
166+
if (project.hasProperty('jmh.env')) {
167+
def envString = project.property('jmh.env')
168+
envString.split(',').each { pair ->
169+
def trimmedPair = pair.trim()
170+
if (trimmedPair.contains('=')) {
171+
def parts = trimmedPair.split('=', 2)
172+
envVars[parts[0].trim()] = parts[1].trim()
173+
}
174+
}
175+
}
176+
177+
if (!envVars.isEmpty()) {
178+
environment envVars
179+
}
67180
}
68181

69182
tasks.withType(JavaCompile).configureEach {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2025, 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+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.datadoghq.profiler.stresstest.scenarios.throughput;
17+
18+
import com.datadoghq.profiler.JavaProfiler;
19+
import com.datadoghq.profiler.stresstest.Configuration;
20+
import org.openjdk.jmh.annotations.Benchmark;
21+
import org.openjdk.jmh.annotations.BenchmarkMode;
22+
import org.openjdk.jmh.annotations.Fork;
23+
import org.openjdk.jmh.annotations.Level;
24+
import org.openjdk.jmh.annotations.Measurement;
25+
import org.openjdk.jmh.annotations.Mode;
26+
import org.openjdk.jmh.annotations.OutputTimeUnit;
27+
import org.openjdk.jmh.annotations.Param;
28+
import org.openjdk.jmh.annotations.Scope;
29+
import org.openjdk.jmh.annotations.Setup;
30+
import org.openjdk.jmh.annotations.State;
31+
import org.openjdk.jmh.annotations.TearDown;
32+
import org.openjdk.jmh.annotations.Threads;
33+
import org.openjdk.jmh.annotations.Warmup;
34+
import org.openjdk.jmh.infra.Blackhole;
35+
36+
import java.io.IOException;
37+
import java.util.concurrent.TimeUnit;
38+
39+
/**
40+
* Benchmark to measure throughput impact of TLS priming overhead during high thread churn
41+
* scenarios. This helps identify performance regressions in the thread directory watcher
42+
* introduced for native thread TLS priming.
43+
*/
44+
@State(Scope.Benchmark)
45+
@BenchmarkMode(Mode.Throughput)
46+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
47+
public class ThreadChurnBenchmark extends Configuration {
48+
49+
@Param({BASE_COMMAND})
50+
public String command;
51+
52+
@Param({"true", "false"})
53+
public boolean profilingEnabled;
54+
55+
@Param({"0", "1000", "10000"})
56+
public int workPerThread;
57+
58+
private JavaProfiler profiler;
59+
60+
@Setup(Level.Trial)
61+
public void setup() throws IOException {
62+
profiler = JavaProfiler.getInstance();
63+
if (profilingEnabled) {
64+
profiler.execute("start," + command + ",jfr,file=/tmp/thread-churn-benchmark.jfr");
65+
}
66+
}
67+
68+
@TearDown(Level.Trial)
69+
public void tearDown() throws IOException {
70+
if (profilingEnabled && profiler != null) {
71+
profiler.execute("stop");
72+
}
73+
}
74+
75+
@Benchmark
76+
@Fork(value = 3, warmups = 1)
77+
@Warmup(iterations = 3, time = 5)
78+
@Measurement(iterations = 5, time = 10)
79+
@Threads(4)
80+
public void threadChurn04(Blackhole bh) throws InterruptedException {
81+
Thread t = new Thread(() -> {
82+
long sum = 0;
83+
for (int i = 0; i < workPerThread; i++) {
84+
sum += i;
85+
}
86+
Blackhole.consumeCPU(sum);
87+
});
88+
t.start();
89+
t.join();
90+
}
91+
92+
@Benchmark
93+
@Fork(value = 3, warmups = 1)
94+
@Warmup(iterations = 3, time = 5)
95+
@Measurement(iterations = 5, time = 10)
96+
@Threads(8)
97+
public void threadChurn08(Blackhole bh) throws InterruptedException {
98+
Thread t = new Thread(() -> {
99+
long sum = 0;
100+
for (int i = 0; i < workPerThread; i++) {
101+
sum += i;
102+
}
103+
Blackhole.consumeCPU(sum);
104+
});
105+
t.start();
106+
t.join();
107+
}
108+
109+
@Benchmark
110+
@Fork(value = 3, warmups = 1)
111+
@Warmup(iterations = 3, time = 5)
112+
@Measurement(iterations = 5, time = 10)
113+
@Threads(16)
114+
public void threadChurn16(Blackhole bh) throws InterruptedException {
115+
Thread t = new Thread(() -> {
116+
long sum = 0;
117+
for (int i = 0; i < workPerThread; i++) {
118+
sum += i;
119+
}
120+
Blackhole.consumeCPU(sum);
121+
});
122+
t.start();
123+
t.join();
124+
}
125+
}

0 commit comments

Comments
 (0)