ServiceLoader: Optimize ServiceLoader.iterator().next()
The optimization is enabled only when the following rule is present:
-assumenosideeffects class java.util.ServiceLoader {
*** load(...);
}
Bug: b/291923475
Change-Id: If73bb7c612a3b6a02c790fa69aced09e3ba694e8
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index a7e37bc..3c056f8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -249,6 +249,8 @@
public final DexString runtimeExceptionDescriptor = createString("Ljava/lang/RuntimeException;");
public final DexString assertionErrorDescriptor = createString("Ljava/lang/AssertionError;");
+ public final DexString noSuchElementExceptionDescriptor =
+ createString("Ljava/util/NoSuchElementException;");
public final DexString charSequenceDescriptor = createString("Ljava/lang/CharSequence;");
public final DexString charSequenceArrayDescriptor = createString("[Ljava/lang/CharSequence;");
public final DexString stringDescriptor = createString("Ljava/lang/String;");
@@ -575,6 +577,11 @@
public final DexType runtimeExceptionType = createStaticallyKnownType(runtimeExceptionDescriptor);
public final DexType assertionErrorType = createStaticallyKnownType(assertionErrorDescriptor);
public final DexType throwableType = createStaticallyKnownType(throwableDescriptor);
+ public final DexType noSuchElementExceptionType =
+ createStaticallyKnownType(noSuchElementExceptionDescriptor);
+ public final DexMethod noSuchElementExceptionInit =
+ createInstanceInitializer(noSuchElementExceptionType);
+
public final DexType illegalAccessErrorType =
createStaticallyKnownType(illegalAccessErrorDescriptor);
public final DexType illegalArgumentExceptionType =
@@ -870,6 +877,7 @@
public final ObjectMethodsMembers objectMethodsMembers = new ObjectMethodsMembers();
public final ServiceLoaderMethods serviceLoaderMethods = new ServiceLoaderMethods();
+ public final IteratorMethods iteratorMethods = new IteratorMethods();
public final StringConcatFactoryMembers stringConcatFactoryMembers =
new StringConcatFactoryMembers();
@@ -2659,6 +2667,12 @@
}
}
+ public class IteratorMethods {
+ public final DexMethod hasNext =
+ createMethod(iteratorType, createProto(booleanType), "hasNext");
+ public final DexMethod next = createMethod(iteratorType, createProto(objectType), "next");
+ }
+
private static <T extends DexItem> T canonicalize(Map<T, T> map, T item) {
assert item != null;
assert !DexItemFactory.isInternalSentinel(item);
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 5077746..307acdf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -1109,7 +1109,7 @@
return new IRCodeInstructionIterator(this);
}
- public InstructionListIterator instructionListIterator() {
+ public IRCodeInstructionListIterator instructionListIterator() {
return new IRCodeInstructionListIterator(this);
}
@@ -1241,6 +1241,14 @@
return createNumberConstant(Float.floatToIntBits(value), TypeElement.getFloat(), local);
}
+ public ConstNumber createBooleanConstant(boolean value) {
+ return createBooleanConstant(value, null);
+ }
+
+ public ConstNumber createBooleanConstant(boolean value, DebugLocalInfo local) {
+ return createNumberConstant(value ? 1 : 0, TypeElement.getInt(), local);
+ }
+
public ConstNumber createIntConstant(int value) {
return createIntConstant(value, null);
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 43c3b39..13e393a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -35,6 +35,10 @@
this.instructionIterator = blockIterator.next().listIterator(code);
}
+ public IRCode getCode() {
+ return code;
+ }
+
@Override
public Value insertConstNumberInstruction(
IRCode code, InternalOptions options, long value, TypeElement type) {
@@ -244,11 +248,25 @@
code, blockIterator, instructionsToAdd, options);
}
+ public void addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+ Collection<? extends Instruction> instructionsToAdd, InternalOptions options) {
+ instructionIterator =
+ instructionIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+ code, blockIterator, instructionsToAdd, options);
+ }
+
@Override
public void remove() {
instructionIterator.remove();
}
+ public void removeRemainingInBlockIgnoreOutValue() {
+ while (instructionIterator.hasNext()) {
+ instructionIterator.next();
+ instructionIterator.removeInstructionIgnoreOutValue();
+ }
+ }
+
@Override
public void set(Instruction instruction) {
instructionIterator.set(instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 10d5150..c1e8aa6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -20,6 +20,7 @@
import com.google.common.collect.Sets;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Set;
@@ -69,6 +70,15 @@
code, blockIterator, Arrays.asList(instructionsToAdd), options);
}
+ default InstructionListIterator addPossiblyThrowingInstructionToPossiblyThrowingBlock(
+ IRCode code,
+ BasicBlockIterator blockIterator,
+ Instruction instructionToAdd,
+ InternalOptions options) {
+ return addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+ code, blockIterator, Collections.singleton(instructionToAdd), options);
+ }
+
default void addAndPositionBeforeNewInstruction(Instruction instruction) {
add(instruction);
Instruction previous = previous();
diff --git a/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
index d87fed8..8877b0e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
@@ -24,4 +24,19 @@
}
return null;
}
+
+ /**
+ * Continue to call {@link #next} until it returns the given item.
+ *
+ * @returns item if it was found, null otherwise.
+ */
+ @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
+ default <S extends T> S nextUntil(T item) {
+ while (hasNext()) {
+ if (next() == item) {
+ return (S) item;
+ }
+ }
+ return null;
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 1764c38..d7bf459 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -12,29 +12,37 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory.IteratorMethods;
import com.android.tools.r8.graph.DexItemFactory.ServiceLoaderMethods;
-import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.MethodAccessFlags;
import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.ConstClass;
import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRCodeInstructionListIterator;
import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Throw;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.MethodProcessor;
import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
import com.android.tools.r8.ir.desugar.ServiceLoaderSourceCode;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.ConsumerUtils;
import com.android.tools.r8.utils.ListUtils;
-import com.google.common.collect.ImmutableList;
+import com.android.tools.r8.utils.WorkList;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@@ -70,11 +78,16 @@
private final AndroidApiLevelCompute apiLevelCompute;
private final ServiceLoaderMethods serviceLoaderMethods;
+ private final IteratorMethods iteratorMethods;
+ private final boolean hasAssumeNoSideEffects;
public ServiceLoaderRewriter(AppView<?> appView) {
super(appView);
this.apiLevelCompute = appView.apiLevelCompute();
this.serviceLoaderMethods = appView.dexItemFactory().serviceLoaderMethods;
+ this.iteratorMethods = appView.dexItemFactory().iteratorMethods;
+ hasAssumeNoSideEffects =
+ appView.getAssumeInfoCollection().isSideEffectFree(serviceLoaderMethods.load);
}
@Override
@@ -103,170 +116,396 @@
IRCode code,
MethodProcessor methodProcessor,
MethodProcessingContext methodProcessingContext) {
- InstructionListIterator instructionIterator = code.instructionListIterator();
// Create a map from service type to loader methods local to this context since two
// service loader calls to the same type in different methods and in the same wave can race.
Map<DexType, DexEncodedMethod> synthesizedServiceLoaders = new IdentityHashMap<>();
- while (instructionIterator.hasNext()) {
- Instruction instruction = instructionIterator.next();
+ IRCodeInstructionListIterator iterator = code.instructionListIterator();
+ Map<Instruction, Instruction> replacements = new HashMap<>();
+ Map<Instruction, List<Instruction>> replacementExtras = new HashMap<>();
- // Check if instruction is an invoke static on the desired form of ServiceLoader.load.
- if (!instruction.isInvokeStatic()) {
+ while (iterator.hasNext()) {
+ Instruction instruction = iterator.next();
+
+ // Find ServiceLoader.load() calls.
+ InvokeStatic invokeInstr = instruction.asInvokeStatic();
+ if (invokeInstr == null
+ || !serviceLoaderMethods.isLoadMethod(invokeInstr.getInvokedMethod())) {
continue;
}
- InvokeStatic serviceLoaderLoad = instruction.asInvokeStatic();
- DexMethod invokedMethod = serviceLoaderLoad.getInvokedMethod();
- if (!serviceLoaderMethods.isLoadMethod(invokedMethod)) {
+ // See if it can be optimized.
+ ServiceLoaderLoadResult loadResult = analyzeServiceLoaderLoad(code, invokeInstr);
+ if (loadResult == null) {
continue;
}
+ // See if there is a subsequent iterator() call that can be optimized.
+ DirectRewriteResult directRewriteResult = analyzeForDirectRewrite(loadResult);
- // Check that the first argument is a const class.
- Value argument = serviceLoaderLoad.getFirstArgument().getAliasedValue();
- if (!argument.isDefinedByInstructionSatisfying(Instruction::isConstClass)) {
- report(code.context(), null, "The service loader type could not be determined");
- continue;
+ // Remove ServiceLoader.load()
+ replacements.put(loadResult.loadInvoke, code.createConstNull());
+ // Remove getClassLoader() if we are about to remove all users.
+ if (loadResult.classLoaderInvoke != null
+ && loadResult.classLoaderInvoke.outValue().aliasedUsers().stream()
+ .allMatch(ins -> ins.isAssume() || replacements.containsKey(ins))) {
+ replacements.put(loadResult.classLoaderInvoke, code.createConstNull());
}
-
- ConstClass constClass = argument.getDefinition().asConstClass();
- DexType serviceType = constClass.getType();
- if (invokedMethod.isNotIdenticalTo(serviceLoaderMethods.loadWithClassLoader)) {
- report(
- code.context(),
- serviceType,
- "Inlining is only supported for `java.util.ServiceLoader.load(java.lang.Class,"
- + " java.lang.ClassLoader)`");
- continue;
+ if (directRewriteResult != null) {
+ populateDirectRewriteChanges(
+ code, replacements, replacementExtras, loadResult, directRewriteResult);
+ } else {
+ populateSyntheticChanges(
+ code,
+ methodProcessor,
+ methodProcessingContext,
+ synthesizedServiceLoaders,
+ replacements,
+ loadResult);
}
-
- String invalidUserMessage =
- "The returned ServiceLoader instance must only be used in a call to `java.util.Iterator"
- + " java.lang.ServiceLoader.iterator()`";
- Value serviceLoaderLoadOut = serviceLoaderLoad.outValue();
- if (!serviceLoaderLoadOut.hasSingleUniqueUser() || serviceLoaderLoadOut.hasPhiUsers()) {
- report(code.context(), serviceType, invalidUserMessage);
- continue;
- }
-
- // Check that the only user is a call to iterator().
- InvokeVirtual singleUniqueUser = serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual();
- if (singleUniqueUser == null
- || singleUniqueUser.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.iterator)) {
- report(code.context(), serviceType, invalidUserMessage + ", but found other usages");
- continue;
- }
-
- // Check that the service is not kept.
- if (appView().appInfo().isPinnedWithDefinitionLookup(constClass.getType())) {
- report(code.context(), serviceType, "The service loader type is kept");
- continue;
- }
-
- // Check that ClassLoader used is the ClassLoader defined for the service configuration
- // that we are instantiating or NULL.
- Value classLoaderValue = serviceLoaderLoad.getLastArgument().getAliasedValue();
- if (classLoaderValue.isPhi()) {
- report(
- code.context(),
- serviceType,
- "The java.lang.ClassLoader argument must be defined locally as null or "
- + serviceType
- + ".class.getClassLoader()");
- continue;
- }
- boolean isNullClassLoader = classLoaderValue.getType().isNullType();
- InvokeVirtual classLoaderInvoke = classLoaderValue.getDefinition().asInvokeVirtual();
- boolean isGetClassLoaderOnConstClass =
- classLoaderInvoke != null
- && classLoaderInvoke.arguments().size() == 1
- && classLoaderInvoke.getReceiver().getAliasedValue().isConstClass()
- && classLoaderInvoke
- .getReceiver()
- .getAliasedValue()
- .getDefinition()
- .asConstClass()
- .getType()
- .isIdenticalTo(serviceType);
- if (!isNullClassLoader && !isGetClassLoaderOnConstClass) {
- report(
- code.context(),
- serviceType,
- "The java.lang.ClassLoader argument must be defined locally as null or "
- + serviceType
- + ".class.getClassLoader()");
- continue;
- }
-
- // Check that the service is configured in the META-INF/services.
- AppServices appServices = appView.appServices();
- List<DexType> dexTypes = appServices.serviceImplementationsFor(constClass.getType());
- List<DexClass> classes = new ArrayList<>(dexTypes.size());
- for (DexType serviceImpl : dexTypes) {
- DexClass serviceImplementation = appView.definitionFor(serviceImpl);
- if (serviceImplementation == null) {
- report(
- code.context(),
- serviceType,
- "Unable to find definition for service implementation " + serviceImpl.getTypeName());
- break;
- }
- if (!appView().appInfo().isSubtype(serviceImpl, serviceType)) {
- report(
- code.context(),
- serviceType,
- "Implementation is not a subtype of the service: " + serviceImpl.getTypeName());
- break;
- }
- DexEncodedMethod method = serviceImplementation.getDefaultInitializer();
- if (method == null) {
- report(
- code.context(),
- serviceType,
- "Implementation has no default constructor: " + serviceImpl.getTypeName());
- break;
- }
- if (!method.getAccessFlags().wasPublic()) {
- // A non-public constructor causes a ServiceConfigurationError on APIs 24 & 25 (Nougat).
- // Check the original access flag to not change app behavior if R8 publicized the method.
- report(
- code.context(),
- serviceType,
- "Implementation's default constructor is not public: " + serviceImpl.getTypeName());
- break;
- }
- classes.add(serviceImplementation);
- }
- if (dexTypes.size() != classes.size()) {
- continue;
- }
-
- // Check that we are not service loading anything from a feature into base.
- if (hasServiceImplementationInDifferentFeature(code, serviceType, isNullClassLoader)) {
- continue;
- }
-
- // We can perform the rewrite of the ServiceLoader.load call.
- DexEncodedMethod synthesizedMethod =
- synthesizedServiceLoaders.computeIfAbsent(
- constClass.getType(),
- service -> {
- DexEncodedMethod addedMethod =
- createSynthesizedMethod(
- service, classes, methodProcessor, methodProcessingContext);
- if (appView.options().isGeneratingClassFiles()) {
- addedMethod.upgradeClassFileVersion(
- code.context().getDefinition().getClassFileVersion());
- }
- return addedMethod;
- });
-
- new Rewriter(code, instructionIterator, serviceLoaderLoad)
- .perform(classLoaderInvoke, synthesizedMethod.getReference());
}
+
+ if (replacements.isEmpty()) {
+ return CodeRewriterResult.NO_CHANGE;
+ }
+
+ AffectedValues affectedValues = new AffectedValues();
+ iterator = code.instructionListIterator();
+ while (iterator.hasNext()) {
+ Instruction instruction = iterator.next();
+ Instruction replacement = replacements.get(instruction);
+ if (replacement == null) {
+ continue;
+ }
+ iterator.replaceCurrentInstruction(replacement, affectedValues);
+ List<Instruction> extras = replacementExtras.get(instruction);
+ if (extras != null) {
+ iterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(extras, options);
+ if (ListUtils.last(extras).isThrow()) {
+ iterator.removeRemainingInBlockIgnoreOutValue();
+ }
+ }
+ }
+
+ code.removeUnreachableBlocks(affectedValues, ConsumerUtils.emptyConsumer());
+ code.removeRedundantBlocks();
+ affectedValues.narrowingWithAssumeRemoval(appView, code);
assert code.isConsistentSSA(appView);
- return synthesizedServiceLoaders.isEmpty()
- ? CodeRewriterResult.NO_CHANGE
- : CodeRewriterResult.HAS_CHANGED;
+
+ return CodeRewriterResult.HAS_CHANGED;
+ }
+
+ private void populateSyntheticChanges(
+ IRCode code,
+ MethodProcessor methodProcessor,
+ MethodProcessingContext methodProcessingContext,
+ Map<DexType, DexEncodedMethod> synthesizedServiceLoaders,
+ Map<Instruction, Instruction> replacements,
+ ServiceLoaderLoadResult loadResult) {
+ DexEncodedMethod synthesizedMethod =
+ synthesizedServiceLoaders.computeIfAbsent(
+ loadResult.serviceType,
+ service -> {
+ DexEncodedMethod addedMethod =
+ createSynthesizedMethod(
+ service, loadResult.implClasses, methodProcessor, methodProcessingContext);
+ if (appView.options().isGeneratingClassFiles()) {
+ addedMethod.upgradeClassFileVersion(
+ code.context().getDefinition().getClassFileVersion());
+ }
+ return addedMethod;
+ });
+ InvokeStatic synthesizedInvoke =
+ new InvokeStatic(
+ synthesizedMethod.getReference(),
+ loadResult.iteratorInvoke.outValue(),
+ Collections.emptyList());
+ replacements.put(loadResult.iteratorInvoke, synthesizedInvoke);
+ }
+
+ private void populateDirectRewriteChanges(
+ IRCode code,
+ Map<Instruction, Instruction> replacements,
+ Map<Instruction, List<Instruction>> replacementExtras,
+ ServiceLoaderLoadResult loadResult,
+ DirectRewriteResult directRewriteResult) {
+ // Remove ServiceLoader.iterator()
+ replacements.put(loadResult.iteratorInvoke, code.createConstNull());
+
+ // Iterator.hasNext() --> true / false
+ if (directRewriteResult.hasNextInstr != null) {
+ replacements.put(
+ directRewriteResult.hasNextInstr,
+ code.createBooleanConstant(!loadResult.implClasses.isEmpty()));
+ }
+ if (directRewriteResult.nextInstr != null) {
+ Position position = directRewriteResult.nextInstr.getPosition();
+ if (loadResult.implClasses.isEmpty()) {
+ // Iterator.next() -> null
+ replacements.put(directRewriteResult.nextInstr, code.createConstNull());
+
+ // throw new NoSuchElementException()
+ NewInstance newInstanceInstr =
+ new NewInstance(
+ dexItemFactory.noSuchElementExceptionType,
+ code.createValue(
+ dexItemFactory.noSuchElementExceptionType.toNonNullTypeElement(appView)));
+ InvokeDirect initInstr =
+ new InvokeDirect(
+ dexItemFactory.noSuchElementExceptionInit,
+ null,
+ List.of(newInstanceInstr.outValue()));
+ Throw throwInstr = new Throw(newInstanceInstr.outValue());
+
+ newInstanceInstr.setPosition(position);
+ initInstr.setPosition(position);
+ throwInstr.setPosition(position);
+ replacementExtras.put(
+ directRewriteResult.nextInstr, List.of(newInstanceInstr, initInstr, throwInstr));
+ } else {
+ // Iterator.next() -> new ServiceImpl()
+ DexType clazz = loadResult.implClasses.get(0).getType();
+ NewInstance newInstance =
+ new NewInstance(
+ clazz,
+ code.createValue(
+ clazz.toNonNullTypeElement(appView),
+ directRewriteResult.nextInstr.getLocalInfo()));
+ replacements.put(directRewriteResult.nextInstr, newInstance);
+ InvokeDirect initInstr =
+ new InvokeDirect(
+ dexItemFactory.createInstanceInitializer(clazz),
+ null,
+ List.of(newInstance.outValue()));
+ initInstr.setPosition(position);
+ replacementExtras.put(directRewriteResult.nextInstr, List.of(initInstr));
+ }
+ }
+ }
+
+ private ServiceLoaderLoadResult analyzeServiceLoaderLoad(IRCode code, InvokeStatic invokeInstr) {
+ // Check that the first argument is a const class.
+ Value argument = invokeInstr.getFirstArgument().getAliasedValue();
+ if (!argument.isDefinedByInstructionSatisfying(Instruction::isConstClass)) {
+ report(code.context(), null, "The service loader type could not be determined");
+ return null;
+ }
+
+ ConstClass constClass = argument.getDefinition().asConstClass();
+ DexType serviceType = constClass.getType();
+ if (invokeInstr.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.loadWithClassLoader)) {
+ report(
+ code.context(),
+ serviceType,
+ "Inlining is only supported for `java.util.ServiceLoader.load(java.lang.Class,"
+ + " java.lang.ClassLoader)`");
+ return null;
+ }
+
+ String invalidUserMessage =
+ "The returned ServiceLoader instance must only be used in a call to `java.util.Iterator"
+ + " java.lang.ServiceLoader.iterator()`";
+ Value serviceLoaderLoadOut = invokeInstr.outValue();
+ if (!serviceLoaderLoadOut.hasSingleUniqueUser() || serviceLoaderLoadOut.hasPhiUsers()) {
+ report(code.context(), serviceType, invalidUserMessage);
+ return null;
+ }
+
+ // Check that the only user is a call to iterator().
+ InvokeVirtual iteratorInvoke = serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual();
+ if (iteratorInvoke == null
+ || iteratorInvoke.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.iterator)) {
+ report(code.context(), serviceType, invalidUserMessage + ", but found other usages");
+ return null;
+ }
+
+ // Check that the service is not kept.
+ if (appView().appInfo().isPinnedWithDefinitionLookup(serviceType)) {
+ report(code.context(), serviceType, "The service loader type is kept");
+ return null;
+ }
+
+ // Check that ClassLoader used is the ClassLoader defined for the service configuration
+ // that we are instantiating or NULL.
+ Value classLoaderValue = invokeInstr.getLastArgument().getAliasedValue();
+ if (classLoaderValue.isPhi()) {
+ report(
+ code.context(),
+ serviceType,
+ "The java.lang.ClassLoader argument must be defined locally as null or "
+ + serviceType
+ + ".class.getClassLoader()");
+ return null;
+ }
+ boolean isNullClassLoader = classLoaderValue.getType().isNullType();
+ InvokeVirtual classLoaderInvoke = classLoaderValue.getDefinition().asInvokeVirtual();
+ boolean isGetClassLoaderOnConstClass =
+ classLoaderInvoke != null
+ && classLoaderInvoke.arguments().size() == 1
+ && classLoaderInvoke.getReceiver().getAliasedValue().isConstClass()
+ && classLoaderInvoke
+ .getReceiver()
+ .getAliasedValue()
+ .getDefinition()
+ .asConstClass()
+ .getType()
+ .isIdenticalTo(serviceType);
+ if (!isNullClassLoader && !isGetClassLoaderOnConstClass) {
+ report(
+ code.context(),
+ serviceType,
+ "The java.lang.ClassLoader argument must be defined locally as null or "
+ + serviceType
+ + ".class.getClassLoader()");
+ return null;
+ }
+
+ // Check that we are not service loading anything from a feature into base.
+ AppServices appServices = appView.appServices();
+ // Check that we are not service loading anything from a feature into base.
+ if (hasServiceImplementationInDifferentFeature(code, serviceType, isNullClassLoader)) {
+ return null;
+ }
+
+ List<DexType> dexTypes = appServices.serviceImplementationsFor(serviceType);
+ List<DexClass> implClasses = new ArrayList<>(dexTypes.size());
+ for (DexType serviceImpl : dexTypes) {
+ DexClass serviceImplementation = appView.definitionFor(serviceImpl);
+ if (serviceImplementation == null) {
+ report(
+ code.context(),
+ serviceType,
+ "Unable to find definition for service implementation " + serviceImpl.getTypeName());
+ return null;
+ }
+ if (appView.isSubtype(serviceImpl, serviceType).isFalse()) {
+ report(
+ code.context(),
+ serviceType,
+ "Implementation is not a subtype of the service: " + serviceImpl.getTypeName());
+ return null;
+ }
+ DexEncodedMethod method = serviceImplementation.getDefaultInitializer();
+ if (method == null) {
+ report(
+ code.context(),
+ serviceType,
+ "Implementation has no default constructor: " + serviceImpl.getTypeName());
+ return null;
+ }
+ if (!method.getAccessFlags().wasPublic()) {
+ // A non-public constructor causes a ServiceConfigurationError on APIs 24 & 25 (Nougat).
+ report(
+ code.context(),
+ serviceType,
+ "Implementation's default constructor is not public: " + serviceImpl.getTypeName());
+ return null;
+ }
+ implClasses.add(serviceImplementation);
+ }
+
+ return new ServiceLoaderLoadResult(
+ invokeInstr, classLoaderInvoke, serviceType, implClasses, iteratorInvoke);
+ }
+
+ /**
+ * Checks that:
+ *
+ * <pre>
+ * * -assumenosideeffects for ServiceLoader.load() is set.
+ * * ServiceLoader.iterator() is used only by a single .hasNext() and/or .next()
+ * * .iterator(), .hasNext(), and .next() are all in the same try/catch.
+ * * .hasNext() never comes after a call to .next()
+ * * .hasNext() and .next() are not in a loop.
+ * </pre>
+ */
+ private DirectRewriteResult analyzeForDirectRewrite(ServiceLoaderLoadResult loadResult) {
+ // Require -assumenosideeffects class java.util.ServiceLoader { java.lang.Object load(...); }
+ // because this direct rewriting does not wrap exceptions in ServiceConfigurationError.
+ if (!hasAssumeNoSideEffects) {
+ return null;
+ }
+ InvokeVirtual iteratorInvoke = loadResult.iteratorInvoke;
+
+ if (iteratorInvoke.outValue().hasPhiUsers()) {
+ return null;
+ }
+ InvokeMethod hasNextInstr = null;
+ InvokeMethod nextInstr = null;
+ // We only bother to support a single call to hasNext() and next(), and they must appear within
+ // the same try/catch.
+ for (Instruction user : iteratorInvoke.outValue().aliasedUsers()) {
+ if (user.isAssume()) {
+ if (user.outValue().hasPhiUsers()) {
+ return null;
+ }
+ continue;
+ }
+ if (!user.getBlock().hasEquivalentCatchHandlers(iteratorInvoke.getBlock())) {
+ return null;
+ }
+ InvokeMethod curCall = user.asInvokeMethod();
+ if (curCall == null) {
+ return null;
+ }
+ if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.hasNext)) {
+ if (hasNextInstr != null) {
+ return null;
+ }
+ hasNextInstr = curCall;
+ } else if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.next)) {
+ if (nextInstr != null) {
+ return null;
+ }
+ nextInstr = curCall;
+ } else {
+ return null;
+ }
+ }
+
+ BasicBlock iteratorBlock = iteratorInvoke.getBlock();
+ BasicBlock hasNextBlock = hasNextInstr != null ? hasNextInstr.getBlock() : null;
+ BasicBlock nextBlock = nextInstr != null ? nextInstr.getBlock() : null;
+ if (hasNextBlock != null && nextBlock != null) {
+ // See if hasNext() is reachable after next().
+ if (hasNextBlock == nextBlock) {
+ if (nextBlock.iterator(nextInstr).nextUntil(hasNextInstr) != null) {
+ return null;
+ }
+ } else if (hasPredecessorPathTo(iteratorBlock, hasNextBlock, nextBlock)) {
+ return null;
+ }
+ }
+
+ // Make sure each instruction can be run at most once (no loops).
+ if (hasNextBlock != null && loopExists(iteratorBlock, hasNextBlock)) {
+ return null;
+ }
+ if (nextBlock != null && loopExists(iteratorBlock, nextBlock)) {
+ return null;
+ }
+
+ return new DirectRewriteResult(hasNextInstr, nextInstr);
+ }
+
+ private static boolean loopExists(BasicBlock subgraphEntryBlock, BasicBlock targetBlock) {
+ return hasPredecessorPathTo(subgraphEntryBlock, targetBlock, targetBlock);
+ }
+
+ private static boolean hasPredecessorPathTo(
+ BasicBlock subgraphEntryBlock, BasicBlock subgraphExitBlock, BasicBlock targetBlock) {
+ if (subgraphEntryBlock == subgraphExitBlock) {
+ return false;
+ }
+ WorkList<BasicBlock> workList = WorkList.newIdentityWorkList();
+ workList.markAsSeen(subgraphEntryBlock);
+ workList.addIfNotSeen(subgraphExitBlock.getPredecessors());
+ while (workList.hasNext()) {
+ BasicBlock curBlock = workList.next();
+ if (curBlock == targetBlock) {
+ return true;
+ }
+ workList.addIfNotSeen(curBlock.getPredecessors());
+ }
+ return false;
}
private void report(ProgramMethod method, DexType serviceLoaderType, String message) {
@@ -375,77 +614,34 @@
return method.getDefinition();
}
- /**
- * Rewriter assumes that the code is of the form:
- *
- * <pre>
- * ConstClass v1 <- X
- * ConstClass v2 <- X or NULL
- * Invoke-Virtual v3 <- v2; method: java.lang.ClassLoader java.lang.Class.getClassLoader()
- * Invoke-Static v4 <- v1, v3; method: java.util.ServiceLoader java.util.ServiceLoader
- * .load(java.lang.Class, java.lang.ClassLoader)
- * Invoke-Virtual v5 <- v4; method: java.util.Iterator java.util.ServiceLoader.iterator()
- * </pre>
- *
- * and rewrites it to:
- *
- * <pre>
- * Invoke-Static v5 <- ; method: java.util.Iterator syn(X)()
- * </pre>
- *
- * where syn(X) is the synthesized method generated for the service class.
- *
- * <p>We rely on the DeadCodeRemover to remove the ConstClasses and any aliased values no longer
- * used.
- */
- private static class Rewriter {
+ private static class ServiceLoaderLoadResult {
+ public final InvokeStatic loadInvoke;
+ public final InvokeVirtual classLoaderInvoke;
+ public final DexType serviceType;
+ public final List<DexClass> implClasses;
+ public final InvokeVirtual iteratorInvoke;
- private final IRCode code;
- private final InvokeStatic serviceLoaderLoad;
-
- private final InstructionListIterator iterator;
-
- Rewriter(IRCode code, InstructionListIterator iterator, InvokeStatic serviceLoaderLoad) {
- this.iterator = iterator;
- this.code = code;
- this.serviceLoaderLoad = serviceLoaderLoad;
- }
-
- public void perform(InvokeVirtual classLoaderInvoke, DexMethod method) {
- // Remove the ClassLoader call since this can throw and will not be removed otherwise.
- if (classLoaderInvoke != null) {
- BooleanBox allClassLoaderUsersAreServiceLoaders =
- new BooleanBox(!classLoaderInvoke.outValue().hasPhiUsers());
- classLoaderInvoke
- .outValue()
- .aliasedUsers()
- .forEach(user -> allClassLoaderUsersAreServiceLoaders.and(user == serviceLoaderLoad));
- if (allClassLoaderUsersAreServiceLoaders.get()) {
- clearGetClassLoader(classLoaderInvoke);
- iterator.nextUntil(i -> i == serviceLoaderLoad);
- }
+ public ServiceLoaderLoadResult(
+ InvokeStatic loadInvoke,
+ InvokeVirtual classLoaderInvoke,
+ DexType serviceType,
+ List<DexClass> implClasses,
+ InvokeVirtual iteratorInvoke) {
+ this.loadInvoke = loadInvoke;
+ this.classLoaderInvoke = classLoaderInvoke;
+ this.serviceType = serviceType;
+ this.implClasses = implClasses;
+ this.iteratorInvoke = iteratorInvoke;
}
+ }
- // Remove the ServiceLoader.load call.
- InvokeVirtual serviceLoaderIterator =
- serviceLoaderLoad.outValue().singleUniqueUser().asInvokeVirtual();
- iterator.replaceCurrentInstruction(code.createConstNull());
+ private static class DirectRewriteResult {
+ public final InvokeMethod nextInstr;
+ public final InvokeMethod hasNextInstr;
- // Find the iterator instruction and replace it.
- iterator.nextUntil(x -> x == serviceLoaderIterator);
- InvokeStatic synthesizedInvoke =
- new InvokeStatic(method, serviceLoaderIterator.outValue(), ImmutableList.of());
- iterator.replaceCurrentInstruction(synthesizedInvoke);
- }
-
- private void clearGetClassLoader(InvokeVirtual classLoaderInvoke) {
- while (iterator.hasPrevious()) {
- Instruction instruction = iterator.previous();
- if (instruction == classLoaderInvoke) {
- iterator.replaceCurrentInstruction(code.createConstNull());
- break;
- }
- }
+ public DirectRewriteResult(InvokeMethod hasNextInstr, InvokeMethod nextInstr) {
+ this.hasNextInstr = hasNextInstr;
+ this.nextInstr = nextInstr;
}
}
}
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
index 111badc..5cd038d 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
@@ -76,10 +76,10 @@
.assertSuccessWithOutput(EXPECTED_OUTPUT)
.inspect(
inspector -> {
- verifyNoServiceLoaderLoads(inspector);
+ verifyNoServiceLoaderLoads(inspector.clazz(MainRunner.class));
verifyNoClassLoaders(inspector);
verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
- // Check the synthesize service loader method is a single shared method.
+ // Check the synthesized service loader method is a single shared method.
// Due to minification we just check there is only a single synthetic class with a
// single static method.
boolean found = false;
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
new file mode 100644
index 0000000..c57f579
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
@@ -0,0 +1,278 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.serviceloader;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderRewritingWithAssumeNoSideEffectsTest extends ServiceLoaderTestBase {
+ private final String EXPECTED_OUTPUT = StringUtils.lines("Hello World!");
+
+ public interface Service {
+
+ void print();
+ }
+
+ public static class ServiceImpl implements Service {
+
+ @Override
+ public void print() {
+ System.out.println("Hello World!");
+ }
+ }
+
+ public static class ServiceImpl2 implements Service {
+
+ @Override
+ public void print() {
+ System.out.println("Hello World 2!");
+ }
+ }
+
+ public static class MainRunner {
+ public static void main(String[] args) {
+ ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator().next().print();
+ }
+ }
+
+ public static class HasNextRunner {
+ public static void main(String[] args) {
+ Iterator<Service> iterator =
+ ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+ if (iterator.hasNext()) {
+ iterator.next().print();
+ }
+ }
+ }
+
+ public static class MultipleCallsRunner {
+ public static void main(String[] args) {
+ Iterator<Service> iterator =
+ ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+ if (iterator.hasNext() && iterator.hasNext()) {
+ iterator.next().print();
+ }
+ }
+ }
+
+ public static class HasNextAfterNextRunner {
+ public static void main(String[] args) {
+ Iterator<Service> iterator =
+ ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+
+ iterator.next().print();
+ if (iterator.hasNext()) {
+ System.out.println("not reached");
+ }
+ }
+ }
+
+ public static class HasNextAfterNextWithTryCatch {
+ public static void main(String[] args) {
+ try {
+ Iterator<Service> iterator =
+ ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+
+ iterator.next().print();
+ if (iterator.hasNext()) {
+ System.out.println("not reached");
+ }
+ } catch (Throwable t) {
+ System.out.println("unreachable");
+ }
+ }
+ }
+
+ public static class LoopingRunner {
+ public static void main(String[] args) {
+ Iterator<Service> iterator =
+ ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+ // Loop without a call to hasNext().
+ for (int i = System.currentTimeMillis() > 0 ? 0 : 1; i < 1; ++i) {
+ iterator.next().print();
+ }
+ }
+ }
+
+ public static class PhiRunner {
+ public static void main(String[] args) {
+ Iterator<Service> iterator =
+ System.currentTimeMillis() > 0
+ ? ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator()
+ : null;
+ iterator.next().print();
+ }
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ServiceLoaderRewritingWithAssumeNoSideEffectsTest(TestParameters parameters) {
+ super(parameters);
+ }
+
+ private static void assertIteratorPresent(CodeInspector inspector, boolean expected) {
+ assertEquals(0, getServiceLoaderLoads(inspector));
+
+ boolean hasIteratorCall =
+ inspector
+ .streamInstructions()
+ .anyMatch(ins -> ins.isInvoke() && ins.getMethod().name.toString().equals("iterator"));
+ assertEquals(expected, hasIteratorCall);
+ }
+
+ private R8FullTestBuilder doTest(Class<?>... implClasses) throws IOException {
+ return serviceLoaderTest(Service.class, implClasses)
+ .addKeepRules("-assumenosideeffects class java.util.ServiceLoader { *** load(...); }");
+ }
+
+ @Test
+ public void testRewritingWithNoImplsHasNext()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest()
+ .addKeepMainRule(HasNextRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), HasNextRunner.class)
+ .assertSuccessWithOutput("")
+ .inspect(inspector -> assertIteratorPresent(inspector, false));
+ }
+
+ @Test
+ public void testRewritingWithMultiple()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class, ServiceImpl2.class)
+ .addKeepMainRule(MainRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), MainRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, false);
+ // ServiceImpl2 gets removed since next() is called only once.
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+
+ @Test
+ public void testRewritingWithHasNext()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class)
+ .addKeepMainRule(HasNextRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), HasNextRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, false);
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+
+ @Test
+ public void testDoNotRewriteMultipleCalls()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class)
+ .addKeepMainRule(MultipleCallsRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), MultipleCallsRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, true);
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+
+ @Test
+ public void testDoNotRewriteHasNextAfterNext()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class)
+ .addKeepMainRule(HasNextAfterNextRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), HasNextAfterNextRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, true);
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+
+ @Test
+ public void testDoNotRewriteHasNextAfterNextWithTryCatch()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class)
+ .addKeepMainRule(HasNextAfterNextWithTryCatch.class)
+ .compile()
+ .run(parameters.getRuntime(), HasNextAfterNextWithTryCatch.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, true);
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+
+ @Test
+ public void testDoNotRewriteHasNextAfterNextBlocks()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class)
+ .addKeepMainRule(HasNextAfterNextRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), HasNextAfterNextRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, true);
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+
+ @Test
+ public void testDoNotRewriteLoop()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class)
+ .addKeepMainRule(LoopingRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), LoopingRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, true);
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+
+ @Test
+ public void testDoNotRewritePhiUser()
+ throws IOException, CompilationFailedException, ExecutionException {
+ doTest(ServiceImpl.class)
+ .addKeepMainRule(PhiRunner.class)
+ .compile()
+ .run(parameters.getRuntime(), PhiRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(
+ inspector -> {
+ assertIteratorPresent(inspector, true);
+ verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+ });
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
index edb1c4e..118a3fc 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
@@ -67,10 +67,6 @@
method -> assertThat(method, not(invokesMethodWithName("getClassLoader"))));
}
- public static void verifyNoServiceLoaderLoads(CodeInspector inspector) {
- inspector.allClasses().forEach(ServiceLoaderTestBase::verifyNoServiceLoaderLoads);
- }
-
public static void verifyNoServiceLoaderLoads(ClassSubject classSubject) {
assertTrue(classSubject.isPresent());
classSubject.forAllMethods(method -> assertThat(method, not(invokesMethodWithName("load"))));