Skip to content

Commit 8437fab

Browse files
jbachorikclaude
andcommitted
Add profiling smoke test for Quarkus native images (#10446)
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 06c3524 commit 8437fab

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

dd-smoke-tests/quarkus-native/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ apply from: "$rootDir/gradle/java.gradle"
44

55
dependencies {
66
testImplementation project(':dd-smoke-tests')
7+
testImplementation libs.bundles.jmc
78
}
89
// Check 'testJvm' gradle command parameter is GraalVM (e.g., -PtestJvm=graalvm21),
910
// if not nothing is done
@@ -40,7 +41,8 @@ if (testGraalvmVersion >= 17) {
4041
"-PappBuildDir=$appBuildDir",
4142
"-PapiJar=${project(':dd-trace-api').tasks.jar.archiveFile.get()}",
4243
"-Dquarkus.native.additional-build-args=-J-javaagent:${project(':dd-java-agent').tasks.shadowJar.archiveFile.get()}," +
43-
"-J-Ddatadog.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSS'Z [dd.trace]',-march=native"
44+
"-J-Ddatadog.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSS'Z [dd.trace]'," +
45+
"-J-Ddd.profiling.enabled=true,-march=native"
4446
)
4547
outputs.cacheIf { true }
4648
outputs.dir(appBuildDir)
@@ -68,6 +70,7 @@ if (testGraalvmVersion >= 17) {
6870

6971
tasks.withType(Test).configureEach {
7072
jvmArgs "-Ddatadog.smoketest.quarkus.native.executable=$appBuildDir/quarkus-native-smoketest--runner"
73+
jvmArgs "-Ddd.profiling.enabled=true"
7174
}
7275
} else {
7376
tasks.withType(Test).configureEach {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package datadog.smoketest
2+
3+
import okhttp3.Request
4+
import org.openjdk.jmc.common.item.ItemFilters
5+
import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit
6+
import org.openjdk.jmc.flightrecorder.internal.InvalidJfrFileException
7+
import spock.lang.Shared
8+
import spock.lang.TempDir
9+
import spock.util.concurrent.PollingConditions
10+
11+
import java.nio.file.FileVisitResult
12+
import java.nio.file.Files
13+
import java.nio.file.Path
14+
import java.nio.file.SimpleFileVisitor
15+
import java.nio.file.attribute.BasicFileAttributes
16+
import java.util.concurrent.atomic.AtomicInteger
17+
18+
/**
19+
* Smoke test for profiling in Quarkus native images.
20+
*
21+
* This test validates the fix for issue #10446 where Quarkus native image builds
22+
* failed with ClassNotFoundException for profiler classes (BufferWriter9, etc.).
23+
* The fix requires proper GraalVM native-image configuration to defer profiler
24+
* class initialization to runtime.
25+
*
26+
* Test validates:
27+
* 1. Native image builds successfully with profiling enabled (no ClassNotFoundException)
28+
* 2. Application starts without agent initialization errors
29+
* 3. Profiler produces JFR files at runtime
30+
*/
31+
class QuarkusNativeProfilingSmokeTest extends QuarkusSlf4jSmokeTest {
32+
33+
@Shared
34+
@TempDir
35+
Path testJfrDir
36+
37+
@Override
38+
protected List<String> additionalArguments() {
39+
return [
40+
"-Ddd.profiling.upload.period=1",
41+
"-Ddd.profiling.start-force-first=true",
42+
"-Ddd.profiling.debug.dump_path=${testJfrDir}".toString(),
43+
// TODO: Remove this arg after JFR initialization is fixed on GraalVM 25.
44+
// https://datadoghq.atlassian.net/browse/PROF-12742
45+
"-XX:StartFlightRecording=filename=${testJfrDir}/recording.jfr".toString(),
46+
]
47+
}
48+
49+
def "profiling works in native image"() {
50+
setup:
51+
String url = "http://localhost:${httpPort}/${endpointName}?id=1"
52+
def conditions = new PollingConditions(initialDelay: 2, timeout: 10)
53+
54+
when:
55+
// Make a request to generate some activity for the profiler
56+
def response = client.newCall(new Request.Builder().url(url).get().build()).execute()
57+
58+
then:
59+
response.code() == 200
60+
response.body().string() == "Hello 1!"
61+
62+
// Wait for JFR files with execution samples to be generated
63+
conditions.eventually {
64+
assert countJfrsWithExecutionSamples() > 0
65+
}
66+
}
67+
68+
int countJfrsWithExecutionSamples() {
69+
AtomicInteger jfrCount = new AtomicInteger(0)
70+
Files.walkFileTree(testJfrDir, new SimpleFileVisitor<Path>() {
71+
@Override
72+
FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
73+
if (file.toString().endsWith(".jfr")) {
74+
try {
75+
def events = JfrLoaderToolkit.loadEvents(file.toFile())
76+
if (events.apply(ItemFilters.type("jdk.ExecutionSample")).hasItems()) {
77+
jfrCount.incrementAndGet()
78+
return FileVisitResult.SKIP_SIBLINGS
79+
}
80+
} catch (InvalidJfrFileException ignored) {
81+
// The recording captured at process exit might be incomplete
82+
}
83+
}
84+
return FileVisitResult.CONTINUE
85+
}
86+
})
87+
return jfrCount.get()
88+
}
89+
}

dd-smoke-tests/quarkus-native/src/test/groovy/datadog/smoketest/QuarkusNativeSmokeTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ abstract class QuarkusNativeSmokeTest extends AbstractServerSmokeTest {
2121
"-Ddd.app.customlogmanager=true",
2222
"-Dquarkus.http.port=${httpPort}"
2323
])
24+
command.addAll(additionalArguments())
2425
ProcessBuilder processBuilder = new ProcessBuilder(command)
2526
processBuilder.directory(new File(buildDirectory))
2627
}
2728

29+
protected List<String> additionalArguments() {
30+
return Collections.emptyList()
31+
}
32+
2833
@Override
2934
File createTemporaryFile() {
3035
return new File("${buildDirectory}/tmp/trace-structure-quarkus-native.out")

0 commit comments

Comments
 (0)