Skip to content

Commit a4087b6

Browse files
authored
Fix instrumenting metric probes for Kotlin (#11890)
Fix instrumenting metric probes for Kotlin track objects on the stack with frame for each instruction using ASM analyzer. based on this generate POP/POP2 bytecode instruction to leave the stack clean before inserting metric calls. Kotlin compiler can, especially for suspend functions, generates a state machine with duplicate values on the stack while a return instruction will clear them (dropping the frame). we are previously relying on the fact that the number of values on the stack are precise even before a return instruction which leads to introduce jump to a same location but without the same number of values on the stack, triggering the verifier. fix leftover Co-authored-by: jean-philippe.bempel <jean-philippe.bempel@datadoghq.com>
1 parent 428b50e commit a4087b6

7 files changed

Lines changed: 103 additions & 12 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.Collection;
4141
import java.util.Collections;
4242
import java.util.List;
43+
import java.util.Map;
4344
import java.util.Set;
4445
import java.util.stream.Collectors;
4546
import org.objectweb.asm.Opcodes;
@@ -58,6 +59,8 @@
5859
import org.objectweb.asm.tree.TryCatchBlockNode;
5960
import org.objectweb.asm.tree.TypeInsnNode;
6061
import org.objectweb.asm.tree.VarInsnNode;
62+
import org.objectweb.asm.tree.analysis.BasicValue;
63+
import org.objectweb.asm.tree.analysis.Frame;
6164
import org.slf4j.Logger;
6265
import org.slf4j.LoggerFactory;
6366

@@ -265,7 +268,8 @@ private boolean isExceptionLocalDeclared(TryCatchBlockNode catchHandler, MethodN
265268
}
266269

267270
@Override
268-
protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) {
271+
protected InsnList getBeforeReturnInsnList(
272+
AbstractInsnNode node, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
269273
InsnList insnList = new InsnList();
270274
// stack [ret_value]
271275
insnList.add(new VarInsnNode(Opcodes.ALOAD, entryContextVar));

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import com.datadog.debugger.probe.ProbeDefinition;
44
import datadog.trace.bootstrap.debugger.Limits;
55
import java.util.List;
6+
import java.util.Map;
67
import org.objectweb.asm.tree.AbstractInsnNode;
78
import org.objectweb.asm.tree.InsnList;
9+
import org.objectweb.asm.tree.analysis.BasicValue;
10+
import org.objectweb.asm.tree.analysis.Frame;
811

912
public class ExceptionInstrumenter extends CapturedContextInstrumenter {
1013

@@ -25,7 +28,8 @@ public InstrumentationResult.Status instrument() {
2528
}
2629

2730
@Override
28-
protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) {
31+
protected InsnList getBeforeReturnInsnList(
32+
AbstractInsnNode node, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
2933
return null;
3034
}
3135

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.ArrayList;
1414
import java.util.Arrays;
1515
import java.util.HashMap;
16+
import java.util.IdentityHashMap;
1617
import java.util.List;
1718
import java.util.Map;
1819
import org.objectweb.asm.Opcodes;
@@ -28,9 +29,17 @@
2829
import org.objectweb.asm.tree.MethodNode;
2930
import org.objectweb.asm.tree.TryCatchBlockNode;
3031
import org.objectweb.asm.tree.TypeInsnNode;
32+
import org.objectweb.asm.tree.analysis.Analyzer;
33+
import org.objectweb.asm.tree.analysis.AnalyzerException;
34+
import org.objectweb.asm.tree.analysis.BasicInterpreter;
35+
import org.objectweb.asm.tree.analysis.BasicValue;
36+
import org.objectweb.asm.tree.analysis.Frame;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
3139

3240
/** Common class for generating instrumentation */
3341
public abstract class Instrumenter {
42+
private static final Logger LOGGER = LoggerFactory.getLogger(Instrumenter.class);
3443
protected static final String CONSTRUCTOR_NAME = "<init>";
3544
protected static final String PROBEID_TAG_NAME = "debugger.probeid";
3645

@@ -151,12 +160,14 @@ private AbstractInsnNode findFirstInsnForConstructor(AbstractInsnNode first) {
151160
}
152161

153162
protected void processInstructions() {
163+
Map<AbstractInsnNode, Frame<BasicValue>> frames = new IdentityHashMap<>();
164+
computeFrames(classNode.name, methodNode, frames);
154165
AbstractInsnNode node = methodNode.instructions.getFirst();
155166
LabelNode sentinelNode = new LabelNode();
156167
methodNode.instructions.add(sentinelNode);
157168
while (node != null && !node.equals(sentinelNode)) {
158169
if (node.getType() != AbstractInsnNode.LINE) {
159-
node = processInstruction(node);
170+
node = processInstruction(node, frames);
160171
}
161172
node = node.getNext();
162173
}
@@ -168,7 +179,40 @@ protected void processInstructions() {
168179
}
169180
}
170181

171-
protected AbstractInsnNode processInstruction(AbstractInsnNode node) {
182+
// returns the extra values sitting *below* `valuesToKeep` values already
183+
// accounted for at the top of the stack (e.g. the return value), as POP/POP2 insns
184+
protected InsnList stackCleanupInsnList(
185+
AbstractInsnNode node, int valuesToKeep, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
186+
InsnList result = new InsnList();
187+
Frame<BasicValue> frame = frames.get(node);
188+
if (frame == null) {
189+
return result;
190+
}
191+
for (int i = frame.getStackSize() - valuesToKeep - 1; i >= 0; i--) {
192+
BasicValue value = frame.getStack(i);
193+
result.add(new InsnNode(value.getSize() == 2 ? Opcodes.POP2 : Opcodes.POP));
194+
}
195+
return result;
196+
}
197+
198+
private static void computeFrames(
199+
String owner, MethodNode methodNode, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
200+
try {
201+
Frame<BasicValue>[] frameArray =
202+
new Analyzer<>(new BasicInterpreter()).analyze(owner, methodNode);
203+
AbstractInsnNode current = methodNode.instructions.getFirst();
204+
int idx = 0;
205+
while (current != null) {
206+
frames.put(current, frameArray[idx++]);
207+
current = current.getNext();
208+
}
209+
} catch (AnalyzerException ex) {
210+
LOGGER.debug("Failed to analyze method[%s::%s] instructions", owner, methodNode.name, ex);
211+
}
212+
}
213+
214+
protected AbstractInsnNode processInstruction(
215+
AbstractInsnNode node, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
172216
switch (node.getOpcode()) {
173217
case Opcodes.RET:
174218
case Opcodes.RETURN:
@@ -179,7 +223,7 @@ protected AbstractInsnNode processInstruction(AbstractInsnNode node) {
179223
case Opcodes.ARETURN:
180224
{
181225
// stack [ret_value]
182-
InsnList beforeReturnInsnList = getBeforeReturnInsnList(node);
226+
InsnList beforeReturnInsnList = getBeforeReturnInsnList(node, frames);
183227
if (beforeReturnInsnList != null) {
184228
methodNode.instructions.insertBefore(node, beforeReturnInsnList);
185229
}
@@ -193,7 +237,8 @@ protected AbstractInsnNode processInstruction(AbstractInsnNode node) {
193237
return node;
194238
}
195239

196-
protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) {
240+
protected InsnList getBeforeReturnInsnList(
241+
AbstractInsnNode node, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
197242
return null;
198243
}
199244

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import java.lang.reflect.Field;
5757
import java.util.ArrayList;
5858
import java.util.List;
59+
import java.util.Map;
5960
import java.util.stream.Collectors;
6061
import org.objectweb.asm.Opcodes;
6162
import org.objectweb.asm.Type;
@@ -71,6 +72,8 @@
7172
import org.objectweb.asm.tree.TryCatchBlockNode;
7273
import org.objectweb.asm.tree.TypeInsnNode;
7374
import org.objectweb.asm.tree.VarInsnNode;
75+
import org.objectweb.asm.tree.analysis.BasicValue;
76+
import org.objectweb.asm.tree.analysis.Frame;
7477
import org.slf4j.Logger;
7578
import org.slf4j.LoggerFactory;
7679

@@ -158,15 +161,18 @@ private InsnList wrapTryCatch(InsnList insnList) {
158161
}
159162

160163
@Override
161-
protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) {
164+
protected InsnList getBeforeReturnInsnList(
165+
AbstractInsnNode node, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
162166
int size = 1;
163167
int storeOpCode = 0;
164168
int loadOpCode = 0;
165169
Type returnType = null;
166170
switch (node.getOpcode()) {
167171
case Opcodes.RET:
168172
case Opcodes.RETURN:
169-
return wrapTryCatch(callMetric(metricProbe, node));
173+
InsnList insnList = wrapTryCatch(callMetric(metricProbe, node));
174+
insnList.insert(stackCleanupInsnList(node, 0, frames)); // void return: nothing to keep
175+
return insnList;
170176
case Opcodes.LRETURN:
171177
storeOpCode = Opcodes.LSTORE;
172178
loadOpCode = Opcodes.LLOAD;
@@ -202,7 +208,10 @@ protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) {
202208
wrapTryCatch(
203209
callMetric(metricProbe, node, new ReturnContext(tmpIdx, loadOpCode, returnType)));
204210
// store return value from the stack to local before wrapped call
205-
insnList.insert(new VarInsnNode(storeOpCode, tmpIdx));
211+
InsnList prefixInsns = new InsnList();
212+
prefixInsns.add(new VarInsnNode(storeOpCode, tmpIdx));
213+
prefixInsns.add(stackCleanupInsnList(node, 1, frames)); // keep 1 value (the one we just stored)
214+
insnList.insert(prefixInsns);
206215
// restore return value to the stack after wrapped call
207216
insnList.add(new VarInsnNode(loadOpCode, tmpIdx));
208217
return insnList;

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
import datadog.trace.bootstrap.debugger.DebuggerContext;
3232
import datadog.trace.bootstrap.debugger.MethodLocation;
3333
import datadog.trace.bootstrap.debugger.ProbeId;
34+
import java.io.File;
3435
import java.io.IOException;
3536
import java.lang.instrument.ClassFileTransformer;
3637
import java.lang.instrument.Instrumentation;
3738
import java.net.URISyntaxException;
39+
import java.net.URL;
3840
import java.util.ArrayList;
3941
import java.util.HashMap;
4042
import java.util.List;
@@ -456,6 +458,33 @@ public void methodSyntheticDurationGaugeMetric() throws IOException, URISyntaxEx
456458
assertArrayEquals(new String[] {METRIC_PROBEID_TAG}, listener.lastTags);
457459
}
458460

461+
@Test
462+
public void methodSyntheticDurationKotlinDist() {
463+
final String CLASS_NAME = "CapturedSnapshot302";
464+
String METRIC_NAME = "syn_dist";
465+
MetricProbe metricProbe =
466+
createMetricBuilder(METRIC_ID, METRIC_NAME, DISTRIBUTION)
467+
.where(CLASS_NAME, "download", null)
468+
.valueScript(new ValueScript(DSL.ref("@duration"), "@duration"))
469+
.evaluateAt(MethodLocation.EXIT)
470+
.build();
471+
MetricForwarderListener listener = installMetricProbes(metricProbe);
472+
URL resource = CapturedSnapshotTest.class.getResource("/" + CLASS_NAME + ".kt");
473+
List<File> filesToDelete = new ArrayList<>();
474+
try {
475+
Class<?> testClass =
476+
KotlinHelper.compileAndLoad(CLASS_NAME, resource.getFile(), filesToDelete);
477+
Object companion = Reflect.onClass(testClass).get("Companion");
478+
int result = Reflect.on(companion).call("main", "1").get();
479+
assertEquals(1, result);
480+
assertTrue(listener.doubleDistributions.containsKey(METRIC_NAME));
481+
assertTrue(listener.doubleDistributions.get(METRIC_NAME).doubleValue() > 0);
482+
assertArrayEquals(new String[] {METRIC_PROBEID_TAG}, listener.lastTags);
483+
} finally {
484+
filesToDelete.forEach(File::delete);
485+
}
486+
}
487+
459488
@Test
460489
public void methodSyntheticDurationExceptionGaugeMetric() throws IOException, URISyntaxException {
461490
final String CLASS_NAME = "CapturedSnapshot05";

dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/LogProbesIntegrationTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ void testLineProbe() throws Exception {
356356
LogProbe probe =
357357
LogProbe.builder()
358358
.probeId(LINE_PROBE_ID1)
359-
.where("DebuggerTestApplication.java", 88)
359+
.where("DebuggerTestApplication.java", 95)
360360
.captureSnapshot(true)
361361
.build();
362362
setCurrentConfiguration(createConfig(probe));
@@ -365,7 +365,7 @@ void testLineProbe() throws Exception {
365365
registerSnapshotListener(
366366
snapshot -> {
367367
assertEquals(LINE_PROBE_ID1.getId(), snapshot.getProbe().getId());
368-
CapturedContext capturedContext = snapshot.getCaptures().getLines().get(88);
368+
CapturedContext capturedContext = snapshot.getCaptures().getLines().get(95);
369369
assertFullMethodCaptureArgs(capturedContext);
370370
assertNull(capturedContext.getLocals());
371371
assertNull(capturedContext.getCapturedThrowable());

dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/MetricProbesIntegrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ private void doLineMetric(
210210
MetricProbe.builder()
211211
.probeId(PROBE_ID)
212212
// on line: System.out.println("fullMethod");
213-
.where("DebuggerTestApplication.java", 88)
213+
.where("DebuggerTestApplication.java", 95)
214214
.kind(kind)
215215
.metricName(metricName)
216216
.valueScript(script)

0 commit comments

Comments
 (0)