Skip to content

Commit 884caf7

Browse files
committed
add safe local var hoisting
Introduce local var hoisting level: 0: no hoisting 1: safe hoisting 2: aggressive hoisting. for now we are only implementing safe hoisting. Aggressive will be later. Safe hoisting consists of scanning the bytecode instructions of the methods to find the store and load for local var, scan local variable table to find potential slot or name conflicts. the safe hoisting is done when there is only one variable per slot without name conflict and for the same type. hoisting is done by extending the range of the local variable to range of the method and initializing to 0 at the beginning of the method. Long and double variable are not part of the safe hoisting because they are using 2 slots and hoisting them can result in a conflict with another variable in another range. Also we prevent hoisting for the second slot for the same reason (forbidden slots)
1 parent ae1f104 commit 884caf7

File tree

8 files changed

+632
-294
lines changed

8 files changed

+632
-294
lines changed

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

Lines changed: 14 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import static com.datadog.debugger.instrumentation.ASMHelper.invokeVirtual;
77
import static com.datadog.debugger.instrumentation.ASMHelper.isFinalField;
88
import static com.datadog.debugger.instrumentation.ASMHelper.isStaticField;
9-
import static com.datadog.debugger.instrumentation.ASMHelper.isStore;
109
import static com.datadog.debugger.instrumentation.ASMHelper.ldc;
1110
import static com.datadog.debugger.instrumentation.ASMHelper.newInstance;
1211
import static com.datadog.debugger.instrumentation.Types.CAPTURED_CONTEXT_TYPE;
@@ -40,10 +39,7 @@
4039
import java.util.ArrayList;
4140
import java.util.Collection;
4241
import java.util.Collections;
43-
import java.util.HashMap;
44-
import java.util.HashSet;
4542
import java.util.List;
46-
import java.util.Map;
4743
import java.util.Set;
4844
import java.util.stream.Collectors;
4945
import org.objectweb.asm.Opcodes;
@@ -52,7 +48,6 @@
5248
import org.objectweb.asm.tree.ClassNode;
5349
import org.objectweb.asm.tree.FieldInsnNode;
5450
import org.objectweb.asm.tree.FieldNode;
55-
import org.objectweb.asm.tree.IincInsnNode;
5651
import org.objectweb.asm.tree.InsnList;
5752
import org.objectweb.asm.tree.InsnNode;
5853
import org.objectweb.asm.tree.JumpInsnNode;
@@ -75,7 +70,7 @@ public class CapturedContextInstrumentor extends Instrumentor {
7570
private int exitContextVar = -1;
7671
private int timestampStartVar = -1;
7772
private int throwableListVar = -1;
78-
private Collection<LocalVariableNode> unscopedLocalVars;
73+
private Collection<LocalVariableNode> hoistedLocalVars = Collections.emptyList();
7974

8075
public CapturedContextInstrumentor(
8176
ProbeDefinition definition,
@@ -413,12 +408,7 @@ private void instrumentMethodEnter() {
413408
if (methodNode.tryCatchBlocks.size() > 0) {
414409
throwableListVar = declareThrowableList(insnList);
415410
}
416-
unscopedLocalVars = Collections.emptyList();
417-
if (Config.get().isDynamicInstrumentationHoistLocalVarsEnabled()
418-
&& language == JvmLanguage.JAVA) {
419-
// for now, only hoist local vars for Java
420-
unscopedLocalVars = initAndHoistLocalVars(insnList);
421-
}
411+
hoistedLocalVars = initAndHoistLocalVars(methodNode);
422412
insnList.add(contextInitLabel);
423413
if (definition instanceof SpanDecorationProbe
424414
&& definition.getEvaluateAt() == MethodLocation.EXIT) {
@@ -485,212 +475,18 @@ private void instrumentMethodEnter() {
485475

486476
// Initialize and hoist local variables to the top of the method
487477
// if there is name/slot conflict, do nothing for the conflicting local variable
488-
private Collection<LocalVariableNode> initAndHoistLocalVars(InsnList insnList) {
489-
if (methodNode.localVariables == null || methodNode.localVariables.isEmpty()) {
478+
private Collection<LocalVariableNode> initAndHoistLocalVars(MethodNode methodNode) {
479+
int hoistingLevel = Config.get().getDynamicInstrumentationLocalVarHoistingLevel();
480+
if (hoistingLevel == 0 || language != JvmLanguage.JAVA) {
481+
// for now, only hoist local vars for Java
490482
return Collections.emptyList();
491483
}
492-
Map<String, LocalVariableNode> localVarsByName = new HashMap<>();
493-
Map<Integer, LocalVariableNode> localVarsBySlot = new HashMap<>();
494-
Map<String, Set<LocalVariableNode>> hoistableVarByName = new HashMap<>();
495-
for (LocalVariableNode localVar : methodNode.localVariables) {
496-
int idx = localVar.index - localVarBaseOffset;
497-
if (idx < argOffset) {
498-
// this is an argument
499-
continue;
500-
}
501-
checkHoistableLocalVar(localVar, localVarsByName, localVarsBySlot, hoistableVarByName);
502-
}
503-
removeDuplicatesFromArgs(hoistableVarByName, localVarsBySlotArray);
504-
// hoist vars
505-
List<LocalVariableNode> results = new ArrayList<>();
506-
for (Map.Entry<String, Set<LocalVariableNode>> entry : hoistableVarByName.entrySet()) {
507-
Set<LocalVariableNode> hoistableVars = entry.getValue();
508-
LocalVariableNode newVarNode;
509-
if (hoistableVars.size() > 1) {
510-
// merge variables
511-
LocalVariableNode firstHoistableVar = hoistableVars.iterator().next();
512-
String name = firstHoistableVar.name;
513-
String desc = firstHoistableVar.desc;
514-
Type localVarType = getType(desc);
515-
int newSlot = newVar(localVarType); // new slot for the local variable
516-
newVarNode = new LocalVariableNode(name, desc, null, null, null, newSlot);
517-
Set<LabelNode> endLabels = new HashSet<>();
518-
for (LocalVariableNode localVar : hoistableVars) {
519-
// rewrite each usage of the old var to the new slot
520-
rewriteLocalVarInsn(localVar, localVar.index, newSlot);
521-
endLabels.add(localVar.end);
522-
}
523-
hoistVar(insnList, newVarNode);
524-
newVarNode.end = findLatestLabel(methodNode.instructions, endLabels);
525-
// remove previous local variables
526-
methodNode.localVariables.removeIf(hoistableVars::contains);
527-
methodNode.localVariables.add(newVarNode);
528-
} else {
529-
// hoist the single variable and rewrite all its local var instructions
530-
newVarNode = hoistableVars.iterator().next();
531-
int oldIndex = newVarNode.index;
532-
newVarNode.index = newVar(getType(newVarNode.desc)); // new slot for the local variable
533-
rewriteLocalVarInsn(newVarNode, oldIndex, newVarNode.index);
534-
hoistVar(insnList, newVarNode);
535-
}
536-
results.add(newVarNode);
537-
}
538-
return results;
539-
}
540-
541-
private void removeDuplicatesFromArgs(
542-
Map<String, Set<LocalVariableNode>> hoistableVarByName,
543-
LocalVariableNode[] localVarsBySlotArray) {
544-
for (int idx = 0; idx < argOffset; idx++) {
545-
LocalVariableNode localVar = localVarsBySlotArray[idx];
546-
if (localVar == null) {
547-
continue;
548-
}
549-
// we are removing local variables that are arguments
550-
hoistableVarByName.remove(localVar.name);
551-
}
552-
}
553-
554-
private LabelNode findLatestLabel(InsnList instructions, Set<LabelNode> endLabels) {
555-
for (AbstractInsnNode insn = instructions.getLast(); insn != null; insn = insn.getPrevious()) {
556-
if (insn instanceof LabelNode && endLabels.contains(insn)) {
557-
return (LabelNode) insn;
558-
}
559-
}
560-
return null;
561-
}
562-
563-
private void hoistVar(InsnList insnList, LocalVariableNode varNode) {
564-
LabelNode labelNode = new LabelNode(); // new start label for the local variable
565-
insnList.add(labelNode);
566-
varNode.start = labelNode; // update the start label of the local variable
567-
Type localVarType = getType(varNode.desc);
568-
addStore0Insn(insnList, varNode, localVarType);
569-
}
570-
571-
private static void addStore0Insn(
572-
InsnList insnList, LocalVariableNode localVar, Type localVarType) {
573-
switch (localVarType.getSort()) {
574-
case Type.BOOLEAN:
575-
case Type.CHAR:
576-
case Type.BYTE:
577-
case Type.SHORT:
578-
case Type.INT:
579-
insnList.add(new InsnNode(Opcodes.ICONST_0));
580-
break;
581-
case Type.LONG:
582-
insnList.add(new InsnNode(Opcodes.LCONST_0));
583-
break;
584-
case Type.FLOAT:
585-
insnList.add(new InsnNode(Opcodes.FCONST_0));
586-
break;
587-
case Type.DOUBLE:
588-
insnList.add(new InsnNode(Opcodes.DCONST_0));
589-
break;
590-
default:
591-
insnList.add(new InsnNode(Opcodes.ACONST_NULL));
592-
break;
593-
}
594-
insnList.add(new VarInsnNode(localVarType.getOpcode(Opcodes.ISTORE), localVar.index));
595-
}
596-
597-
private void checkHoistableLocalVar(
598-
LocalVariableNode localVar,
599-
Map<String, LocalVariableNode> localVarsByName,
600-
Map<Integer, LocalVariableNode> localVarsBySlot,
601-
Map<String, Set<LocalVariableNode>> hoistableVarByName) {
602-
LocalVariableNode previousVarBySlot = localVarsBySlot.putIfAbsent(localVar.index, localVar);
603-
LocalVariableNode previousVarByName = localVarsByName.putIfAbsent(localVar.name, localVar);
604-
if (previousVarBySlot != null) {
605-
// there are multiple local variables with the same slot but different names
606-
// by hoisting in a new slot, we can avoid the conflict
607-
hoistableVarByName.computeIfAbsent(localVar.name, k -> new HashSet<>()).add(localVar);
608-
}
609-
if (previousVarByName != null) {
610-
// there are multiple local variables with the same name
611-
// checking type to see if they are compatible
612-
Type previousType = getType(previousVarByName.desc);
613-
Type currentType = getType(localVar.desc);
614-
if (!ASMHelper.isStoreCompatibleType(previousType, currentType)) {
615-
reportWarning(
616-
"Local variable "
617-
+ localVar.name
618-
+ " has multiple definitions with incompatible types: "
619-
+ previousVarByName.desc
620-
+ " and "
621-
+ localVar.desc);
622-
// remove name from hoistable
623-
hoistableVarByName.remove(localVar.name);
624-
return;
625-
}
626-
// Merge variables because compatible type
627-
}
628-
// by default, there is no conflict => hoistable
629-
hoistableVarByName.computeIfAbsent(localVar.name, k -> new HashSet<>()).add(localVar);
630-
}
631-
632-
private void rewriteLocalVarInsn(LocalVariableNode localVar, int oldSlot, int newSlot) {
633-
rewritePreviousStoreInsn(localVar, oldSlot, newSlot);
634-
for (AbstractInsnNode insn = localVar.start;
635-
insn != null && insn != localVar.end;
636-
insn = insn.getNext()) {
637-
if (insn instanceof VarInsnNode) {
638-
VarInsnNode varInsnNode = (VarInsnNode) insn;
639-
if (varInsnNode.var == oldSlot) {
640-
varInsnNode.var = newSlot;
641-
}
642-
}
643-
if (insn instanceof IincInsnNode) {
644-
IincInsnNode iincInsnNode = (IincInsnNode) insn;
645-
if (iincInsnNode.var == oldSlot) {
646-
iincInsnNode.var = newSlot;
647-
}
648-
}
649-
}
650-
}
651-
652-
// Previous insn(s) to local var range could be a store to index that need to be rewritten as well
653-
// LocalVariableTable ranges starts after the init of the local var.
654-
// ex:
655-
// 9: iconst_0
656-
// 10: astore_1
657-
// 11: aload_1
658-
// range for slot 1 starts at 11
659-
// javac often starts the range right after the init of the local var, so we can just look for
660-
// the previous instruction. But not always, and we put an arbitrary limit to 10 instructions
661-
// for kotlinc, many instructions can separate the init and the range start
662-
// ex:
663-
// LocalVariableTable:
664-
// Start Length Slot Name Signature
665-
// 70 121 4 $i$f$map I
666-
//
667-
// 64: istore 4
668-
// 66: aload_2
669-
// 67: iconst_2
670-
// 68: iconst_1
671-
// 69: bastore
672-
// 70: aload_3
673-
private static void rewritePreviousStoreInsn(
674-
LocalVariableNode localVar, int oldSlot, int newSlot) {
675-
AbstractInsnNode previous = localVar.start.getPrevious();
676-
int processed = 0;
677-
// arbitrary fixing limit to 10 previous instructions to look at
678-
while (previous != null && !isVarStoreForSlot(previous, oldSlot) && processed < 10) {
679-
previous = previous.getPrevious();
680-
processed++;
681-
}
682-
if (isVarStoreForSlot(previous, oldSlot)) {
683-
VarInsnNode varInsnNode = (VarInsnNode) previous;
684-
if (varInsnNode.var == oldSlot) {
685-
varInsnNode.var = newSlot;
686-
}
484+
if (methodNode.localVariables == null || methodNode.localVariables.isEmpty()) {
485+
return Collections.emptyList();
687486
}
688-
}
689-
690-
private static boolean isVarStoreForSlot(AbstractInsnNode node, int slotIdx) {
691-
return node instanceof VarInsnNode
692-
&& isStore(node.getOpcode())
693-
&& ((VarInsnNode) node).var == slotIdx;
487+
LOGGER.debug(
488+
"Hoisting local variables level={} for method: {}", hoistingLevel, methodNode.name);
489+
return LocalVarHoisting.processMethod(methodNode, hoistingLevel);
694490
}
695491

696492
private void createInProbeFinallyHandler(LabelNode inProbeStartLabel, LabelNode inProbeEndLabel) {
@@ -916,20 +712,18 @@ private void collectLocalVariables(AbstractInsnNode location, InsnList insnList)
916712
return;
917713
}
918714
Collection<LocalVariableNode> localVarNodes;
919-
boolean isLocalVarHoistingEnabled =
920-
Config.get().isDynamicInstrumentationHoistLocalVarsEnabled();
921-
if (definition.isLineProbe() || !isLocalVarHoistingEnabled) {
715+
if (definition.isLineProbe() || hoistedLocalVars.isEmpty()) {
922716
localVarNodes = methodNode.localVariables;
923717
} else {
924-
localVarNodes = unscopedLocalVars;
718+
localVarNodes = hoistedLocalVars;
925719
}
926720
List<LocalVariableNode> applicableVars = new ArrayList<>();
927721
boolean isLineProbe = definition.isLineProbe();
928722
for (LocalVariableNode variableNode : localVarNodes) {
929723
int idx = variableNode.index - localVarBaseOffset;
930724
if (idx >= argOffset) {
931725
// var is local not arg
932-
if (isLineProbe || !isLocalVarHoistingEnabled) {
726+
if (isLineProbe || hoistedLocalVars.isEmpty()) {
933727
if (ASMHelper.isInScope(methodNode, variableNode, location)) {
934728
applicableVars.add(variableNode);
935729
}

0 commit comments

Comments
 (0)