Skip to content

Commit

Permalink
Refactor probe matching for methods
Browse files Browse the repository at this point in the history
To be able to match probe with more flexibility (probes targeting a
method with or without signature) we are iterating over all methods
of a class and using Where::isMethodMatching to select probe
definitions that will be apply on the current method.
With this new approach we are dropping the support of overloaded
methods: only one method is instrumented or a definition is applied
only once.
  • Loading branch information
jpbempel committed Nov 26, 2024
1 parent 565afcd commit f2cc473
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -240,22 +240,6 @@ public byte[] transform(
return null;
}

private Map<Where, List<ProbeDefinition>> mergeLocations(
List<ProbeDefinition> definitions, ClassFileLines classFileLines) {
Map<Where, List<ProbeDefinition>> mergedProbes = new HashMap<>();
for (ProbeDefinition definition : definitions) {
Where where = definition.getWhere();
if (definition instanceof ForceMethodInstrumentation) {
// normalize where for line => to precise method location
where = Where.convertLineToMethod(definition.getWhere(), classFileLines);
}
List<ProbeDefinition> instrumentationDefinitions =
mergedProbes.computeIfAbsent(where, key -> new ArrayList<>());
instrumentationDefinitions.add(definition);
}
return mergedProbes;
}

private boolean skipInstrumentation(ClassLoader loader, String classFilePath) {
if (definitionMatcher.isEmpty()) {
log.warn("No debugger definitions present.");
Expand Down Expand Up @@ -496,41 +480,35 @@ private boolean performInstrumentation(
ClassNode classNode) {
boolean transformed = false;
ClassFileLines classFileLines = new ClassFileLines(classNode);
Map<Where, List<ProbeDefinition>> definitionByLocation =
mergeLocations(definitions, classFileLines);
// FIXME build a map also for methods to optimize the matching, currently O(probes*methods)
for (Map.Entry<Where, List<ProbeDefinition>> entry : definitionByLocation.entrySet()) {
Where where = entry.getKey();
String methodName = where.getMethodName();
String[] lines = where.getLines();
List<MethodNode> methodNodes;
if (methodName == null && lines != null) {
MethodNode methodNode = matchSourceFile(classNode, where, classFileLines);
methodNodes = methodNode != null ? singletonList(methodNode) : Collections.emptyList();
} else {
methodNodes = matchMethodDescription(classNode, where, classFileLines);
Set<ProbeDefinition> remainingDefinitions = new HashSet<>(definitions);
for (MethodNode methodNode : classNode.methods) {
List<ProbeDefinition> matchingDefs = new ArrayList<>();
for (ProbeDefinition definition : definitions) {
if (definition.getWhere().isMethodMatching(methodNode, classFileLines)
&& remainingDefinitions.contains(definition)) {
matchingDefs.add(definition);
remainingDefinitions.remove(definition);
}
}
List<ProbeDefinition> definitionsByWhere = entry.getValue();
if (methodNodes.isEmpty()) {
reportLocationNotFound(definitionsByWhere, classNode.name, methodName);
if (matchingDefs.isEmpty()) {
continue;
}
for (MethodNode methodNode : methodNodes) {
if (log.isDebugEnabled()) {
List<String> probeIds =
definitionsByWhere.stream().map(ProbeDefinition::getId).collect(toList());
log.debug(
"Instrumenting method: {}.{}{} for probe ids: {}",
fullyQualifiedClassName,
methodNode.name,
methodNode.desc,
probeIds);
}
MethodInfo methodInfo = new MethodInfo(loader, classNode, methodNode, classFileLines);
InstrumentationResult result = applyInstrumentation(methodInfo, definitionsByWhere);
transformed |= result.isInstalled();
handleInstrumentationResult(definitionsByWhere, result);
if (log.isDebugEnabled()) {
List<String> probeIds = matchingDefs.stream().map(ProbeDefinition::getId).collect(toList());
log.debug(
"Instrumenting method: {}.{}{} for probe ids: {}",
fullyQualifiedClassName,
methodNode.name,
methodNode.desc,
probeIds);
}
MethodInfo methodInfo = new MethodInfo(loader, classNode, methodNode, classFileLines);
InstrumentationResult result = applyInstrumentation(methodInfo, matchingDefs);
transformed |= result.isInstalled();
handleInstrumentationResult(matchingDefs, result);
}
for (ProbeDefinition definition : remainingDefinitions) {
reportLocationNotFound(definition, classNode.name, definition.getWhere().getMethodName());
}
return transformed;
}
Expand All @@ -556,9 +534,9 @@ private void handleInstrumentationResult(
}

private void reportLocationNotFound(
List<ProbeDefinition> definitions, String className, String methodName) {
ProbeDefinition definition, String className, String methodName) {
if (methodName != null) {
reportErrorForAllProbes(definitions, CANNOT_FIND_METHOD, className, methodName);
reportErrorForAllProbes(singletonList(definition), CANNOT_FIND_METHOD, className, methodName);
return;
}
// This is a line probe, so we don't report line not found because the line may be found later
Expand Down Expand Up @@ -628,39 +606,44 @@ static class ToInstrumentInfo {
}
}

private static boolean isCapturedContextProbe(ProbeDefinition definition) {
return definition instanceof LogProbe
|| definition instanceof SpanDecorationProbe
|| definition instanceof TriggerProbe;
}

private List<ToInstrumentInfo> filterAndSortDefinitions(
List<ProbeDefinition> definitions, ClassFileLines classFileLines) {
List<ToInstrumentInfo> toInstrument = new ArrayList<>();
List<ProbeDefinition> capturedContextProbes = new ArrayList<>();
Map<Where, List<ProbeDefinition>> capturedContextLineProbes = new HashMap<>();
boolean addedExceptionProbe = false;
for (ProbeDefinition definition : definitions) {
// Log and span decoration probe shared the same instrumentor: CaptureContextInstrumentor
// and therefore need to be instrumented once
// note: exception probes are log probes and are handled the same way
if (definition instanceof LogProbe
|| definition instanceof SpanDecorationProbe
|| definition instanceof TriggerProbe) {
if (definition instanceof ExceptionProbe) {
if (addedExceptionProbe) {
continue;
if (isCapturedContextProbe(definition)) {
if (definition.isLineProbe()) {
capturedContextLineProbes
.computeIfAbsent(definition.getWhere(), key -> new ArrayList<>())
.add(definition);
} else {
if (definition instanceof ExceptionProbe) {
if (addedExceptionProbe) {
continue;
}
// only add one exception probe to the list of probes to instrument
// to have only one instance (1 probeId) of exception probe to handle all exceptions
addedExceptionProbe = true;
}
// only add one exception probe to the list of probes to instrument
// to have only one instance (1 probeId) of exception probe to handle all exceptions
addedExceptionProbe = true;
capturedContextProbes.add(definition);
}
capturedContextProbes.add(definition);
} else {
toInstrument.add(new ToInstrumentInfo(definition, singletonList(definition.getProbeId())));
}
}
if (!capturedContextProbes.isEmpty()) {
List<ProbeId> probesIds =
capturedContextProbes.stream().map(ProbeDefinition::getProbeId).collect(toList());
ProbeDefinition referenceDefinition =
selectReferenceDefinition(capturedContextProbes, classFileLines);
toInstrument.add(new ToInstrumentInfo(referenceDefinition, probesIds));
}
// LOGGER.debug("exception probe is already instrumented for {}", preciseWhere);
processCapturedContextLineProbes(capturedContextLineProbes, toInstrument);
processCapturedContextMethodProbes(classFileLines, capturedContextProbes, toInstrument);
// ordering: metric < log < span decoration < span
toInstrument.sort(
(info1, info2) -> {
Expand All @@ -671,6 +654,32 @@ private List<ToInstrumentInfo> filterAndSortDefinitions(
return toInstrument;
}

private void processCapturedContextMethodProbes(
ClassFileLines classFileLines,
List<ProbeDefinition> capturedContextProbes,
List<ToInstrumentInfo> toInstrument) {
if (capturedContextProbes.isEmpty()) {
return;
}
List<ProbeId> probesIds =
capturedContextProbes.stream().map(ProbeDefinition::getProbeId).collect(toList());
ProbeDefinition referenceDefinition =
selectReferenceDefinition(capturedContextProbes, classFileLines);
toInstrument.add(new ToInstrumentInfo(referenceDefinition, probesIds));
}

private static void processCapturedContextLineProbes(
Map<Where, List<ProbeDefinition>> lineProbes, List<ToInstrumentInfo> toInstrument) {
for (Map.Entry<Where, List<ProbeDefinition>> entry : lineProbes.entrySet()) {
if (entry.getValue().isEmpty()) {
continue;
}
List<ProbeId> probeIds =
entry.getValue().stream().map(ProbeDefinition::getProbeId).collect(toList());
toInstrument.add(new ToInstrumentInfo(entry.getValue().get(0), probeIds));
}
}

// Log & Span Decoration probes share the same instrumentor so only one definition should be
// used to generate the instrumentation. This method generate a synthetic definition that
// match the type of the probe to instrument: if at least one probe is LogProbe then we are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,22 @@ public void mergedProbesConditionMixedLocation() throws IOException, URISyntaxEx
assertNull(listener.snapshots.get(1).getEvaluationErrors());
}

@Test
public void mergedProbesDifferentSignature() throws IOException, URISyntaxException {
final String CLASS_NAME = "CapturedSnapshot08";
LogProbe probe1 = createProbeAtExit(PROBE_ID1, CLASS_NAME, "doit", null);
LogProbe probe2 = createProbeAtExit(PROBE_ID2, CLASS_NAME, "doit", "int (java.lang.String)");
LogProbe probe3 = createProbeAtExit(PROBE_ID3, CLASS_NAME, "doit", "(String)");
TestSnapshotListener listener = installProbes(probe1, probe2, probe3);
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
int result = Reflect.onClass(testClass).call("main", "1").get();
Assertions.assertEquals(3, result);
Assertions.assertEquals(3, listener.snapshots.size());
assertNull(listener.snapshots.get(0).getEvaluationErrors());
assertNull(listener.snapshots.get(1).getEvaluationErrors());
assertNull(listener.snapshots.get(2).getEvaluationErrors());
}

@Test
public void fields() throws IOException, URISyntaxException {
final String CLASS_NAME = "CapturedSnapshot06";
Expand Down Expand Up @@ -1579,11 +1595,9 @@ public void overloadedMethods() throws Exception {
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
int result = Reflect.onClass(testClass).call("main", "").get();
assertEquals(63, result);
List<Snapshot> snapshots = assertSnapshots(listener, 4, PROBE_ID, PROBE_ID, PROBE_ID, PROBE_ID);
List<Snapshot> snapshots = assertSnapshots(listener, 1, PROBE_ID);
assertCaptureReturnValue(snapshots.get(0).getCaptures().getReturn(), "int", "42");
assertCaptureArgs(snapshots.get(1).getCaptures().getEntry(), "s", "java.lang.String", "1");
assertCaptureArgs(snapshots.get(2).getCaptures().getEntry(), "s", "java.lang.String", "2");
assertCaptureArgs(snapshots.get(3).getCaptures().getEntry(), "s", "java.lang.String", "3");
assertEquals(1, snapshots.get(0).getCaptures().getEntry().getArguments().size());
}

@Test
Expand Down

0 comments on commit f2cc473

Please sign in to comment.