Skip to content

Commit 33b430f

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 c5581ea commit 33b430f

File tree

7 files changed

+655
-287
lines changed

7 files changed

+655
-287
lines changed

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

Lines changed: 14 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import java.util.ArrayList;
4141
import java.util.Collection;
4242
import java.util.Collections;
43-
import java.util.HashMap;
4443
import java.util.HashSet;
4544
import java.util.List;
4645
import java.util.Map;
@@ -75,7 +74,7 @@ public class CapturedContextInstrumentor extends Instrumentor {
7574
private int exitContextVar = -1;
7675
private int timestampStartVar = -1;
7776
private int throwableListVar = -1;
78-
private Collection<LocalVariableNode> unscopedLocalVars;
77+
private Collection<LocalVariableNode> hoistedLocalVars = Collections.emptyList();
7978

8079
public CapturedContextInstrumentor(
8180
ProbeDefinition definition,
@@ -413,12 +412,7 @@ private void instrumentMethodEnter() {
413412
if (methodNode.tryCatchBlocks.size() > 0) {
414413
throwableListVar = declareThrowableList(insnList);
415414
}
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-
}
415+
hoistedLocalVars = initAndHoistLocalVars(methodNode);
422416
insnList.add(contextInitLabel);
423417
if (definition instanceof SpanDecorationProbe
424418
&& definition.getEvaluateAt() == MethodLocation.EXIT) {
@@ -485,213 +479,20 @@ private void instrumentMethodEnter() {
485479

486480
// Initialize and hoist local variables to the top of the method
487481
// 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()) {
482+
private Collection<LocalVariableNode> initAndHoistLocalVars(MethodNode methodNode) {
483+
int hoistingLevel = Config.get().getDynamicInstrumentationLocalVarHoistingLevel();
484+
if (hoistingLevel == 0 || language != JvmLanguage.JAVA) {
485+
// for now, only hoist local vars for Java
490486
return Collections.emptyList();
491487
}
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-
}
488+
if (methodNode.localVariables == null || methodNode.localVariables.isEmpty()) {
489+
return Collections.emptyList();
687490
}
491+
LOGGER.debug(
492+
"Hoisting local variables level={} for method: {}", hoistingLevel, methodNode.name);
493+
return LocalVarHoisting.processMethod(methodNode, hoistingLevel);
688494
}
689495

690-
private static boolean isVarStoreForSlot(AbstractInsnNode node, int slotIdx) {
691-
return node instanceof VarInsnNode
692-
&& isStore(node.getOpcode())
693-
&& ((VarInsnNode) node).var == slotIdx;
694-
}
695496

696497
private void createInProbeFinallyHandler(LabelNode inProbeStartLabel, LabelNode inProbeEndLabel) {
697498
LabelNode handlerLabel = new LabelNode();
@@ -916,20 +717,18 @@ private void collectLocalVariables(AbstractInsnNode location, InsnList insnList)
916717
return;
917718
}
918719
Collection<LocalVariableNode> localVarNodes;
919-
boolean isLocalVarHoistingEnabled =
920-
Config.get().isDynamicInstrumentationHoistLocalVarsEnabled();
921-
if (definition.isLineProbe() || !isLocalVarHoistingEnabled) {
720+
if (definition.isLineProbe() || hoistedLocalVars.isEmpty()) {
922721
localVarNodes = methodNode.localVariables;
923722
} else {
924-
localVarNodes = unscopedLocalVars;
723+
localVarNodes = hoistedLocalVars;
925724
}
926725
List<LocalVariableNode> applicableVars = new ArrayList<>();
927726
boolean isLineProbe = definition.isLineProbe();
928727
for (LocalVariableNode variableNode : localVarNodes) {
929728
int idx = variableNode.index - localVarBaseOffset;
930729
if (idx >= argOffset) {
931730
// var is local not arg
932-
if (isLineProbe || !isLocalVarHoistingEnabled) {
731+
if (isLineProbe || hoistedLocalVars.isEmpty()) {
933732
if (ASMHelper.isInScope(methodNode, variableNode, location)) {
934733
applicableVars.add(variableNode);
935734
}

0 commit comments

Comments
 (0)