Skip to content

Commit bb5d791

Browse files
authored
Fix local var hoisting (#7624)
Rewrite the algorithm for hoisting local vars: Go through all local vars defined in the method and determine by name all local var that are hoistable. local vars are hoistable if there is no conflict by slot or by name with another. If conflict by slot we are allocating a new slot for one of the local. If conflict by name and type are not compatible we cancel hoisting for this name. If type compatible, we can add in hoistable list. Then we are considering all hoistable variables by name and if there are multiple occurrence for a name, we are merging those variables in a new slot and rewrite var instructions to use this new slot. if only one variable we just assign a new slot and rewrite the instructions in the range.
1 parent 1429d59 commit bb5d791

File tree

4 files changed

+254
-54
lines changed

4 files changed

+254
-54
lines changed

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,9 @@ private List<ToInstrumentInfo> filterAndSortDefinitions(
569569
// Log and span decoration probe shared the same instrumentor: CaptureContextInstrumentor
570570
// and therefore need to be instrumented once
571571
// note: exception probes are log probes and are handled the same way
572-
if (definition instanceof LogProbe || definition instanceof SpanDecorationProbe) {
572+
if (definition instanceof LogProbe
573+
|| definition instanceof SpanDecorationProbe
574+
|| definition instanceof DebuggerProbe) {
573575
if (definition instanceof ExceptionProbe) {
574576
if (addedExceptionProbe) {
575577
continue;

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

Lines changed: 136 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.objectweb.asm.tree.ClassNode;
5555
import org.objectweb.asm.tree.FieldInsnNode;
5656
import org.objectweb.asm.tree.FieldNode;
57+
import org.objectweb.asm.tree.IincInsnNode;
5758
import org.objectweb.asm.tree.InsnList;
5859
import org.objectweb.asm.tree.InsnNode;
5960
import org.objectweb.asm.tree.JumpInsnNode;
@@ -459,75 +460,161 @@ private void instrumentMethodEnter() {
459460
}
460461

461462
// Initialize and hoist local variables to the top of the method
462-
// if there is name conflict, do nothing for the conflicting local variable
463+
// if there is name/slot conflict, do nothing for the conflicting local variable
463464
private Collection<LocalVariableNode> initAndHoistLocalVars(InsnList insnList) {
464465
if (methodNode.localVariables == null || methodNode.localVariables.isEmpty()) {
465466
return Collections.emptyList();
466467
}
467468
Map<String, LocalVariableNode> localVarsByName = new HashMap<>();
468-
Set<String> duplicateNames = new HashSet<>();
469+
Map<Integer, LocalVariableNode> localVarsBySlot = new HashMap<>();
470+
Map<String, List<LocalVariableNode>> hoistableVarByName = new HashMap<>();
469471
for (LocalVariableNode localVar : methodNode.localVariables) {
470472
int idx = localVar.index - localVarBaseOffset;
471473
if (idx < argOffset) {
472474
// this is an argument
473475
continue;
474476
}
475-
if (!checkDuplicateLocalVar(localVar, localVarsByName)) {
476-
duplicateNames.add(localVar.name);
477+
checkHoistableLocalVar(localVar, localVarsByName, localVarsBySlot, hoistableVarByName);
478+
}
479+
// hoist vars
480+
List<LocalVariableNode> results = new ArrayList<>();
481+
for (Map.Entry<String, List<LocalVariableNode>> entry : hoistableVarByName.entrySet()) {
482+
List<LocalVariableNode> hoistableVars = entry.getValue();
483+
LocalVariableNode newVarNode;
484+
if (hoistableVars.size() > 1) {
485+
// merge variables
486+
String name = hoistableVars.get(0).name;
487+
String desc = hoistableVars.get(0).desc;
488+
Type localVarType = getType(desc);
489+
int newSlot = newVar(localVarType); // new slot for the local variable
490+
newVarNode = new LocalVariableNode(name, desc, null, null, null, newSlot);
491+
Set<LabelNode> endLabels = new HashSet<>();
492+
for (LocalVariableNode localVar : hoistableVars) {
493+
// rewrite each usage of the old var to the new slot
494+
rewriteLocalVarInsn(localVar, localVar.index, newSlot);
495+
endLabels.add(localVar.end);
496+
}
497+
hoistVar(insnList, newVarNode);
498+
newVarNode.end = findLatestLabel(methodNode.instructions, endLabels);
499+
// remove previous local variables
500+
methodNode.localVariables.removeIf(hoistableVars::contains);
501+
methodNode.localVariables.add(newVarNode);
502+
} else {
503+
// hoist the single variable and rewrite all its local var instructions
504+
newVarNode = hoistableVars.get(0);
505+
int oldIndex = newVarNode.index;
506+
newVarNode.index = newVar(getType(newVarNode.desc)); // new slot for the local variable
507+
rewriteLocalVarInsn(newVarNode, oldIndex, newVarNode.index);
508+
hoistVar(insnList, newVarNode);
509+
}
510+
results.add(newVarNode);
511+
}
512+
return results;
513+
}
514+
515+
private LabelNode findLatestLabel(InsnList instructions, Set<LabelNode> endLabels) {
516+
for (AbstractInsnNode insn = instructions.getLast(); insn != null; insn = insn.getPrevious()) {
517+
if (insn instanceof LabelNode && endLabels.contains(insn)) {
518+
return (LabelNode) insn;
477519
}
478520
}
479-
for (String name : duplicateNames) {
480-
localVarsByName.remove(name);
521+
return null;
522+
}
523+
524+
private void hoistVar(InsnList insnList, LocalVariableNode varNode) {
525+
LabelNode labelNode = new LabelNode(); // new start label for the local variable
526+
insnList.add(labelNode);
527+
varNode.start = labelNode; // update the start label of the local variable
528+
Type localVarType = getType(varNode.desc);
529+
addStore0Insn(insnList, varNode, localVarType);
530+
}
531+
532+
private static void addStore0Insn(
533+
InsnList insnList, LocalVariableNode localVar, Type localVarType) {
534+
switch (localVarType.getSort()) {
535+
case Type.BOOLEAN:
536+
case Type.CHAR:
537+
case Type.BYTE:
538+
case Type.SHORT:
539+
case Type.INT:
540+
insnList.add(new InsnNode(Opcodes.ICONST_0));
541+
break;
542+
case Type.LONG:
543+
insnList.add(new InsnNode(Opcodes.LCONST_0));
544+
break;
545+
case Type.FLOAT:
546+
insnList.add(new InsnNode(Opcodes.FCONST_0));
547+
break;
548+
case Type.DOUBLE:
549+
insnList.add(new InsnNode(Opcodes.DCONST_0));
550+
break;
551+
default:
552+
insnList.add(new InsnNode(Opcodes.ACONST_NULL));
553+
break;
554+
}
555+
insnList.add(new VarInsnNode(localVarType.getOpcode(Opcodes.ISTORE), localVar.index));
556+
}
557+
558+
private void checkHoistableLocalVar(
559+
LocalVariableNode localVar,
560+
Map<String, LocalVariableNode> localVarsByName,
561+
Map<Integer, LocalVariableNode> localVarsBySlot,
562+
Map<String, List<LocalVariableNode>> hoistableVarByName) {
563+
LocalVariableNode previousVarBySlot = localVarsBySlot.putIfAbsent(localVar.index, localVar);
564+
LocalVariableNode previousVarByName = localVarsByName.putIfAbsent(localVar.name, localVar);
565+
if (previousVarBySlot != null) {
566+
// there are multiple local variables with the same slot but different names
567+
// by hoisting in a new slot, we can avoid the conflict
568+
hoistableVarByName.computeIfAbsent(localVar.name, k -> new ArrayList<>()).add(localVar);
481569
}
482-
for (LocalVariableNode localVar : localVarsByName.values()) {
483-
Type localVarType = getType(localVar.desc);
484-
switch (localVarType.getSort()) {
485-
case Type.BOOLEAN:
486-
case Type.CHAR:
487-
case Type.BYTE:
488-
case Type.SHORT:
489-
case Type.INT:
490-
insnList.add(new InsnNode(Opcodes.ICONST_0));
491-
break;
492-
case Type.LONG:
493-
insnList.add(new InsnNode(Opcodes.LCONST_0));
494-
break;
495-
case Type.FLOAT:
496-
insnList.add(new InsnNode(Opcodes.FCONST_0));
497-
break;
498-
case Type.DOUBLE:
499-
insnList.add(new InsnNode(Opcodes.DCONST_0));
500-
break;
501-
default:
502-
insnList.add(new InsnNode(Opcodes.ACONST_NULL));
503-
break;
570+
if (previousVarByName != null) {
571+
// there are multiple local variables with the same name
572+
// checking type to see if they are compatible
573+
Type previousType = getType(previousVarByName.desc);
574+
Type currentType = getType(localVar.desc);
575+
if (!ASMHelper.isStoreCompatibleType(previousType, currentType)) {
576+
reportWarning(
577+
"Local variable "
578+
+ localVar.name
579+
+ " has multiple definitions with incompatible types: "
580+
+ previousVarByName.desc
581+
+ " and "
582+
+ localVar.desc);
583+
// remove name from hoistable
584+
hoistableVarByName.remove(localVar.name);
585+
return;
504586
}
505-
insnList.add(new VarInsnNode(localVarType.getOpcode(Opcodes.ISTORE), localVar.index));
587+
// Merge variables because compatible type
506588
}
507-
return localVarsByName.values();
589+
// by default, there is no conflict => hoistable
590+
hoistableVarByName.computeIfAbsent(localVar.name, k -> new ArrayList<>()).add(localVar);
508591
}
509592

510-
private boolean checkDuplicateLocalVar(
511-
LocalVariableNode localVar, Map<String, LocalVariableNode> localVarsByName) {
512-
LocalVariableNode previousVar = localVarsByName.putIfAbsent(localVar.name, localVar);
513-
if (previousVar == null) {
514-
return true;
593+
private void rewriteLocalVarInsn(LocalVariableNode localVar, int oldSlot, int newSlot) {
594+
// previous insn could be a store to index that need to be rewritten as well
595+
AbstractInsnNode previous = localVar.start.getPrevious();
596+
if (previous instanceof VarInsnNode) {
597+
VarInsnNode varInsnNode = (VarInsnNode) previous;
598+
if (varInsnNode.var == oldSlot) {
599+
varInsnNode.var = newSlot;
600+
}
515601
}
516-
// there are multiple local variables with the same name
517-
// checking type to see if they are compatible
518-
Type previousType = getType(previousVar.desc);
519-
Type currentType = getType(localVar.desc);
520-
if (!ASMHelper.isStoreCompatibleType(previousType, currentType)) {
521-
reportWarning(
522-
"Local variable "
523-
+ localVar.name
524-
+ " has multiple definitions with incompatible types: "
525-
+ previousVar.desc
526-
+ " and "
527-
+ localVar.desc);
528-
return false;
602+
for (AbstractInsnNode insn = localVar.start;
603+
insn != null && insn != localVar.end;
604+
insn = insn.getNext()) {
605+
if (insn instanceof VarInsnNode) {
606+
VarInsnNode varInsnNode = (VarInsnNode) insn;
607+
if (varInsnNode.var == oldSlot) {
608+
varInsnNode.var = newSlot;
609+
}
610+
}
611+
if (insn instanceof IincInsnNode) {
612+
IincInsnNode iincInsnNode = (IincInsnNode) insn;
613+
if (iincInsnNode.var == oldSlot) {
614+
iincInsnNode.var = newSlot;
615+
}
616+
}
529617
}
530-
return true;
531618
}
532619

533620
private void createInProbeFinallyHandler(LabelNode inProbeStartLabel, LabelNode inProbeEndLabel) {

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

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import static datadog.trace.bootstrap.debugger.util.Redaction.REDACTED_VALUE;
1212
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
1313
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertFalse;
1415
import static org.junit.jupiter.api.Assertions.assertNotNull;
1516
import static org.junit.jupiter.api.Assertions.assertNull;
1617
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1779,7 +1780,7 @@ public void uncaughtExceptionCaptureLocalVars() throws IOException, URISyntaxExc
17791780
"java.lang.RuntimeException",
17801781
"oops",
17811782
"com.datadog.debugger.CapturedSnapshot31.uncaughtException",
1782-
24);
1783+
30);
17831784
}
17841785

17851786
@Test
@@ -1810,6 +1811,18 @@ public void methodProbeLocalVarsDeepScopes() throws IOException, URISyntaxExcept
18101811
int result = Reflect.onClass(testClass).call("main", "deepScopes").get();
18111812
assertEquals(4, result);
18121813
Snapshot snapshot = assertOneSnapshot(listener);
1814+
// localVarL4 can not be captured/hoisted because same name, same slot
1815+
// LocalVariableTable:
1816+
// Start Length Slot Name Signature
1817+
// 59 3 6 localVarL4 I
1818+
// 65 3 6 localVarL4 I
1819+
// 71 3 6 localVarL4 I
1820+
// 29 45 5 localVarL3 I
1821+
// 20 54 4 localVarL2 I
1822+
// 13 64 3 localVarL1 I
1823+
// 0 79 0 this Lcom/datadog/debugger/CapturedSnapshot31;
1824+
// 0 79 1 arg Ljava/lang/String;
1825+
// 2 77 2 localVarL0 I
18131826
assertEquals(6, snapshot.getCaptures().getReturn().getLocals().size());
18141827
assertCaptureLocals(snapshot.getCaptures().getReturn(), "localVarL0", "int", "0");
18151828
assertCaptureLocals(snapshot.getCaptures().getReturn(), "localVarL1", "int", "1");
@@ -1852,6 +1865,40 @@ public void methodProbeExceptionLocalVars() throws IOException, URISyntaxExcepti
18521865
expectedFields);
18531866
}
18541867

1868+
@Test
1869+
@DisabledIf(
1870+
value = "datadog.trace.api.Platform#isJ9",
1871+
disabledReason = "we cannot get local variable debug info")
1872+
public void overlappingLocalVarSlot() throws IOException, URISyntaxException {
1873+
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot31";
1874+
LogProbe probe = createProbeAtExit(PROBE_ID, CLASS_NAME, "overlappingSlots", "(String)");
1875+
TestSnapshotListener listener = installProbes(CLASS_NAME, probe);
1876+
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
1877+
int result = Reflect.onClass(testClass).call("main", "overlappingSlots").get();
1878+
assertEquals(5, result);
1879+
Snapshot snapshot = assertOneSnapshot(listener);
1880+
// i local var cannot be hoisted because of overlapping slots and name
1881+
assertFalse(snapshot.getCaptures().getReturn().getLocals().containsKey("i"));
1882+
assertTrue(snapshot.getCaptures().getReturn().getLocals().containsKey("subStr"));
1883+
}
1884+
1885+
@Test
1886+
@DisabledIf(
1887+
value = "datadog.trace.api.Platform#isJ9",
1888+
disabledReason = "we cannot get local variable debug info")
1889+
public void duplicateLocalDifferentScope() throws IOException, URISyntaxException {
1890+
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot31";
1891+
LogProbe probe =
1892+
createProbeAtExit(PROBE_ID, CLASS_NAME, "duplicateLocalDifferentScope", "(String)");
1893+
TestSnapshotListener listener = installProbes(CLASS_NAME, probe);
1894+
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
1895+
int result = Reflect.onClass(testClass).call("main", "duplicateLocalDifferentScope").get();
1896+
assertEquals(28, result);
1897+
Snapshot snapshot = assertOneSnapshot(listener);
1898+
assertCaptureLocals(
1899+
snapshot.getCaptures().getReturn(), "ch", Character.TYPE.getTypeName(), "e");
1900+
}
1901+
18551902
@Test
18561903
public void enumConstructorArgs() throws IOException, URISyntaxException {
18571904
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot23";
@@ -2436,7 +2483,7 @@ private TestSnapshotListener installProbes(
24362483
Config config = mock(Config.class);
24372484
when(config.isDebuggerEnabled()).thenReturn(true);
24382485
when(config.isDebuggerClassFileDumpEnabled()).thenReturn(true);
2439-
when(config.isDebuggerVerifyByteCode()).thenReturn(true);
2486+
when(config.isDebuggerVerifyByteCode()).thenReturn(false);
24402487
when(config.getFinalDebuggerSnapshotUrl())
24412488
.thenReturn("http://localhost:8126/debugger/v1/input");
24422489
when(config.getFinalDebuggerSymDBUrl()).thenReturn("http://localhost:8126/symdb/v1/input");
@@ -2449,7 +2496,12 @@ private TestSnapshotListener installProbes(
24492496
instr.addTransformer(currentTransformer);
24502497
DebuggerAgentHelper.injectSink(listener);
24512498
DebuggerContext.initProbeResolver(
2452-
(encodedId) -> resolver(encodedId, logProbes, configuration.getSpanDecorationProbes()));
2499+
(encodedId) ->
2500+
resolver(
2501+
encodedId,
2502+
logProbes,
2503+
configuration.getSpanDecorationProbes(),
2504+
configuration.getDebuggerProbes()));
24532505
DebuggerContext.initClassFilter(new DenyListHelper(null));
24542506
DebuggerContext.initValueSerializer(new JsonSnapshotSerializer());
24552507
if (logProbes != null) {
@@ -2471,7 +2523,8 @@ private TestSnapshotListener installProbes(
24712523
private ProbeImplementation resolver(
24722524
String encodedId,
24732525
Collection<LogProbe> logProbes,
2474-
Collection<SpanDecorationProbe> spanDecorationProbes) {
2526+
Collection<SpanDecorationProbe> spanDecorationProbes,
2527+
Collection<DebuggerProbe> debuggerProbes) {
24752528
for (LogProbe probe : logProbes) {
24762529
if (probe.getProbeId().getEncodedId().equals(encodedId)) {
24772530
return probe;
@@ -2482,6 +2535,11 @@ private ProbeImplementation resolver(
24822535
return probe;
24832536
}
24842537
}
2538+
for (DebuggerProbe probe : debuggerProbes) {
2539+
if (probe.getProbeId().getEncodedId().equals(encodedId)) {
2540+
return probe;
2541+
}
2542+
}
24852543
return null;
24862544
}
24872545

0 commit comments

Comments
 (0)