Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memoized type matching #5026

Merged
merged 8 commits into from
May 11, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import datadog.trace.agent.tooling.bytebuddy.DDOutlinePoolStrategy;
import datadog.trace.agent.tooling.bytebuddy.SharedTypePools;
import datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers;
import datadog.trace.agent.tooling.bytebuddy.memoize.MemoizedMatchers;
import datadog.trace.agent.tooling.usm.UsmExtractorImpl;
import datadog.trace.agent.tooling.usm.UsmMessageFactoryImpl;
import datadog.trace.api.InstrumenterConfig;
Expand Down Expand Up @@ -101,7 +102,11 @@ public static ClassFileTransformer installBytebuddyAgent(
DDCachingPoolStrategy.registerAsSupplier();
}

DDElementMatchers.registerAsSupplier();
if (InstrumenterConfig.get().isResolverMemoizingEnabled()) {
MemoizedMatchers.registerAsSupplier();
} else {
DDElementMatchers.registerAsSupplier();
}

if (enabledSystems.contains(Instrumenter.TargetSystem.USM)) {
UsmMessageFactoryImpl.registerAsSupplier();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public static ElementMatcher.Junction<ClassLoader> hasClassNamedOneOf(
return new ElementMatcher.Junction.Disjunction<>(matchers);
}

public static void reset() {
public static void resetState() {
hasClassCache.clear();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package datadog.trace.agent.tooling.bytebuddy.memoize;

import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter;
import java.util.BitSet;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

/** Matches types that should have context-store fields injected. */
final class HasContextField extends ElementMatcher.Junction.ForNonNullValues<TypeDescription> {
private final ElementMatcher<TypeDescription> storeMatcher;
private final ElementMatcher<TypeDescription> skipMatcher;
private final BitSet skippableStoreIds = new BitSet();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems less exact than the weak store id bit sets in ShouldInjectFieldsState because of the maybeSkip, if I read the code correctly. Is there a possibility that we use a weak store unnecessary, or are could we just add redundant code that never gets used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is potentially less exact - let me add some tests to confirm when it would make a difference, I can then make it more exact based on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out the java-completablefuture tests already demonstrate the difference - and the new approach is actually more deterministic. For each context class that can be skipped/excluded we collect store ids associated with that context class (ie. the value side of all the context-value associations) and therefore might need to be 'skipped' when that excluded class is a super-class - by delegating to the weak map instead.

When we do find a context class that should be excluded and it's a super-class of the class we're injecting then we notify the field-injection code that super-class requests involving those stores should delegate to the weak map. That part of the injected code will only be used if someone tries to push/get values to those stores. If a store isn't used then it just means we injected an unused case block.

The old approach was more lazy in that it only recorded a store as needing weak map delegation at the time the match was made. Unfortunately variations in class-loading order can make this non-deterministic - for example with the java-completablefuture tests the UniCompletion class is processed before its superclasses (since both are not yet loaded at matching time) so it doesn't realize that it should delegate Runnable / ForkJoinTask super-class requests to the weak map. This changes if the Completion class is forcibly loaded first, because it now sees the skipped class before processing UniCompletion.

In practice this doesn't make a difference because we add a last-resort catch block to handle unexpected requests and this catch block ends up being used by the old approach. Whereas with the new approach we always add those weak map case blocks (and avoid triggering the exceptional catch block)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mcculls for the detailed explanation. Determinism FTW!


HasContextField(ElementMatcher<TypeDescription> storeMatcher) {
this(storeMatcher, null);
}

HasContextField(
ElementMatcher<TypeDescription> storeMatcher, ElementMatcher<TypeDescription> skipMatcher) {
this.storeMatcher = storeMatcher;
this.skipMatcher = skipMatcher;
}

@Override
protected boolean doMatch(TypeDescription target) {
// match first type in the hierarchy which injects this store, that isn't skipped
return storeMatcher.matches(target)
&& (null == skipMatcher || !skipMatcher.matches(target))
&& (!storeMatcher.matches(target.getSuperClass().asErasure()));
}

void maybeSkip(int contextStoreId) {
skippableStoreIds.set(contextStoreId);
}

boolean hasSuperStore(TypeDescription target, BitSet weakStoreIds) {
TypeDescription superTarget = target.getSuperClass().asErasure();
boolean hasSuperStore = storeMatcher.matches(superTarget);
if (hasSuperStore) {
// report if super-class was skipped from field-injection
if (null != skipMatcher && skipMatcher.matches(superTarget)) {
weakStoreIds.or(skippableStoreIds);
}
}
return hasSuperStore;
}

/** Matches types that would have had fields injected, but were explicitly excluded. */
static final class Skip extends ElementMatcher.Junction.ForNonNullValues<TypeDescription> {
private final ExcludeFilter.ExcludeType excludeType;

Skip(ExcludeFilter.ExcludeType excludeType) {
this.excludeType = excludeType;
}

@Override
protected boolean doMatch(TypeDescription target) {
return ExcludeFilter.exclude(excludeType, target.getName());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a comment on this PR, but more of an idea. I assume that this isn't hit that often now with memoization, but would it make sense to let the exclude filter build up a trie and use that instead of the hash set lookup and string comparisons?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, worth looking into

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package datadog.trace.agent.tooling.bytebuddy.memoize;

import static net.bytebuddy.matcher.ElementMatchers.hasSignature;

import java.util.HashSet;
import java.util.Set;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.description.type.TypeList;
import net.bytebuddy.matcher.ElementMatcher;

/**
* Matches methods that are either a direct match or have a super-class method with the same
* signature that matches. Uses a memoized type matcher to reduce the potential search space.
*/
final class HasSuperMethod extends ElementMatcher.Junction.ForNonNullValues<MethodDescription> {
private final ElementMatcher<TypeDescription> typeMatcher;
private final ElementMatcher<? super MethodDescription> methodMatcher;

HasSuperMethod(
ElementMatcher<TypeDescription> typeMatcher,
ElementMatcher<? super MethodDescription> methodMatcher) {
this.typeMatcher = typeMatcher;
this.methodMatcher = methodMatcher;
}

@Override
protected boolean doMatch(MethodDescription target) {
if (target.isConstructor()) {
return false;
}

TypeDefinition type = target.getDeclaringType();
if (!typeMatcher.matches(type.asErasure())) {
return false; // no further matches recorded in hierarchy
} else if (methodMatcher.matches(target)) {
return true; // direct match, no need to check hierarchy
}

// need to search hierarchy; use expected signature to filter candidate methods
ElementMatcher<MethodDescription> signatureMatcher = hasSignature(target.asSignatureToken());
Set<String> visited = new HashSet<>();

if (interfaceMatches(type.getInterfaces(), signatureMatcher, visited)) {
return true;
}

type = type.getSuperClass();
while (null != type && typeMatcher.matches(type.asErasure())) {
for (MethodDescription method : type.getDeclaredMethods()) {
if (signatureMatcher.matches(method) && methodMatcher.matches(method)) {
return true;
}
}
if (interfaceMatches(type.getInterfaces(), signatureMatcher, visited)) {
return true;
}
type = type.getSuperClass();
}
return false;
}

private boolean interfaceMatches(
TypeList.Generic interfaces,
ElementMatcher<MethodDescription> signatureMatcher,
Set<String> visited) {
for (TypeDefinition type : interfaces) {
if (!visited.add(type.getTypeName()) || !typeMatcher.matches(type.asErasure())) {
continue; // skip if already visited or that part of the hierarchy doesn't match
}
for (MethodDescription method : type.getDeclaredMethods()) {
if (signatureMatcher.matches(method) && methodMatcher.matches(method)) {
return true;
}
}
if (interfaceMatches(type.getInterfaces(), signatureMatcher, visited)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package datadog.trace.agent.tooling.bytebuddy.memoize;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.ANNOTATION;
import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.CLASS;
import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.FIELD;
import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.INTERFACE;
import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.METHOD;
import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.TYPE;
import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStoreId;

import datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers;
import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

/** Supplies memoized matchers. */
public final class MemoizedMatchers implements HierarchyMatchers.Supplier {
public static void registerAsSupplier() {
HierarchyMatchers.registerIfAbsent(new MemoizedMatchers());
Memoizer.resetState();
}

@Override
public ElementMatcher.Junction<TypeDescription> declaresAnnotation(
ElementMatcher.Junction<? super NamedElement> matcher) {
return Memoizer.prepare(ANNOTATION, matcher, false);
}

@Override
public ElementMatcher.Junction<TypeDescription> declaresField(
ElementMatcher.Junction<? super FieldDescription> matcher) {
return Memoizer.prepare(FIELD, matcher, false);
}

@Override
public ElementMatcher.Junction<TypeDescription> declaresMethod(
ElementMatcher.Junction<? super MethodDescription> matcher) {
return Memoizer.prepare(METHOD, matcher, false);
}

@Override
public ElementMatcher.Junction<TypeDescription> concreteClass() {
return Memoizer.isConcrete;
}

@Override
public ElementMatcher.Junction<TypeDescription> extendsClass(
ElementMatcher.Junction<? super TypeDescription> matcher) {
return Memoizer.prepare(CLASS, matcher, true);
}

@Override
public ElementMatcher.Junction<TypeDescription> implementsInterface(
ElementMatcher.Junction<? super TypeDescription> matcher) {
return Memoizer.isClass.and(Memoizer.prepare(INTERFACE, matcher, true));
}

@Override
public ElementMatcher.Junction<TypeDescription> hasInterface(
ElementMatcher.Junction<? super TypeDescription> matcher) {
return Memoizer.prepare(INTERFACE, matcher, true);
}

@Override
public ElementMatcher.Junction<TypeDescription> hasSuperType(
ElementMatcher.Junction<? super TypeDescription> matcher) {
return Memoizer.isClass.and(Memoizer.prepare(TYPE, matcher, true));
}

@Override
public ElementMatcher.Junction<MethodDescription> hasSuperMethod(
ElementMatcher.Junction<? super MethodDescription> matcher) {
return new HasSuperMethod(Memoizer.prepare(METHOD, matcher, true), matcher);
}

/** Keeps track of which context-field matchers we've supplied so far. */
private static final Map<String, HasContextField> contextFields = new HashMap<>();

@Override
public ElementMatcher.Junction<TypeDescription> declaresContextField(
String keyType, String contextType) {

// is there a chance a type might match, but be excluded (skipped) from field-injection?
ExcludeFilter.ExcludeType excludeType = ExcludeFilter.ExcludeType.fromFieldType(keyType);

HasContextField contextField = contextFields.get(keyType);
if (null == contextField) {
ElementMatcher<TypeDescription> storeMatcher = hasSuperType(named(keyType));
if (null != excludeType) {
ElementMatcher<TypeDescription> skipMatcher =
Memoizer.prepare(CLASS, new HasContextField.Skip(excludeType), true);
contextField = new HasContextField(storeMatcher, skipMatcher);
} else {
contextField = new HasContextField(storeMatcher);
}
contextFields.put(keyType, contextField);
}

if (null != excludeType) {
// record that this store may be skipped from field-injection
contextField.maybeSkip(getContextStoreId(keyType, contextType));
}

return contextField;
}

/**
* Returns {@code true} if ancestors of the given type have any context-stores.
*
* <p>Stores which were skipped from field-injection in the super-class hierarchy are recorded in
* {@code weakStoreIds} so the appropriate weak-map delegation can be applied when injecting code
* to handle context-store requests.
*/
public static boolean hasSuperStores(TypeDescription target, BitSet weakStoreIds) {
boolean hasSuperStores = false;
for (HasContextField contextField : contextFields.values()) {
if (contextField.hasSuperStore(target, weakStoreIds)) {
hasSuperStores = true;
}
}
return hasSuperStores;
}
}
Loading