blob: f9e89bd478eff112f930a601e96c94cbedd14d83 [file] [log] [blame]
// Copyright (c) 2017, 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 com.android.tools.r8.ProgramResource;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.ClassKind;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.threading.TaskCollection;
import com.android.tools.r8.utils.timing.Timing;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* Represents a collection of classes. Collection can be fully loaded, lazy loaded or have preloaded
* classes along with lazy loaded content.
*
* The {@link #get(DexType)} operation for loading the type of a class is non-locking if a class was
* loaded before but may block if a class has not yet been loaded.
*
* {@link #forceLoad(Predicate)} can be used to load all classes available from the given class
* provider. Only after
*/
public abstract class ClassMap<T extends DexClass> {
/**
* For each type which has ever been queried stores one class loaded from resources provided by
* different resource providers.
*
* <p><b>NOTE:</b> mutated concurrently but we require that the value assigned to a keys never
* changes its meaning, i.e., the supplier object might change but the contained value does not.
* We also allow the transition from Supplier of a null value to the actual value null and vice
* versa.
*/
private final ConcurrentHashMap<DexType, Supplier<T>> classes;
/**
* Class provider if available.
* <p>
* If the class provider is `null` it indicates that all classes are already present in a map
* referenced by `classes` and thus the collection is fully loaded.
* <p>
* <b>NOTE:</b> the field may only transition from a value to null while the this object is
* locked. Furthermore, it may never transition back from null.
*/
private final AtomicReference<ClassProvider<T>> classProvider = new AtomicReference<>();
ClassMap(ConcurrentHashMap<DexType, Supplier<T>> classes) {
this.classes = classes;
this.classProvider.set(null);
}
ClassMap(ClassProvider<T> classProvider) {
assert classProvider == null || classProvider.getClassKind() == getClassKind();
this.classes = new ConcurrentHashMap<>();
this.classProvider.set(classProvider);
}
/**
* Resolves a class conflict by selecting a class, may generate compilation error.
*/
abstract T resolveClassConflict(T a, T b);
/**
* Return supplier for preloaded class.
*/
abstract Supplier<T> getTransparentSupplier(T clazz);
/** Kind of the classes supported by this collection. */
abstract ClassKind<?> getClassKind();
@Override
public String toString() {
return classes.size() + " loaded, provider: " + this.classProvider.get();
}
/**
* Returns a definition for a class or `null` if there is no such class in the collection.
*/
public T get(DexType type) {
// If this collection is fully loaded, just return the found result.
ClassProvider<T> classProvider = this.classProvider.get();
if (classProvider == null) {
Supplier<T> supplier = classes.get(type);
return supplier == null ? null : supplier.get();
}
return internalGetNotFullyLoaded(type, classProvider);
}
private T internalGetNotFullyLoaded(DexType type, ClassProvider<T> classProvider) {
return classes
.computeIfAbsent(type, key -> new ConcurrentClassLoader<>(this, classProvider, type))
.get();
}
/**
* Clears the type so if a class with the given type was present, it cannot be found anymore. This
* has to be run at a join point, concurrent accesses may be confused.
*/
public void clearType(DexType type) {
classes.remove(type);
ClassProvider<T> provider = classProvider.get();
if (provider == null) {
return;
}
classProvider.set(provider.without(ImmutableSet.of(type)));
}
/**
* Returns all classes from the collection. The collection must be force-loaded.
*/
public List<T> getAllClasses() {
if (classProvider.get() != null) {
throw new Unreachable("Getting all classes from not fully loaded collection.");
}
List<T> loadedClasses = new ArrayList<>();
// This is fully loaded, so the class map will no longer change.
for (Supplier<T> supplier : classes.values()) {
// Since the class map is fully loaded, all suppliers must be
// loaded and non-null.
T clazz = supplier.get();
assert clazz != null;
loadedClasses.add(clazz);
}
return loadedClasses;
}
public ImmutableMap<DexType, T> getAllClassesInMap() {
if (classProvider.get() != null) {
throw new Unreachable("Getting all classes from not fully loaded collection.");
}
ImmutableMap.Builder<DexType, T> builder = ImmutableMap.builder();
// This is fully loaded, so the class map will no longer change.
forEach(builder::put);
return builder.build();
}
public void forEach(BiConsumer<DexType, T> consumer) {
if (classProvider.get() != null) {
throw new Unreachable("Getting all classes from not fully loaded collection.");
}
classes.forEach((type, supplier) -> consumer.accept(type, supplier.get()));
}
public Iterable<DexType> getAllTypes() {
return classes.keySet();
}
public Iterable<DexType> getAllClassProviderTypes() {
ClassProvider<T> theClassProvider = classProvider.get();
if (theClassProvider != null) {
return theClassProvider.collectTypes();
}
throw new CompilationError(
"Cannot access all types since the classProvider is no longer available");
}
/**
* Forces loading of all the classes satisfying the criteria specified.
*
* <p>NOTE: after this method finishes, the class map is considered to be fully-loaded and thus
* sealed. This has one side-effect: if we filter out some of the classes with `load` predicate,
* these classes will never be loaded.
*/
public ClassMap<T> forceLoad(
InternalOptions options,
TaskCollection<?> tasks,
Timing timing,
Predicate<ProgramResource> load) {
// Cache value of class provider, as it might change concurrently.
if (isFullyLoaded()) {
return this;
}
ClassProvider<T> classProvider = this.classProvider.get();
classProvider.forceLoad(
clazz ->
classes.compute(
clazz.getType(),
(type, existing) ->
getTransparentSupplier(
existing == null ? clazz : resolveClassConflict(existing.get(), clazz))),
options,
load,
tasks,
timing);
return this;
}
public void setFullyLoaded() {
// Remove null entries.
MapUtils.removeIf(classes, (type, supplier) -> supplier.get() == null);
// Mark the class map as fully loaded. This has to be the last operation, as this toggles
// the class map into fully loaded state and the get operation will no longer try to load
// classes by blocking on 'this' and hence wait for the loading operation to finish.
this.classProvider.set(null);
}
public boolean isFullyLoaded() {
return this.classProvider.get() == null;
}
// Supplier implementing a thread-safe loader for a class loaded from a
// class provider. Helps avoid synchronizing on the whole class map
// when loading a class.
private static class ConcurrentClassLoader<T extends DexClass> implements Supplier<T> {
private ClassMap<T> classMap;
private ClassProvider<T> provider;
private DexType type;
private T clazz = null;
private volatile boolean ready = false;
private ConcurrentClassLoader(ClassMap<T> classMap, ClassProvider<T> provider, DexType type) {
this.classMap = classMap;
this.provider = provider;
this.type = type;
}
@Override
@SuppressWarnings("ReferenceEquality")
public T get() {
if (ready) {
return clazz;
}
synchronized (this) {
if (!ready) {
assert classMap != null && provider != null && type != null;
provider.collectClass(type, createdClass -> {
assert createdClass != null;
assert classMap.getClassKind().isOfKind(createdClass);
assert !ready;
if (createdClass.type != type) {
throw new CompilationError(
"Class content provided for type descriptor " + type.toSourceString() +
" actually defines class " + createdClass.type.toSourceString());
}
if (clazz == null) {
clazz = createdClass;
} else {
// The class resolution *may* generate a compilation error as one of
// possible resolutions. In this case we leave `value` in (false, null)
// state so in rare case of another thread trying to get the same class
// before this error is propagated it will get the same conflict.
T oldClass = clazz;
clazz = null;
clazz = classMap.resolveClassConflict(oldClass, createdClass);
}
});
classMap = null;
provider = null;
type = null;
ready = true;
}
}
assert ready;
assert classMap == null && provider == null && type == null;
return clazz;
}
}
}