Reland "Build apimodel cache on top of computed database builder"
This reverts commit 4e0cd0f7dc5ef7c59b328c28463693745e449e53.
Change-Id: Iaebb47b46059e98213c056618e6e24d22efedb2c
diff --git a/build.gradle b/build.gradle
index 7f0d08f..d9641a3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -538,6 +538,8 @@
}
}
+compileJava.dependsOn downloadDeps
+
sourceSets.configureEach { sourceSet ->
tasks.named(sourceSet.compileJavaTaskName).configure {
// Default disable errorprone (enabled and setup below).
@@ -727,6 +729,7 @@
}
task repackageDepsNew(type: ShadowJar) {
+ dependsOn downloadDeps
configurations = [project.configurations.runtimeClasspath]
mergeServiceFiles(it)
exclude { it.getRelativePath().getPathString().endsWith("module-info.class") }
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiClass.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiClass.java
index f33f5bd..ee0ffb2 100644
--- a/src/main/java/com/android/tools/r8/androidapi/AndroidApiClass.java
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiClass.java
@@ -26,6 +26,8 @@
public abstract AndroidApiLevel getApiLevel();
+ public abstract int getMemberCount();
+
public abstract TraversalContinuation visitFields(
BiFunction<FieldReference, AndroidApiLevel, TraversalContinuation> visitor);
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiReferenceLevelCache.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiReferenceLevelCache.java
new file mode 100644
index 0000000..124ea3f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiReferenceLevelCache.java
@@ -0,0 +1,150 @@
+// Copyright (c) 2021, 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.androidapi;
+
+import com.android.tools.r8.apimodel.AndroidApiDatabaseBuilder;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMember;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.TraversalContinuation;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+public class AndroidApiReferenceLevelCache {
+
+ private static final int BUILD_CACHE_TRESHOLD = 20;
+
+ private final Map<DexType, AndroidApiClass> apiTypeLookup;
+ private final Map<DexReference, AndroidApiLevel> apiMemberLookup = new IdentityHashMap<>();
+ private final AppView<?> appView;
+
+ private AndroidApiReferenceLevelCache(AppView<?> appView) {
+ this.appView = appView;
+ this.apiTypeLookup = new IdentityHashMap<>();
+ }
+
+ private AndroidApiReferenceLevelCache(
+ AppView<?> appView, Map<DexType, AndroidApiClass> apiTypeLookup) {
+ this.appView = appView;
+ this.apiTypeLookup = apiTypeLookup;
+ }
+
+ public static AndroidApiReferenceLevelCache create(AppView<?> appView) {
+ if (!appView.options().apiModelingOptions().enableApiCallerIdentification) {
+ // If enableApiCallerIdentification is not enabled then override lookup to always return
+ // AndroidApiLevel.B.
+ return new AndroidApiReferenceLevelCache(appView) {
+ @Override
+ public AndroidApiLevel lookup(DexReference reference) {
+ return AndroidApiLevel.B;
+ }
+ };
+ }
+ // The apiTypeLookup is build lazily except for the mocked api types that we define in tests
+ // externally.
+ Map<DexType, AndroidApiClass> apiTypeLookup = new IdentityHashMap<>();
+ appView
+ .options()
+ .apiModelingOptions()
+ .visitMockedApiReferences(
+ (classReference, androidApiClass) ->
+ apiTypeLookup.put(
+ appView.dexItemFactory().createType(classReference.getDescriptor()),
+ androidApiClass));
+ return new AndroidApiReferenceLevelCache(appView, apiTypeLookup);
+ }
+
+ public AndroidApiLevel lookupMax(DexReference reference, AndroidApiLevel minApiLevel) {
+ return lookup(reference).max(minApiLevel);
+ }
+
+ public AndroidApiLevel lookup(DexReference reference) {
+ DexType contextType = reference.getContextType();
+ assert !contextType.isArrayType();
+ if (contextType.isPrimitiveType() || contextType.isVoidType()) {
+ return AndroidApiLevel.B;
+ }
+ DexClass clazz = appView.definitionFor(contextType);
+ if (clazz == null) {
+ return AndroidApiLevel.UNKNOWN;
+ }
+ if (!clazz.isLibraryClass()) {
+ return appView.options().minApiLevel;
+ }
+ AndroidApiClass androidApiClass =
+ apiTypeLookup.computeIfAbsent(
+ contextType, type -> AndroidApiDatabaseBuilder.buildClass(type.asClassReference()));
+ if (androidApiClass == null) {
+ // This is a library class but we have no api model for it. This happens if using an older
+ // version of R8 to compile a new target. We simply have to disallow inlining of methods
+ // that has such references.
+ return AndroidApiLevel.UNKNOWN;
+ }
+ if (reference.isDexType()) {
+ return androidApiClass.getApiLevel();
+ }
+ return androidApiClass.getMemberCount() > BUILD_CACHE_TRESHOLD
+ ? findMemberByCaching(reference, androidApiClass)
+ : findMemberByIteration(reference.asDexMember(), androidApiClass);
+ }
+
+ private AndroidApiLevel findMemberByIteration(
+ DexMember<?, ?> reference, AndroidApiClass apiClass) {
+ DexItemFactory factory = appView.dexItemFactory();
+ // Similar to the case for api classes we are unable to find, if the member
+ // is unknown we have to be conservative.
+ Box<AndroidApiLevel> apiLevelBox = new Box<>(AndroidApiLevel.UNKNOWN);
+ reference.apply(
+ field ->
+ apiClass.visitFields(
+ (fieldReference, apiLevel) -> {
+ if (factory.createField(fieldReference) == field) {
+ apiLevelBox.set(apiLevel);
+ return TraversalContinuation.BREAK;
+ }
+ return TraversalContinuation.CONTINUE;
+ }),
+ method ->
+ apiClass.visitMethods(
+ (methodReference, apiLevel) -> {
+ if (factory.createMethod(methodReference) == method) {
+ apiLevelBox.set(apiLevel);
+ return TraversalContinuation.BREAK;
+ }
+ return TraversalContinuation.CONTINUE;
+ }));
+ return apiLevelBox.get();
+ }
+
+ private AndroidApiLevel findMemberByCaching(DexReference reference, AndroidApiClass apiClass) {
+ buildCacheForMembers(reference.getContextType(), apiClass);
+ return apiMemberLookup.getOrDefault(reference, AndroidApiLevel.UNKNOWN);
+ }
+
+ private void buildCacheForMembers(DexType context, AndroidApiClass apiClass) {
+ assert apiClass.getMemberCount() > BUILD_CACHE_TRESHOLD;
+ // Use the context type as a token for us having build a cache for it.
+ if (apiMemberLookup.containsKey(context)) {
+ return;
+ }
+ DexItemFactory factory = appView.dexItemFactory();
+ apiClass.visitFields(
+ (fieldReference, apiLevel) -> {
+ apiMemberLookup.put(factory.createField(fieldReference), apiLevel);
+ return TraversalContinuation.CONTINUE;
+ });
+ apiClass.visitMethods(
+ (methodReference, apiLevel) -> {
+ apiMemberLookup.put(factory.createMethod(methodReference), apiLevel);
+ return TraversalContinuation.CONTINUE;
+ });
+ apiMemberLookup.put(context, AndroidApiLevel.UNKNOWN);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/DexMember.java b/src/main/java/com/android/tools/r8/graph/DexMember.java
index ae9d9fb..bffc842 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMember.java
@@ -5,7 +5,7 @@
import com.android.tools.r8.utils.AndroidApiLevel;
import com.google.common.collect.Iterables;
-import java.util.Map;
+import java.util.function.BiFunction;
import java.util.function.Function;
public abstract class DexMember<D extends DexEncodedMember<D, R>, R extends DexMember<D, R>>
@@ -64,12 +64,11 @@
}
public AndroidApiLevel computeApiLevelForReferencedTypes(
- AppView<?> appView, Map<DexReference, AndroidApiLevel> apiLevelMap) {
- AndroidApiLevel minApiLevel = appView.options().minApiLevel;
- AndroidApiLevel apiLevel = minApiLevel;
+ AppView<?> appView, BiFunction<DexReference, AndroidApiLevel, AndroidApiLevel> computeMax) {
+ AndroidApiLevel computedLevel = appView.options().minApiLevel;
for (DexType type : getReferencedBaseTypes(appView.dexItemFactory())) {
- apiLevel = apiLevel.max(apiLevelMap.getOrDefault(type, minApiLevel));
+ computedLevel = computeMax.apply(type, computedLevel);
}
- return apiLevel;
+ return computedLevel;
}
}
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java
index f93437e..62ed777 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ApiModelAnalysis.java
@@ -4,10 +4,10 @@
package com.android.tools.r8.graph.analysis;
+import com.android.tools.r8.androidapi.AndroidApiReferenceLevelCache;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexEncodedMember;
import com.android.tools.r8.graph.DexMember;
-import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.ProgramDefinition;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
@@ -16,19 +16,17 @@
import com.android.tools.r8.ir.optimize.info.MemberOptimizationInfo;
import com.android.tools.r8.shaking.DefaultEnqueuerUseRegistry;
import com.android.tools.r8.utils.AndroidApiLevel;
-import java.util.Map;
public class ApiModelAnalysis extends EnqueuerAnalysis {
private final AppView<?> appView;
private final AndroidApiLevel minApiLevel;
- private final Map<DexReference, AndroidApiLevel> referenceToApiLevelMap;
+ private final AndroidApiReferenceLevelCache referenceLevelCache;
- public ApiModelAnalysis(
- AppView<?> appView, Map<DexReference, AndroidApiLevel> referenceToApiLevelMap) {
+ public ApiModelAnalysis(AppView<?> appView, AndroidApiReferenceLevelCache referenceLevelCache) {
this.appView = appView;
this.minApiLevel = appView.options().minApiLevel;
- this.referenceToApiLevelMap = referenceToApiLevelMap;
+ this.referenceLevelCache = referenceLevelCache;
}
@Override
@@ -46,6 +44,13 @@
@Override
public void processTracedCode(ProgramMethod method, DefaultEnqueuerUseRegistry registry) {
assert registry.getMaxApiReferenceLevel().isGreaterThanOrEqualTo(minApiLevel);
+ if (appView.options().apiModelingOptions().tracedMethodApiLevelCallback != null) {
+ appView
+ .options()
+ .apiModelingOptions()
+ .tracedMethodApiLevelCallback
+ .accept(method.getMethodReference(), registry.getMaxApiReferenceLevel());
+ }
setApiLevelForMember(method.getDefinition(), registry.getMaxApiReferenceLevel());
}
@@ -77,6 +82,6 @@
}
private AndroidApiLevel computeApiLevelForReferencedTypes(DexMember<?, ?> member) {
- return member.computeApiLevelForReferencedTypes(appView, referenceToApiLevelMap);
+ return member.computeApiLevelForReferencedTypes(appView, referenceLevelCache::lookupMax);
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
index f8aae86..ecd958d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
@@ -20,7 +20,7 @@
import com.android.tools.r8.shaking.EnqueuerUseRegistryFactory;
import com.android.tools.r8.utils.AndroidApiLevel;
import java.util.ListIterator;
-import java.util.Map;
+import java.util.function.BiFunction;
public class ProtoEnqueuerUseRegistry extends DefaultEnqueuerUseRegistry {
@@ -32,7 +32,7 @@
AppView<? extends AppInfoWithClassHierarchy> appView,
ProgramMethod currentMethod,
Enqueuer enqueuer,
- Map<DexReference, AndroidApiLevel> apiLevelReferenceMap) {
+ BiFunction<DexReference, AndroidApiLevel, AndroidApiLevel> apiLevelReferenceMap) {
super(appView, currentMethod, enqueuer, apiLevelReferenceMap);
this.references = appView.protoShrinker().references;
}
diff --git a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
index da86fe5..8a3a7e3 100644
--- a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
@@ -19,21 +19,21 @@
import com.android.tools.r8.graph.UseRegistry;
import com.android.tools.r8.utils.AndroidApiLevel;
import java.util.ListIterator;
-import java.util.Map;
+import java.util.function.BiFunction;
public class DefaultEnqueuerUseRegistry extends UseRegistry {
protected final AppView<? extends AppInfoWithClassHierarchy> appView;
private final ProgramMethod context;
protected final Enqueuer enqueuer;
- private final Map<DexReference, AndroidApiLevel> apiReferenceMapping;
+ private final BiFunction<DexReference, AndroidApiLevel, AndroidApiLevel> apiReferenceMapping;
private AndroidApiLevel maxApiReferenceLevel;
public DefaultEnqueuerUseRegistry(
AppView<? extends AppInfoWithClassHierarchy> appView,
ProgramMethod context,
Enqueuer enqueuer,
- Map<DexReference, AndroidApiLevel> apiReferenceMapping) {
+ BiFunction<DexReference, AndroidApiLevel, AndroidApiLevel> apiReferenceMapping) {
super(appView.dexItemFactory());
this.appView = appView;
this.context = context;
@@ -188,14 +188,13 @@
private void setMaxApiReferenceLevel(DexReference reference) {
if (reference.isDexMember()) {
- this.maxApiReferenceLevel =
+ maxApiReferenceLevel =
maxApiReferenceLevel.max(
reference
.asDexMember()
.computeApiLevelForReferencedTypes(appView, apiReferenceMapping));
}
- this.maxApiReferenceLevel =
- maxApiReferenceLevel.max(apiReferenceMapping.getOrDefault(reference, maxApiReferenceLevel));
+ maxApiReferenceLevel = apiReferenceMapping.apply(reference, maxApiReferenceLevel);
}
public AndroidApiLevel getMaxApiReferenceLevel() {
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index abb6523..ea9b2d7 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -15,6 +15,7 @@
import static java.util.Collections.emptySet;
import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.androidapi.AndroidApiReferenceLevelCache;
import com.android.tools.r8.cf.code.CfInstruction;
import com.android.tools.r8.cf.code.CfInvoke;
import com.android.tools.r8.code.CfOrDexInstruction;
@@ -120,7 +121,6 @@
import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult;
import com.android.tools.r8.synthesis.SyntheticItems.SynthesizingContextOracle;
import com.android.tools.r8.utils.Action;
-import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.IteratorUtils;
import com.android.tools.r8.utils.ListUtils;
@@ -255,7 +255,7 @@
private final Set<DexReference> identifierNameStrings = Sets.newIdentityHashSet();
- private final Map<DexReference, AndroidApiLevel> referenceToApiLevelMap;
+ private final AndroidApiReferenceLevelCache apiReferenceLevelCache;
/**
* Tracks the dependency between a method and the super-method it calls, if any. Used to make
@@ -473,10 +473,7 @@
} else {
desugaredLibraryWrapperAnalysis = null;
}
- referenceToApiLevelMap = new IdentityHashMap<>();
- if (options.apiModelingOptions().enableApiCallerIdentification) {
- options.apiModelingOptions().appendToApiLevelMap(referenceToApiLevelMap, dexItemFactory);
- }
+ apiReferenceLevelCache = AndroidApiReferenceLevelCache.create(appView);
}
private AppInfoWithClassHierarchy appInfo() {
@@ -3028,7 +3025,7 @@
registerAnalysis(new GenericSignatureEnqueuerAnalysis(enqueuerDefinitionSupplier));
}
if (appView.options().apiModelingOptions().enableApiCallerIdentification) {
- registerAnalysis(new ApiModelAnalysis(appView, referenceToApiLevelMap));
+ registerAnalysis(new ApiModelAnalysis(appView, apiReferenceLevelCache));
}
if (mode.isInitialTreeShaking()) {
// This is simulating the effect of the "root set" applied rules.
@@ -3944,7 +3941,7 @@
void traceCode(ProgramMethod method) {
DefaultEnqueuerUseRegistry registry =
- useRegistryFactory.create(appView, method, this, referenceToApiLevelMap);
+ useRegistryFactory.create(appView, method, this, apiReferenceLevelCache::lookupMax);
method.registerCodeReferences(registry);
// Notify analyses.
analyses.forEach(analysis -> analysis.processTracedCode(method, registry));
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java
index 6bd8473..def9a41 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java
@@ -9,7 +9,7 @@
import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.utils.AndroidApiLevel;
-import java.util.Map;
+import java.util.function.BiFunction;
public interface EnqueuerUseRegistryFactory {
@@ -17,5 +17,5 @@
AppView<? extends AppInfoWithClassHierarchy> appView,
ProgramMethod currentMethod,
Enqueuer enqueuer,
- Map<DexReference, AndroidApiLevel> apiLevelReferenceMap);
+ BiFunction<DexReference, AndroidApiLevel, AndroidApiLevel> apiLevelReferenceMap);
}
diff --git a/src/main/java/com/android/tools/r8/utils/EntryUtils.java b/src/main/java/com/android/tools/r8/utils/EntryUtils.java
new file mode 100644
index 0000000..e1d28b1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/EntryUtils.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2021, 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.utils;
+
+import java.util.Map.Entry;
+import java.util.function.BiFunction;
+
+public class EntryUtils {
+
+ public static <K, V, R> R accept(Entry<K, V> entry, BiFunction<K, V, R> consumer) {
+ return consumer.apply(entry.getKey(), entry.getValue());
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 0a1fcc7..5af6963 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.ProgramConsumer;
import com.android.tools.r8.StringConsumer;
import com.android.tools.r8.Version;
+import com.android.tools.r8.androidapi.AndroidApiClass;
import com.android.tools.r8.cf.CfVersion;
import com.android.tools.r8.dex.Marker;
import com.android.tools.r8.dex.Marker.Backend;
@@ -37,7 +38,6 @@
import com.android.tools.r8.graph.DexLibraryClass;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
@@ -52,10 +52,10 @@
import com.android.tools.r8.naming.MapVersion;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.position.Position;
+import com.android.tools.r8.references.ClassReference;
import com.android.tools.r8.references.FieldReference;
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.references.TypeReference;
import com.android.tools.r8.repackaging.Repackaging.DefaultRepackagingConfiguration;
import com.android.tools.r8.repackaging.Repackaging.RepackagingConfiguration;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -84,10 +84,12 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -1319,21 +1321,68 @@
// A mapping from references to the api-level introducing them.
public Map<MethodReference, AndroidApiLevel> methodApiMapping = new HashMap<>();
public Map<FieldReference, AndroidApiLevel> fieldApiMapping = new HashMap<>();
- public Map<TypeReference, AndroidApiLevel> typeApiMapping = new HashMap<>();
+ public Map<ClassReference, AndroidApiLevel> classApiMapping = new HashMap<>();
+ public BiConsumer<MethodReference, AndroidApiLevel> tracedMethodApiLevelCallback = null;
public boolean enableApiCallerIdentification = false;
- public void appendToApiLevelMap(
- Map<DexReference, AndroidApiLevel> apiLevelMap, DexItemFactory factory) {
- methodApiMapping.forEach(
- (methodReference, apiLevel) ->
- apiLevelMap.put(factory.createMethod(methodReference), apiLevel));
- fieldApiMapping.forEach(
- (fieldReference, apiLevel) ->
- apiLevelMap.put(factory.createField(fieldReference), apiLevel));
- typeApiMapping.forEach(
- (typeReference, apiLevel) ->
- apiLevelMap.put(factory.createType(typeReference.getDescriptor()), apiLevel));
+ public void visitMockedApiReferences(BiConsumer<ClassReference, AndroidApiClass> consumer) {
+ if (methodApiMapping.isEmpty() && fieldApiMapping.isEmpty() && classApiMapping.isEmpty()) {
+ return;
+ }
+ Set<ClassReference> classReferences = new HashSet<>(classApiMapping.keySet());
+ methodApiMapping
+ .keySet()
+ .forEach(methodReference -> classReferences.add(methodReference.getHolderClass()));
+ fieldApiMapping
+ .keySet()
+ .forEach(methodReference -> classReferences.add(methodReference.getHolderClass()));
+ classReferences.forEach(
+ classReference -> {
+ consumer.accept(
+ classReference,
+ new AndroidApiClass(classReference) {
+ @Override
+ public AndroidApiLevel getApiLevel() {
+ return classApiMapping.getOrDefault(classReference, AndroidApiLevel.B);
+ }
+
+ @Override
+ public int getMemberCount() {
+ return 0;
+ }
+
+ @Override
+ public TraversalContinuation visitFields(
+ BiFunction<FieldReference, AndroidApiLevel, TraversalContinuation> visitor) {
+ for (Entry<FieldReference, AndroidApiLevel> entry :
+ fieldApiMapping.entrySet()) {
+ if (!entry.getKey().getHolderClass().equals(classReference)) {
+ continue;
+ }
+ if (EntryUtils.accept(entry, visitor).shouldBreak()) {
+ return TraversalContinuation.BREAK;
+ }
+ }
+ return TraversalContinuation.CONTINUE;
+ }
+
+ @Override
+ public TraversalContinuation visitMethods(
+ BiFunction<MethodReference, AndroidApiLevel, TraversalContinuation> visitor) {
+ for (Entry<MethodReference, AndroidApiLevel> entry :
+ methodApiMapping.entrySet()) {
+ if (!entry.getKey().getHolderClass().equals(classReference)) {
+ continue;
+ }
+ if (EntryUtils.accept(entry, visitor).shouldBreak()) {
+ return TraversalContinuation.BREAK;
+ }
+ }
+ return TraversalContinuation.CONTINUE;
+ }
+ });
+ });
}
}
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGenerator.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGenerator.java
index bc25cd4..9aa8f4d 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGenerator.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGenerator.java
@@ -94,10 +94,12 @@
.setClassDescriptor(getApiClassDescriptor(apiClass))
.addMethodTransformer(getInitTransformer(apiClass))
.addMethodTransformer(getApiLevelTransformer(apiClass))
+ .addMethodTransformer(getGetMemberCountTransformer(apiClass))
.addMethodTransformer(getVisitFieldsTransformer(apiClass))
.addMethodTransformer(getVisitMethodsTransformer(apiClass))
.removeMethods(MethodPredicate.onName("placeHolderForInit"))
.removeMethods(MethodPredicate.onName("placeHolderForGetApiLevel"))
+ .removeMethods(MethodPredicate.onName("placeHolderForGetMemberCount"))
.removeMethods(MethodPredicate.onName("placeHolderForVisitFields"))
.removeMethods(MethodPredicate.onName("placeHolderForVisitMethods"))
.transform());
@@ -170,6 +172,16 @@
});
}
+ // The transformer below changes AndroidApiDatabaseClassTemplate.getMemberCount from:
+ // return placeHolderForGetMemberCount();
+ // into
+ // return <memberCount>;
+ private static MethodTransformer getGetMemberCountTransformer(ParsedApiClass apiClass) {
+ return replaceCode(
+ "placeHolderForGetMemberCount",
+ transformer -> transformer.visitLdcInsn(apiClass.getMemberCount()));
+ }
+
// The transformer below changes AndroidApiDatabaseClassTemplate.visitFields from:
// placeHolder();
// return TraversalContinuation.CONTINUE;
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
index d85e3e5..6d46bfd 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
@@ -225,12 +225,7 @@
ZipUtils.unzip(
ToolHelper.DEPS.toString(),
tempDeps.toFile(),
- entry -> {
- if (entry.getName().startsWith("com/android/tools/r8/apimodel/")) {
- return false;
- }
- return true;
- });
+ entry -> !entry.getName().startsWith("com/android/tools/r8/apimodel/"));
Path modifiedDeps = Files.createTempFile("modified_deps", ".jar");
ZipUtils.zip(modifiedDeps, tempDeps);
return modifiedDeps;
@@ -242,6 +237,7 @@
apiClass -> {
expected.add(apiClass.getClassReference().getDescriptor());
expected.add(apiClass.getApiLevel().getName());
+ expected.add(apiClass.getMemberCount() + "");
BooleanBox added = new BooleanBox(false);
apiClass.visitFieldReferences(
(apiLevel, fieldReferences) -> {
@@ -292,6 +288,7 @@
if (apiClass != null) {
System.out.println(descriptor);
System.out.println(apiClass.getApiLevel().getName());
+ System.out.println(apiClass.getMemberCount());
apiClass.visitFields(
(reference, apiLevel) -> {
System.out.println(reference.getFieldType().getDescriptor());
@@ -322,6 +319,7 @@
if (apiClass != null) {
System.out.println(descriptor);
System.out.println(apiClass.getApiLevel().getName());
+ System.out.println(apiClass.getMemberCount());
apiClass.visitFields(
(reference, apiLevel) -> {
System.out.println(reference.getFieldType().getDescriptor());
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseClassTemplate.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseClassTemplate.java
index 2598898..faefd62 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseClassTemplate.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseClassTemplate.java
@@ -27,6 +27,12 @@
}
@Override
+ public int getMemberCount() {
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ return placeHolderForGetMemberCount();
+ }
+
+ @Override
public TraversalContinuation visitFields(
BiFunction<FieldReference, AndroidApiLevel, TraversalContinuation> visitor) {
// Code added dynamically in AndroidApiDatabaseBuilderGenerator.
@@ -50,6 +56,10 @@
return null;
}
+ private static int placeHolderForGetMemberCount() {
+ return 0;
+ }
+
private static void placeHolderForVisitFields() {}
private static void placeHolderForVisitMethods() {}
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
index c904dff..dbb71d3 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
@@ -135,6 +135,11 @@
private final TreeMap<AndroidApiLevel, List<FieldReference>> fieldReferences = new TreeMap<>();
private final Map<AndroidApiLevel, List<MethodReference>> methodReferences = new TreeMap<>();
+ private ParsedApiClass(ClassReference classReference, AndroidApiLevel apiLevel) {
+ this.classReference = classReference;
+ this.apiLevel = apiLevel;
+ }
+
public ClassReference getClassReference() {
return classReference;
}
@@ -143,9 +148,8 @@
return apiLevel;
}
- private ParsedApiClass(ClassReference classReference, AndroidApiLevel apiLevel) {
- this.classReference = classReference;
- this.apiLevel = apiLevel;
+ public int getMemberCount() {
+ return fieldReferences.size() + methodReferences.size();
}
private void register(FieldReference reference, AndroidApiLevel apiLevel) {
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelClassMergingWithDifferentApiFieldsTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelClassMergingWithDifferentApiFieldsTest.java
index 5d0c0b8..2f4e83c 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelClassMergingWithDifferentApiFieldsTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelClassMergingWithDifferentApiFieldsTest.java
@@ -4,7 +4,7 @@
package com.android.tools.r8.apimodel;
-import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForType;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
import com.android.tools.r8.NeverInline;
import com.android.tools.r8.TestBase;
@@ -45,7 +45,7 @@
.addHorizontallyMergedClassesInspector(
inspector -> inspector.assertClassesMerged(A.class, B.class))
.apply(ApiModelingTestHelper::enableApiCallerIdentification)
- .apply(setMockApiLevelForType(Api.class, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForClass(Api.class, AndroidApiLevel.L_MR1))
.compile()
.addRunClasspathClasses(Api.class)
.run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMethodWithApiTypeTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMethodWithApiTypeTest.java
index f6996e9..03b2f3e 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMethodWithApiTypeTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMethodWithApiTypeTest.java
@@ -4,7 +4,7 @@
package com.android.tools.r8.apimodel;
-import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForType;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.CoreMatchers.not;
@@ -46,7 +46,7 @@
.setMinApi(parameters.getApiLevel())
.addKeepMainRule(Main.class)
.enableNoHorizontalClassMergingAnnotations()
- .apply(setMockApiLevelForType(ApiType.class, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForClass(ApiType.class, AndroidApiLevel.L_MR1))
.apply(ApiModelingTestHelper::enableApiCallerIdentification)
.compile()
.addRunClasspathClasses(ApiType.class)
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInstanceFieldTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInstanceFieldTest.java
index c7b2b1c..a1ecb6a 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInstanceFieldTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelInstanceFieldTest.java
@@ -4,15 +4,16 @@
package com.android.tools.r8.apimodel;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForField;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static com.android.tools.r8.utils.AndroidApiLevel.L_MR1;
import com.android.tools.r8.NeverInline;
import com.android.tools.r8.NoHorizontalClassMerging;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.utils.AndroidApiLevel;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.junit.Test;
@@ -47,12 +48,11 @@
.addKeepMainRule(Main.class)
.enableInliningAnnotations()
.enableNoHorizontalClassMergingAnnotations()
- .apply(setMockApiLevelForField(apiField, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForField(apiField, L_MR1))
+ .apply(setMockApiLevelForDefaultInstanceInitializer(Api.class, L_MR1))
.apply(ApiModelingTestHelper::enableApiCallerIdentification)
.compile()
- .inspect(
- verifyThat(parameters, apiCaller)
- .inlinedIntoFromApiLevel(apiCallerCaller, AndroidApiLevel.L_MR1))
+ .inspect(verifyThat(parameters, apiCaller).inlinedIntoFromApiLevel(apiCallerCaller, L_MR1))
.addRunClasspathClasses(Api.class)
.run(parameters.getRuntime(), Main.class)
.assertSuccessWithOutputLines("Hello World!");
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelSuperTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelSuperTest.java
index 7d6230b..e1cf2e2 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelSuperTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelSuperTest.java
@@ -4,16 +4,19 @@
package com.android.tools.r8.apimodel;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.addTracedApiReferenceLevelCallBack;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static org.junit.Assert.assertEquals;
import com.android.tools.r8.NeverClassInline;
import com.android.tools.r8.NeverInline;
import com.android.tools.r8.NoHorizontalClassMerging;
-import com.android.tools.r8.NoVerticalClassMerging;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
import com.android.tools.r8.utils.AndroidApiLevel;
import java.lang.reflect.Method;
import org.junit.Test;
@@ -41,25 +44,36 @@
Method apiCaller = ApiCaller.class.getDeclaredMethod("apiLevel22");
Method apiCallerCaller = A.class.getDeclaredMethod("noApiCall");
testForR8(parameters.getBackend())
- .addInnerClasses(getClass())
+ .addProgramClasses(ApiCaller.class, A.class, Main.class)
+ .addLibraryClasses(Api.class)
+ .addDefaultRuntimeLibrary(parameters)
.setMinApi(parameters.getApiLevel())
.addKeepMainRule(Main.class)
.enableInliningAnnotations()
.enableNoHorizontalClassMergingAnnotations()
.enableNeverClassInliningAnnotations()
- .enableNoVerticalClassMergingAnnotations()
.apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForDefaultInstanceInitializer(Api.class, AndroidApiLevel.L_MR1))
.apply(ApiModelingTestHelper::enableApiCallerIdentification)
+ .apply(
+ addTracedApiReferenceLevelCallBack(
+ (method, apiLevel) -> {
+ if (Reference.methodFromMethod(apiCaller).equals(method)) {
+ if (parameters.isCfRuntime()) {
+ assertEquals(AndroidApiLevel.L_MR1, apiLevel);
+ } else {
+ assertEquals(AndroidApiLevel.L_MR1.max(parameters.getApiLevel()), apiLevel);
+ }
+ }
+ }))
.compile()
- .inspect(
- verifyThat(parameters, apiCaller)
- .inlinedIntoFromApiLevel(apiCallerCaller, AndroidApiLevel.L_MR1))
+ // We do not inline overrides calling super.
+ .inspect(verifyThat(parameters, apiCaller).notInlinedInto(apiCallerCaller))
.addRunClasspathClasses(Api.class)
.run(parameters.getRuntime(), Main.class)
.assertSuccessWithOutputLines("A::noApiCall", "ApiCaller::apiLevel22", "Api::apiLevel22");
}
- @NoVerticalClassMerging
public static class Api {
void apiLevel22() {
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelVirtualTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelVirtualTest.java
index d87ed82..f09f09d 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfHigherApiLevelVirtualTest.java
@@ -5,6 +5,7 @@
package com.android.tools.r8.apimodel;
import static com.android.tools.r8.apimodel.ApiModelNoInliningOfHigherApiLevelVirtualTest.ApiCaller.callVirtualMethod;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
@@ -48,6 +49,7 @@
.enableInliningAnnotations()
.enableNoHorizontalClassMergingAnnotations()
.apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForDefaultInstanceInitializer(Api.class, AndroidApiLevel.L_MR1))
.apply(ApiModelingTestHelper::enableApiCallerIdentification)
.compile()
.inspect(
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelVerticalMergingOfSuperClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelVerticalMergingOfSuperClassTest.java
index 80e213e..6cc4d9d 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelVerticalMergingOfSuperClassTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelVerticalMergingOfSuperClassTest.java
@@ -4,7 +4,7 @@
package com.android.tools.r8.apimodel;
-import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForType;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -44,7 +44,7 @@
.addKeepMainRule(Main.class)
.enableInliningAnnotations()
.enableNeverClassInliningAnnotations()
- .apply(setMockApiLevelForType(Api.class, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForClass(Api.class, AndroidApiLevel.L_MR1))
.apply(ApiModelingTestHelper::enableApiCallerIdentification)
.compile()
.inspect(
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
index 33d1deb..184536c 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
@@ -11,14 +11,17 @@
import com.android.tools.r8.TestCompilerBuilder;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.ThrowingConsumer;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.CodeMatchers;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.util.function.BiConsumer;
public abstract class ApiModelingTestHelper {
@@ -36,6 +39,23 @@
}
static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>>
+ ThrowableConsumer<T> setMockApiLevelForDefaultInstanceInitializer(
+ Class<?> clazz, AndroidApiLevel apiLevel) {
+ return compilerBuilder -> {
+ compilerBuilder.addOptionsModification(
+ options -> {
+ options
+ .apiModelingOptions()
+ .methodApiMapping
+ .put(
+ Reference.method(
+ Reference.classFromClass(clazz), "<init>", ImmutableList.of(), null),
+ apiLevel);
+ });
+ };
+ }
+
+ static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>>
ThrowableConsumer<T> setMockApiLevelForField(Field field, AndroidApiLevel apiLevel) {
return compilerBuilder -> {
compilerBuilder.addOptionsModification(
@@ -48,14 +68,14 @@
};
}
- static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>> ThrowableConsumer<T> setMockApiLevelForType(
- Class<?> clazz, AndroidApiLevel apiLevel) {
+ static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>>
+ ThrowableConsumer<T> setMockApiLevelForClass(Class<?> clazz, AndroidApiLevel apiLevel) {
return compilerBuilder -> {
compilerBuilder.addOptionsModification(
options -> {
options
.apiModelingOptions()
- .typeApiMapping
+ .classApiMapping
.put(Reference.classFromClass(clazz), apiLevel);
});
};
@@ -68,6 +88,17 @@
});
}
+ static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>>
+ ThrowableConsumer<T> addTracedApiReferenceLevelCallBack(
+ BiConsumer<MethodReference, AndroidApiLevel> consumer) {
+ return compilerBuilder -> {
+ compilerBuilder.addOptionsModification(
+ options -> {
+ options.apiModelingOptions().tracedMethodApiLevelCallback = consumer;
+ });
+ };
+ }
+
static ApiModelingMethodVerificationHelper verifyThat(TestParameters parameters, Method method) {
return new ApiModelingMethodVerificationHelper(parameters, method);
}
@@ -89,7 +120,7 @@
: notInlinedInto(method);
}
- private ThrowingConsumer<CodeInspector, Exception> notInlinedInto(Method method) {
+ protected ThrowingConsumer<CodeInspector, Exception> notInlinedInto(Method method) {
return inspector -> {
MethodSubject candidate = inspector.method(methodOfInterest);
assertThat(candidate, isPresent());
diff --git a/third_party/android_jar/api-database.tar.gz.sha1 b/third_party/android_jar/api-database.tar.gz.sha1
index 84abf49..6336e23 100644
--- a/third_party/android_jar/api-database.tar.gz.sha1
+++ b/third_party/android_jar/api-database.tar.gz.sha1
@@ -1 +1 @@
-e4da4b29079ac393e0012e7676dcca0799841e29
\ No newline at end of file
+be72aeca006f1aba8b1fe4d9c3ff4c0e76259960
\ No newline at end of file