Skip to content

Commit baedf8d

Browse files
authored
Fix suspend Kotlin methods instrumentation (#8080)
suspend methods in Kotlin generates a special bytecode for the method with a state machine and could return different objects. for each return branch we need to have a specific return handler to avoid having mismatch stack maps add config for local var hoisting `DD_DYNAMIC_INSTRUMENTATION_HOIST_LOCALVARS_ENABLED` enable only hoisting for java language
1 parent fd1f40f commit baedf8d

File tree

6 files changed

+84
-5
lines changed

6 files changed

+84
-5
lines changed

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumentor.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,11 @@ private void instrumentMethodEnter() {
396396
if (methodNode.tryCatchBlocks.size() > 0) {
397397
throwableListVar = declareThrowableList(insnList);
398398
}
399-
unscopedLocalVars = initAndHoistLocalVars(insnList);
399+
unscopedLocalVars = Collections.emptyList();
400+
if (Config.get().isDebuggerHoistLocalVarsEnabled() && language == JvmLanguage.JAVA) {
401+
// for now, only hoist local vars for Java
402+
unscopedLocalVars = initAndHoistLocalVars(insnList);
403+
}
400404
insnList.add(contextInitLabel);
401405
if (definition instanceof SpanDecorationProbe
402406
&& definition.getEvaluateAt() == MethodLocation.EXIT) {

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumentor.java

+35-4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public abstract class Instrumentor {
4646
protected int localVarBaseOffset;
4747
protected int argOffset;
4848
protected final LocalVariableNode[] localVarsBySlotArray;
49+
protected final JvmLanguage language;
4950
protected LabelNode returnHandlerLabel;
5051
protected final List<CapturedContextInstrumentor.FinallyBlock> finallyBlocks = new ArrayList<>();
5152

@@ -69,6 +70,7 @@ public Instrumentor(
6970
argOffset += t.getSize();
7071
}
7172
localVarsBySlotArray = extractLocalVariables(argTypes);
73+
this.language = JvmLanguage.of(classNode);
7274
}
7375

7476
public abstract InstrumentationResult.Status instrument();
@@ -150,12 +152,15 @@ private AbstractInsnNode findFirstInsnForConstructor(AbstractInsnNode first) {
150152

151153
protected void processInstructions() {
152154
AbstractInsnNode node = methodNode.instructions.getFirst();
153-
while (node != null && !node.equals(returnHandlerLabel)) {
155+
LabelNode sentinelNode = new LabelNode();
156+
methodNode.instructions.add(sentinelNode);
157+
while (node != null && !node.equals(sentinelNode)) {
154158
if (node.getType() != AbstractInsnNode.LINE) {
155159
node = processInstruction(node);
156160
}
157161
node = node.getNext();
158162
}
163+
methodNode.instructions.remove(sentinelNode);
159164
if (returnHandlerLabel == null) {
160165
// if no return found, fallback to use the last instruction as last resort
161166
returnHandlerLabel = new LabelNode();
@@ -197,9 +202,8 @@ protected LabelNode getReturnHandler(AbstractInsnNode exitNode) {
197202
if (exitNode.getNext() != null || exitNode.getPrevious() != null) {
198203
throw new IllegalArgumentException("exitNode is not removed from original instruction list");
199204
}
200-
if (returnHandlerLabel != null) {
201-
return returnHandlerLabel;
202-
}
205+
// Create the returnHandlerLabel every time because the stack state could be different
206+
// for each return (suspend method in Kotlin)
203207
returnHandlerLabel = new LabelNode();
204208
methodNode.instructions.add(returnHandlerLabel);
205209
// stack top is return value (if any)
@@ -292,4 +296,31 @@ public FinallyBlock(LabelNode startLabel, LabelNode endLabel, LabelNode handlerL
292296
this.handlerLabel = handlerLabel;
293297
}
294298
}
299+
300+
protected enum JvmLanguage {
301+
JAVA,
302+
KOTLIN,
303+
SCALA,
304+
GROOVY,
305+
UNKNOWN;
306+
307+
public static JvmLanguage of(ClassNode classNode) {
308+
if (classNode.sourceFile == null) {
309+
return UNKNOWN;
310+
}
311+
if (classNode.sourceFile.endsWith(".java")) {
312+
return JAVA;
313+
}
314+
if (classNode.sourceFile.endsWith(".kt")) {
315+
return KOTLIN;
316+
}
317+
if (classNode.sourceFile.endsWith(".scala")) {
318+
return SCALA;
319+
}
320+
if (classNode.sourceFile.endsWith(".groovy")) {
321+
return GROOVY;
322+
}
323+
return UNKNOWN;
324+
}
325+
}
295326
}

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,39 @@ public void suspendKotlin() {
582582
}
583583
}
584584

585+
@Test
586+
@DisabledIf(
587+
value = "datadog.trace.api.Platform#isJ9",
588+
disabledReason = "Issue with J9 when compiling Kotlin code")
589+
public void suspendMethodKotlin() {
590+
final String CLASS_NAME = "CapturedSnapshot302";
591+
TestSnapshotListener listener =
592+
installProbes(createProbe(PROBE_ID, CLASS_NAME, "download", null));
593+
URL resource = CapturedSnapshotTest.class.getResource("/" + CLASS_NAME + ".kt");
594+
assertNotNull(resource);
595+
List<File> filesToDelete = new ArrayList<>();
596+
try {
597+
Class<?> testClass =
598+
KotlinHelper.compileAndLoad(CLASS_NAME, resource.getFile(), filesToDelete);
599+
Object companion = Reflect.onClass(testClass).get("Companion");
600+
int result = Reflect.on(companion).call("main", "1").get();
601+
assertEquals(1, result);
602+
// 2 snapshots are expected because the method is executed twice one for each state
603+
// before the delay, after the delay
604+
List<Snapshot> snapshots = assertSnapshots(listener, 2);
605+
Snapshot snapshot0 = snapshots.get(0);
606+
assertCaptureReturnValue(
607+
snapshot0.getCaptures().getReturn(),
608+
"kotlin.coroutines.intrinsics.CoroutineSingletons",
609+
"COROUTINE_SUSPENDED");
610+
Snapshot snapshot1 = snapshots.get(1);
611+
assertCaptureReturnValue(
612+
snapshot1.getCaptures().getReturn(), String.class.getTypeName(), "1");
613+
} finally {
614+
filesToDelete.forEach(File::delete);
615+
}
616+
}
617+
585618
@Test
586619
@DisabledIf(
587620
value = "datadog.trace.api.Platform#isJ9",

dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java

+1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ public final class ConfigDefaults {
181181
static final boolean DEFAULT_DEBUGGER_VERIFY_BYTECODE = true;
182182
static final boolean DEFAULT_DEBUGGER_INSTRUMENT_THE_WORLD = false;
183183
static final int DEFAULT_DEBUGGER_CAPTURE_TIMEOUT = 100; // milliseconds
184+
static final boolean DEFAULT_DEBUGGER_HOIST_LOCALVARS_ENABLED = true;
184185
static final boolean DEFAULT_DEBUGGER_SYMBOL_ENABLED = true;
185186
static final boolean DEFAULT_DEBUGGER_SYMBOL_FORCE_UPLOAD = false;
186187
static final int DEFAULT_DEBUGGER_SYMBOL_FLUSH_THRESHOLD = 100; // nb of classes

dd-trace-api/src/main/java/datadog/trace/api/config/DebuggerConfig.java

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public final class DebuggerConfig {
2828
public static final String DEBUGGER_REDACTION_EXCLUDED_IDENTIFIERS =
2929
"dynamic.instrumentation.redaction.excluded.identifiers";
3030
public static final String DEBUGGER_REDACTED_TYPES = "dynamic.instrumentation.redacted.types";
31+
public static final String DEBUGGER_HOIST_LOCALVARS_ENABLED =
32+
"dynamic.instrumentation.hoist.localvars.enabled";
3133
public static final String DEBUGGER_SYMBOL_ENABLED = "symbol.database.upload.enabled";
3234
public static final String DEBUGGER_SYMBOL_FORCE_UPLOAD = "internal.force.symbol.database.upload";
3335
public static final String DEBUGGER_SYMBOL_INCLUDES = "symbol.database.includes";

internal-api/src/main/java/datadog/trace/api/Config.java

+8
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ public static String getHostName() {
396396
private final String debuggerRedactedIdentifiers;
397397
private final Set<String> debuggerRedactionExcludedIdentifiers;
398398
private final String debuggerRedactedTypes;
399+
private final boolean debuggerHoistLocalVarsEnabled;
399400
private final boolean debuggerSymbolEnabled;
400401
private final boolean debuggerSymbolForceUpload;
401402
private final String debuggerSymbolIncludes;
@@ -1538,6 +1539,9 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
15381539
debuggerRedactionExcludedIdentifiers =
15391540
tryMakeImmutableSet(configProvider.getList(DEBUGGER_REDACTION_EXCLUDED_IDENTIFIERS));
15401541
debuggerRedactedTypes = configProvider.getString(DEBUGGER_REDACTED_TYPES, null);
1542+
debuggerHoistLocalVarsEnabled =
1543+
configProvider.getBoolean(
1544+
DEBUGGER_HOIST_LOCALVARS_ENABLED, DEFAULT_DEBUGGER_HOIST_LOCALVARS_ENABLED);
15411545
debuggerSymbolEnabled =
15421546
configProvider.getBoolean(DEBUGGER_SYMBOL_ENABLED, DEFAULT_DEBUGGER_SYMBOL_ENABLED);
15431547
debuggerSymbolForceUpload =
@@ -3063,6 +3067,10 @@ public String getDebuggerRedactedTypes() {
30633067
return debuggerRedactedTypes;
30643068
}
30653069

3070+
public boolean isDebuggerHoistLocalVarsEnabled() {
3071+
return debuggerHoistLocalVarsEnabled;
3072+
}
3073+
30663074
public boolean isAwsPropagationEnabled() {
30673075
return awsPropagationEnabled;
30683076
}

0 commit comments

Comments
 (0)