// 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.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.google.common.collect.Sets; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; 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. */ public abstract class ClassMap { // For each type which has ever been queried stores one class loaded from // resources provided by different resource providers. // // NOTE: all access must be synchronized on `classes`. private final Map> classes; // Class provider if available. // // 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. // // NOTE: all access must be synchronized on `classes`. private ClassProvider classProvider; ClassMap(Map> classes, ClassProvider classProvider) { this.classes = classes == null ? new IdentityHashMap<>() : classes; this.classProvider = classProvider; assert this.classProvider == null || this.classProvider.getClassKind() == getClassKind(); } /** 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 getTransparentSupplier(T clazz); /** Kind of the classes supported by this collection. */ abstract ClassKind getClassKind(); @Override public String toString() { synchronized (classes) { return classes.size() + " loaded, provider: " + (classProvider == null ? "none" : classProvider.toString()); } } /** Returns a definition for a class or `null` if there is no such class in the collection. */ public T get(DexType type) { Supplier supplier; synchronized (classes) { supplier = classes.get(type); // Get class supplier, create it if it does not // exist and the collection is NOT fully loaded. if (supplier == null) { if (classProvider == null) { // There is no supplier, but the collection is fully loaded. return null; } supplier = new ConcurrentClassLoader<>(this, this.classProvider, type); classes.put(type, supplier); } } return supplier.get(); } /** Returns all classes from the collection. The collection must be force-loaded. */ public List getAllClasses() { List loadedClasses = new ArrayList<>(); synchronized (classes) { if (classProvider != null) { throw new Unreachable("Getting all classes from not fully loaded collection."); } for (Supplier 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; } /** * Forces loading of all the classes satisfying the criteria specified. * * 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 void forceLoad(Predicate load) { Set knownClasses; ClassProvider classProvider; synchronized (classes) { classProvider = this.classProvider; if (classProvider == null) { return; } // Collects the types which might be represented in fully loaded class map. knownClasses = Sets.newIdentityHashSet(); knownClasses.addAll(classes.keySet()); } // Add all types the class provider provides. Note that it may take time for class // provider to collect these types, so ve do it outside synchronized context. knownClasses.addAll(classProvider.collectTypes()); // Make sure all the types in `knownClasses` are loaded. // // We just go and touch every class, thus triggering their loading if they // are not loaded so far. In case the class has already been loaded, // touching the class will be a no-op with minimal overhead. for (DexType type : knownClasses) { if (load.test(type)) { get(type); } } synchronized (classes) { if (this.classProvider == null) { return; // Has been force-loaded concurrently. } // We avoid calling get() on a class supplier unless we know it was loaded. // At this time `classes` may have more types then `knownClasses`, but for // all extra classes we expect the supplier to return 'null' after loading. Iterator>> iterator = classes.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry> e = iterator.next(); if (knownClasses.contains(e.getKey())) { // Get the class (it is expected to be loaded by this time). T clazz = e.getValue().get(); if (clazz != null) { // Since the class is already loaded, get rid of possible wrapping suppliers. assert clazz.type == e.getKey(); e.setValue(getTransparentSupplier(clazz)); continue; } } // If the type is not in `knownClasses` or resolves to `null`, // just remove the record from the map. iterator.remove(); } // Mark the class map as fully loaded. this.classProvider = 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 implements Supplier { private ClassMap classMap; private ClassProvider provider; private DexType type; private T clazz = null; private volatile boolean ready = false; ConcurrentClassLoader(ClassMap classMap, ClassProvider provider, DexType type) { this.classMap = classMap; this.provider = provider; this.type = type; } @Override 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; } } }