Skip to content

Commit 81f2326

Browse files
authored
Support aot_training mode to be used when creating AOT caches (#10166)
In this mode only the agent jar is added to the boot classpath, no services are started. Also workaround potential AOT bug where TraceInterceptor is mistakenly restored from the system class-loader in production, even though it was visible from the boot class-loader during training, resulting in LinkageErrors. Any call to Tracer.addTraceInterceptor from application code in the system class-loader appears to trigger this bug. The workaround is to replace these calls during training with opcodes that pop the tracer and argument, and push the expected return value. This transformation is not persisted, so in production the original method is invoked. * Add automatic check for AOT training mode, based on CDS.isDumpingArchive() * Automatic AOT training detection can be turned off with DD_DETECT_AOT_TRAINING_MODE=false
1 parent d0b4113 commit 81f2326

File tree

6 files changed

+207
-3
lines changed

6 files changed

+207
-3
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package datadog.trace.bootstrap.aot;
2+
3+
import static datadog.instrument.asm.Opcodes.ACC_ABSTRACT;
4+
import static datadog.instrument.asm.Opcodes.ACC_NATIVE;
5+
import static datadog.instrument.asm.Opcodes.ASM9;
6+
import static datadog.instrument.asm.Opcodes.INVOKEINTERFACE;
7+
import static datadog.instrument.asm.Opcodes.POP2;
8+
9+
import datadog.instrument.asm.ClassReader;
10+
import datadog.instrument.asm.ClassVisitor;
11+
import datadog.instrument.asm.ClassWriter;
12+
import datadog.instrument.asm.MethodVisitor;
13+
import java.lang.instrument.ClassFileTransformer;
14+
import java.security.ProtectionDomain;
15+
import java.util.concurrent.atomic.AtomicBoolean;
16+
17+
/**
18+
* Workaround a potential AOT bug where {@link datadog.trace.api.interceptor.TraceInterceptor} is
19+
* mistakenly restored from the system class-loader in production, even though it was visible from
20+
* the boot class-loader during training, resulting in {@link LinkageError}s.
21+
*
22+
* <p>Any call to {@link datadog.trace.api.Tracer#addTraceInterceptor} from application code in the
23+
* system class-loader appears to trigger this bug. The workaround is to replace these calls during
24+
* training with opcodes that pop the tracer and argument, and push the expected return value.
25+
*
26+
* <p>Note this transformation is not persisted, so in production the original method is invoked.
27+
*/
28+
final class TraceApiTransformer implements ClassFileTransformer {
29+
private static final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader();
30+
31+
@Override
32+
public byte[] transform(
33+
ClassLoader loader,
34+
String className,
35+
Class<?> classBeingRedefined,
36+
ProtectionDomain pd,
37+
byte[] bytecode) {
38+
39+
// workaround only needed in the system class-loader
40+
if (loader == SYSTEM_CLASS_LOADER) {
41+
try {
42+
ClassReader cr = new ClassReader(bytecode);
43+
ClassWriter cw = new ClassWriter(cr, 0);
44+
AtomicBoolean modified = new AtomicBoolean();
45+
cr.accept(new CallerPatch(cw, modified), 0);
46+
// only return something when we've modified the bytecode
47+
if (modified.get()) {
48+
return cw.toByteArray();
49+
}
50+
} catch (Throwable ignore) {
51+
// skip this class
52+
}
53+
}
54+
return null; // tells the JVM to keep the original bytecode
55+
}
56+
57+
/** Patches callers of {@link datadog.trace.api.Tracer#addTraceInterceptor}. */
58+
static final class CallerPatch extends ClassVisitor {
59+
private final AtomicBoolean modified;
60+
61+
CallerPatch(ClassVisitor cv, AtomicBoolean modified) {
62+
super(ASM9, cv);
63+
this.modified = modified;
64+
}
65+
66+
@Override
67+
public MethodVisitor visitMethod(
68+
int access, String name, String descriptor, String signature, String[] exceptions) {
69+
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
70+
if ((access & (ACC_ABSTRACT | ACC_NATIVE)) == 0) {
71+
return new InvokePatch(mv, modified);
72+
} else {
73+
return mv; // no need to patch abstract/native methods
74+
}
75+
}
76+
}
77+
78+
/** Removes calls to {@link datadog.trace.api.Tracer#addTraceInterceptor}. */
79+
static final class InvokePatch extends MethodVisitor {
80+
private final AtomicBoolean modified;
81+
82+
InvokePatch(MethodVisitor mv, AtomicBoolean modified) {
83+
super(ASM9, mv);
84+
this.modified = modified;
85+
}
86+
87+
@Override
88+
public void visitMethodInsn(
89+
int opcode, String owner, String name, String descriptor, boolean isInterface) {
90+
if (INVOKEINTERFACE == opcode
91+
&& "datadog/trace/api/Tracer".equals(owner)
92+
&& "addTraceInterceptor".equals(name)
93+
&& "(Ldatadog/trace/api/interceptor/TraceInterceptor;)Z".equals(descriptor)) {
94+
// pop tracer and trace interceptor argument from call stack
95+
mv.visitInsn(POP2);
96+
// push true return value
97+
mv.visitLdcInsn(true);
98+
// flag that we've modified the bytecode
99+
modified.set(true);
100+
} else {
101+
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
102+
}
103+
}
104+
}
105+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package datadog.trace.bootstrap.aot;
2+
3+
import java.lang.instrument.Instrumentation;
4+
import java.net.URL;
5+
6+
/** Prepares the agent for Ahead-of-Time training. */
7+
public final class TrainingAgent {
8+
public static void start(
9+
final Object bootstrapInitTelemetry,
10+
final Instrumentation inst,
11+
final URL agentJarURL,
12+
final String agentArgs) {
13+
14+
// apply TraceInterceptor LinkageError workaround
15+
inst.addTransformer(new TraceApiTransformer());
16+
17+
// don't start services, they won't be cached as they use a custom classloader
18+
}
19+
}

dd-java-agent/build.gradle

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,35 @@ tasks.named("processResources") {
2323
dependsOn(includedJarFileTree)
2424
}
2525

26-
// The special pre-check should be compiled with Java 6 to detect unsupported Java versions
27-
// and prevent issues for users that still using them.
2826
sourceSets {
27+
// The special pre-check must be compiled with Java 6 to detect unsupported
28+
// Java versions and prevent issues for users that still using them.
2929
"main_java6" {
3030
java.srcDirs "${project.projectDir}/src/main/java6"
3131
}
32+
// Additional checks that use the Java 11 API.
33+
"main_java11" {
34+
java.srcDirs "${project.projectDir}/src/main/java11"
35+
}
3236
main.resources.srcDir(includedAgentDir)
3337
}
3438

3539
def java6CompileTask = tasks.named("compileMain_java6Java") {
3640
configureCompiler(it, 8, JavaVersion.VERSION_1_6)
3741
}
3842

43+
def java11CompileTask = tasks.named("compileMain_java11Java") {
44+
configureCompiler(it, 11)
45+
}
46+
3947
tasks.named("compileJava") {
4048
dependsOn(java6CompileTask)
49+
dependsOn(java11CompileTask)
4150
}
4251

4352
dependencies {
53+
implementation sourceSets.main_java11.output
54+
main_java11CompileOnly libs.forbiddenapis
4455
main_java6CompileOnly libs.forbiddenapis
4556
testImplementation sourceSets.main_java6.output
4657
}
@@ -249,6 +260,8 @@ includeShadowJar(traceShadowJar, 'trace', includedJarFileTree)
249260
tasks.named("shadowJar", ShadowJar) {
250261
// Include AgentPreCheck compiled with Java 6.
251262
from sourceSets.main_java6.output
263+
// Include additional checks compiled with Java 11.
264+
from sourceSets.main_java11.output
252265

253266
generalShadowJarConfig(it)
254267

dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,17 @@ private static void agentmainImpl(
142142
recordInstrumentationSource("cmd_line");
143143
}
144144

145+
String agentClassName;
146+
if (isAotTraining(agentArgs, inst)) {
147+
agentClassName = "datadog.trace.bootstrap.aot.TrainingAgent";
148+
} else {
149+
agentClassName = "datadog.trace.bootstrap.Agent";
150+
}
151+
145152
final URL agentJarURL = installAgentJar(inst);
146153
final Class<?> agentClass;
147154
try {
148-
agentClass = Class.forName("datadog.trace.bootstrap.Agent", true, null);
155+
agentClass = Class.forName(agentClassName, true, null);
149156
} catch (ClassNotFoundException | LinkageError e) {
150157
throw new IllegalStateException("Unable to load DD Java Agent.", e);
151158
}
@@ -422,4 +429,17 @@ private static void checkJarManifestMainClassIsThis(final URL jarUrl) throws IOE
422429
+ jarUrl
423430
+ "'. Make sure you don't have this .class-file anywhere, besides dd-java-agent.jar");
424431
}
432+
433+
/** Returns {@code true} if the JVM is training, i.e. writing to a CDS/AOT archive. */
434+
private static boolean isAotTraining(String agentArgs, Instrumentation inst) {
435+
if (!JavaVirtualMachine.isJavaVersionAtLeast(25)) {
436+
return false; // agent doesn't support training mode before Java 25
437+
} else if ("aot_training".equalsIgnoreCase(agentArgs)) {
438+
return true; // training mode explicitly enabled via -javaagent
439+
} else if ("false".equalsIgnoreCase(EnvironmentVariables.get("DD_DETECT_AOT_TRAINING_MODE"))) {
440+
return false; // detection of training mode disabled via DD_DETECT_AOT_TRAINING_MODE=false
441+
} else {
442+
return AdvancedAgentChecks.isAotTraining(inst); // check JVM status
443+
}
444+
}
425445
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package datadog.trace.bootstrap;
2+
3+
import static java.util.Collections.emptyMap;
4+
import static java.util.Collections.emptySet;
5+
import static java.util.Collections.singleton;
6+
import static java.util.Collections.singletonMap;
7+
8+
import de.thetaphi.forbiddenapis.SuppressForbidden;
9+
import java.lang.instrument.Instrumentation;
10+
import java.lang.reflect.Method;
11+
12+
/** Additional agent checks that require Java 11+. */
13+
public final class AdvancedAgentChecks {
14+
15+
/** Returns {@code true} if the JVM is writing to a CDS/AOT archive, i.e. is in training mode. */
16+
@SuppressForbidden
17+
public static boolean isAotTraining(Instrumentation inst) {
18+
try {
19+
Class<?> cds = Class.forName("jdk.internal.misc.CDS");
20+
21+
// ensure the module containing CDS exports it to our unnamed module
22+
Module cdsModule = cds.getModule();
23+
Module unnamedModule = AdvancedAgentChecks.class.getClassLoader().getUnnamedModule();
24+
inst.redefineModule(
25+
cdsModule,
26+
emptySet(),
27+
singletonMap("jdk.internal.misc", singleton(unnamedModule)),
28+
emptyMap(),
29+
emptySet(),
30+
emptyMap());
31+
32+
// if the JVM is writing to a CDS/AOT archive then it's in training mode
33+
Method isDumpingArchive = cds.getMethod("isDumpingArchive");
34+
return (boolean) isDumpingArchive.invoke(null);
35+
} catch (Throwable ignore) {
36+
return false; // if we don't have access then assume we're not training
37+
}
38+
}
39+
}

metadata/supported-configurations.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@
105105
"aliases": []
106106
}
107107
],
108+
"DD_DETECT_AOT_TRAINING_MODE": [
109+
{
110+
"version": "A",
111+
"type": "boolean",
112+
"default": null,
113+
"aliases": []
114+
}
115+
],
108116
"DD_API_KEY": [
109117
{
110118
"version": "A",

0 commit comments

Comments
 (0)