blob: f06aa45b9e3429ffaba482d2e6693ad5c6d81155 [file] [log] [blame]
// Copyright (c) 2016, 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.dex;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.InternalCompilerError;
import com.android.tools.r8.errors.MainDexError;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexCallSite;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItem;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexMethodHandle;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.IndexedDexItem;
import com.android.tools.r8.graph.ObjectToOffsetMapping;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.PackageDistribution;
import com.android.tools.r8.utils.ThreadUtils;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Function;
public class VirtualFile {
// The fill strategy determine how to distribute classes into dex files.
enum FillStrategy {
// Distribute classes in as few dex files as possible filling each dex file as much as possible.
FILL_MAX,
// Distribute classes keeping some space for future growth. This is mainly useful together with
// the package map distribution.
LEAVE_SPACE_FOR_GROWTH,
// TODO(sgjesse): Does "minimal main dex" combined with "leave space for growth" make sense?
}
private static final int MAX_ENTRIES = Constants.U16BIT_MAX + 1;
/**
* When distributing classes across files we aim to leave some space. The amount of space left is
* driven by this constant.
*/
private static final int MAX_PREFILL_ENTRIES = MAX_ENTRIES - 5000;
private final int id;
private final VirtualFileIndexedItemCollection indexedItems;
private final IndexedItemTransaction transaction;
private VirtualFile(int id, NamingLens namingLens) {
this.id = id;
this.indexedItems = new VirtualFileIndexedItemCollection(id);
this.transaction = new IndexedItemTransaction(indexedItems, namingLens);
}
public int getId() {
return id;
}
public Set<String> getClassDescriptors() {
Set<String> classDescriptors = new HashSet<>();
for (DexProgramClass clazz : indexedItems.classes) {
boolean added = classDescriptors.add(clazz.type.descriptor.toString());
assert added;
}
return classDescriptors;
}
public static String deriveCommonPrefixAndSanityCheck(List<String> fileNames) {
Iterator<String> nameIterator = fileNames.iterator();
String first = nameIterator.next();
if (!first.toLowerCase().endsWith(FileUtils.DEX_EXTENSION)) {
throw new RuntimeException("Illegal suffix for dex file: `" + first + "`.");
}
String prefix = first.substring(0, first.length() - FileUtils.DEX_EXTENSION.length());
int index = 2;
while (nameIterator.hasNext()) {
String next = nameIterator.next();
if (!next.toLowerCase().endsWith(FileUtils.DEX_EXTENSION)) {
throw new RuntimeException("Illegal suffix for dex file: `" + first + "`.");
}
if (!next.startsWith(prefix)) {
throw new RuntimeException("Input filenames lack common prefix.");
}
String numberPart =
next.substring(prefix.length(), next.length() - FileUtils.DEX_EXTENSION.length());
if (Integer.parseInt(numberPart) != index++) {
throw new RuntimeException("DEX files are not numbered consecutively.");
}
}
return prefix;
}
private static Map<DexProgramClass, String> computeOriginalNameMapping(
Collection<DexProgramClass> classes,
ClassNameMapper proguardMap) {
Map<DexProgramClass, String> originalNames = new HashMap<>();
classes.forEach((DexProgramClass c) ->
originalNames.put(c,
DescriptorUtils.descriptorToJavaType(c.type.toDescriptorString(), proguardMap)));
return originalNames;
}
private static String extractPrefixToken(int prefixLength, String className, boolean addStar) {
int index = 0;
int lastIndex = 0;
int segmentCount = 0;
while (lastIndex != -1 && segmentCount++ < prefixLength) {
index = lastIndex;
lastIndex = className.indexOf('.', index + 1);
}
String prefix = className.substring(0, index);
if (addStar && segmentCount >= prefixLength) {
// Full match, add a * to also match sub-packages.
prefix += ".*";
}
return prefix;
}
public ObjectToOffsetMapping computeMapping(DexApplication application) {
assert transaction.isEmpty();
return new ObjectToOffsetMapping(
id,
application,
indexedItems.classes.toArray(new DexProgramClass[indexedItems.classes.size()]),
indexedItems.protos.toArray(new DexProto[indexedItems.protos.size()]),
indexedItems.types.toArray(new DexType[indexedItems.types.size()]),
indexedItems.methods.toArray(new DexMethod[indexedItems.methods.size()]),
indexedItems.fields.toArray(new DexField[indexedItems.fields.size()]),
indexedItems.strings.toArray(new DexString[indexedItems.strings.size()]),
indexedItems.callSites.toArray(new DexCallSite[indexedItems.callSites.size()]),
indexedItems.methodHandles.toArray(new DexMethodHandle[indexedItems.methodHandles.size()]));
}
private void addClass(DexProgramClass clazz) {
transaction.addClassAndDependencies(clazz);
}
private static boolean isFull(int numberOfMethods, int numberOfFields, int maximum) {
return (numberOfMethods > maximum) || (numberOfFields > maximum);
}
private boolean isFull() {
return isFull(transaction.getNumberOfMethods(), transaction.getNumberOfFields(), MAX_ENTRIES);
}
void throwIfFull(boolean hasMainDexList) {
if (!isFull()) {
return;
}
throw new MainDexError(
hasMainDexList,
transaction.getNumberOfMethods(),
transaction.getNumberOfFields(),
MAX_ENTRIES);
}
private boolean isFilledEnough(FillStrategy fillStrategy) {
return isFull(
transaction.getNumberOfMethods(),
transaction.getNumberOfFields(),
fillStrategy == FillStrategy.FILL_MAX ? MAX_ENTRIES : MAX_PREFILL_ENTRIES);
}
public void abortTransaction() {
transaction.abort();
}
public void commitTransaction() {
transaction.commit();
}
public boolean isEmpty() {
return indexedItems.classes.isEmpty();
}
public List<DexProgramClass> classes() {
return indexedItems.classes;
}
public abstract static class Distributor {
protected final DexApplication application;
protected final ApplicationWriter writer;
protected final Map<Integer, VirtualFile> nameToFileMap = new HashMap<>();
Distributor(ApplicationWriter writer) {
this.application = writer.application;
this.writer = writer;
}
public abstract Map<Integer, VirtualFile> run() throws ExecutionException, IOException;
}
public static class FilePerClassDistributor extends Distributor {
FilePerClassDistributor(ApplicationWriter writer) {
super(writer);
}
public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
// Assign dedicated virtual files for all program classes.
for (DexProgramClass clazz : application.classes()) {
VirtualFile file = new VirtualFile(nameToFileMap.size(), writer.namingLens);
nameToFileMap.put(nameToFileMap.size(), file);
// Write the marker string to all files in files-per-class mode.
// TODO(sgjesse): Get rid of this (currently some tests fails without it).
if (writer.markerString != null) {
file.transaction.addString(writer.markerString);
}
file.addClass(clazz);
file.commitTransaction();
}
return nameToFileMap;
}
}
public abstract static class DistributorBase extends Distributor {
protected Set<DexProgramClass> classes;
protected Map<DexProgramClass, String> originalNames;
protected final VirtualFile mainDexFile;
DistributorBase(ApplicationWriter writer) {
super(writer);
// Create the primary dex file. The distribution will add more if needed.
mainDexFile = new VirtualFile(0, writer.namingLens);
nameToFileMap.put(0, mainDexFile);
if (writer.markerString != null) {
mainDexFile.transaction.addString(writer.markerString);
mainDexFile.commitTransaction();
}
classes = Sets.newHashSet(application.classes());
originalNames = computeOriginalNameMapping(classes, application.getProguardMap());
}
protected void fillForMainDexList(Set<DexProgramClass> classes) {
if (!application.mainDexList.isEmpty()) {
VirtualFile mainDexFile = nameToFileMap.get(0);
for (DexType type : application.mainDexList) {
DexClass clazz = application.definitionFor(type);
if (clazz != null && clazz.isProgramClass()) {
DexProgramClass programClass = (DexProgramClass) clazz;
mainDexFile.addClass(programClass);
classes.remove(programClass);
} else {
System.out.println(
"WARNING: Application does not contain `"
+ type.toSourceString()
+ "` as referenced in main-dex-list.");
}
mainDexFile.commitTransaction();
}
mainDexFile.throwIfFull(true);
}
}
TreeSet<DexProgramClass> sortClassesByPackage(Set<DexProgramClass> classes,
Map<DexProgramClass, String> originalNames) {
TreeSet<DexProgramClass> sortedClasses = new TreeSet<>(
(DexProgramClass a, DexProgramClass b) -> {
String originalA = originalNames.get(a);
String originalB = originalNames.get(b);
int indexA = originalA.lastIndexOf('.');
int indexB = originalB.lastIndexOf('.');
if (indexA == -1 && indexB == -1) {
// Empty package, compare the class names.
return originalA.compareTo(originalB);
}
if (indexA == -1) {
// Empty package name comes first.
return -1;
}
if (indexB == -1) {
// Empty package name comes first.
return 1;
}
String prefixA = originalA.substring(0, indexA);
String prefixB = originalB.substring(0, indexB);
int result = prefixA.compareTo(prefixB);
if (result != 0) {
return result;
}
return originalA.compareTo(originalB);
});
sortedClasses.addAll(classes);
return sortedClasses;
}
}
public static class FillFilesDistributor extends DistributorBase {
boolean minimalMainDex;
private final FillStrategy fillStrategy;
FillFilesDistributor(ApplicationWriter writer, boolean minimalMainDex) {
super(writer);
this.minimalMainDex = minimalMainDex;
this.fillStrategy = FillStrategy.FILL_MAX;
}
public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
// First fill required classes into the main dex file.
fillForMainDexList(classes);
if (classes.isEmpty()) {
// All classes ended up in the main dex file, no more to do.
return nameToFileMap;
}
Map<Integer, VirtualFile> filesForDistribution = nameToFileMap;
if (minimalMainDex && !mainDexFile.isEmpty()) {
assert !nameToFileMap.get(0).isEmpty();
// The main dex file is filtered out, so create ensure at least one file for the remaining
// classes
nameToFileMap.put(1, new VirtualFile(1, writer.namingLens));
filesForDistribution = Maps.filterKeys(filesForDistribution, key -> key != 0);
}
// Sort the remaining classes based on the original names.
// This with make classes from the same package be adjacent.
classes = sortClassesByPackage(classes, originalNames);
new PackageSplitPopulator(
filesForDistribution, classes, originalNames, null, application.dexItemFactory,
fillStrategy, writer.namingLens)
.call();
return nameToFileMap;
}
}
public static class MonoDexDistributor extends DistributorBase {
MonoDexDistributor(ApplicationWriter writer) {
super(writer);
}
@Override
public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
// Add all classes to the main dex file.
for (DexProgramClass programClass : classes) {
mainDexFile.addClass(programClass);
}
mainDexFile.commitTransaction();
mainDexFile.throwIfFull(false);
return nameToFileMap;
}
}
public static class PackageMapDistributor extends DistributorBase {
private final PackageDistribution packageDistribution;
private final ExecutorService executorService;
PackageMapDistributor(
ApplicationWriter writer,
PackageDistribution packageDistribution,
ExecutorService executorService) {
super(writer);
this.packageDistribution = packageDistribution;
this.executorService = executorService;
}
public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
// Strategy for distributing classes for write out:
// 1. Place all files in the package distribution file in the proposed files (if any).
// 2. Place the remaining files based on their packages in sorted order.
assert nameToFileMap.size() == 1;
assert nameToFileMap.containsKey(0);
int maxReferencedIndex = packageDistribution.maxReferencedIndex();
for (int index = 1; index <= maxReferencedIndex; index++) {
VirtualFile file = new VirtualFile(index, writer.namingLens);
nameToFileMap.put(index, file);
}
// First fill required classes into the main dex file.
fillForMainDexList(classes);
// Sort the remaining classes based on the original names.
// This with make classes from the same package be adjacent.
classes = sortClassesByPackage(classes, originalNames);
Set<String> usedPrefixes = fillForDistribution(classes, originalNames);
// TODO(zerny): Add the package map to AndroidApp and refactor its generation.
Map<String, Integer> newAssignments;
if (classes.isEmpty()) {
newAssignments = Collections.emptyMap();
} else {
newAssignments =
new PackageSplitPopulator(
nameToFileMap, classes, originalNames, usedPrefixes, application.dexItemFactory,
FillStrategy.LEAVE_SPACE_FOR_GROWTH, writer.namingLens)
.call();
if (!newAssignments.isEmpty() && nameToFileMap.size() > 1) {
System.err.println(" * The used package map is missing entries. The following default "
+ "mappings have been used:");
writeAssignments(newAssignments, new OutputStreamWriter(System.err));
System.err.println(" * Consider updating the map.");
}
}
Path newPackageMap = Paths.get("package.map");
System.out.println(" - " + newPackageMap.toString());
PackageDistribution.writePackageToFileMap(newPackageMap, newAssignments, packageDistribution);
return nameToFileMap;
}
private Set<String> fillForDistribution(Set<DexProgramClass> classes,
Map<DexProgramClass, String> originalNames) throws ExecutionException {
Set<String> usedPrefixes = null;
if (packageDistribution != null) {
ArrayList<Future<List<DexProgramClass>>> futures = new ArrayList<>(nameToFileMap.size());
usedPrefixes = packageDistribution.getFiles();
for (VirtualFile file : nameToFileMap.values()) {
PackageMapPopulator populator =
new PackageMapPopulator(file, classes, packageDistribution, originalNames);
futures.add(executorService.submit(populator));
}
ThreadUtils.awaitFutures(futures).forEach(classes::removeAll);
}
return usedPrefixes;
}
private void writeAssignments(Map<String, Integer> assignments, Writer output)
throws IOException{
for (Entry<String, Integer> entry : assignments.entrySet()) {
output.write(" ");
PackageDistribution.formatEntry(entry, output);
output.write("\n");
}
output.flush();
}
}
private static class VirtualFileIndexedItemCollection implements IndexedItemCollection {
final int id;
private final List<DexProgramClass> classes = new ArrayList<>();
private final List<DexProto> protos = new ArrayList<>();
private final List<DexType> types = new ArrayList<>();
private final List<DexMethod> methods = new ArrayList<>();
private final List<DexField> fields = new ArrayList<>();
private final List<DexString> strings = new ArrayList<>();
private final List<DexCallSite> callSites = new ArrayList<>();
private final List<DexMethodHandle> methodHandles = new ArrayList<>();
private final Set<DexClass> seenClasses = Sets.newIdentityHashSet();
private VirtualFileIndexedItemCollection(int id) {
this.id = id;
}
private <T extends IndexedDexItem> boolean addItem(T item, List<T> itemList) {
assert item != null;
if (item.assignToVirtualFile(id)) {
itemList.add(item);
return true;
}
return false;
}
@Override
public boolean addClass(DexProgramClass clazz) {
if (seenClasses.add(clazz)) {
classes.add(clazz);
return true;
}
return false;
}
@Override
public boolean addField(DexField field) {
return addItem(field, fields);
}
@Override
public boolean addMethod(DexMethod method) {
return addItem(method, methods);
}
@Override
public boolean addString(DexString string) {
return addItem(string, strings);
}
@Override
public boolean addProto(DexProto proto) {
return addItem(proto, protos);
}
@Override
public boolean addCallSite(DexCallSite callSite) {
return addItem(callSite, callSites);
}
@Override
public boolean addMethodHandle(DexMethodHandle methodHandle) {
return addItem(methodHandle, methodHandles);
}
@Override
public boolean addType(DexType type) {
return addItem(type, types);
}
public int getNumberOfMethods() {
return methods.size();
}
public int getNumberOfFields() {
return fields.size();
}
public int getNumberOfStrings() {
return strings.size();
}
}
private static class IndexedItemTransaction implements IndexedItemCollection {
private final VirtualFileIndexedItemCollection base;
private final NamingLens namingLens;
private final Set<DexProgramClass> classes = new LinkedHashSet<>();
private final Set<DexField> fields = new LinkedHashSet<>();
private final Set<DexMethod> methods = new LinkedHashSet<>();
private final Set<DexType> types = new LinkedHashSet<>();
private final Set<DexProto> protos = new LinkedHashSet<>();
private final Set<DexString> strings = new LinkedHashSet<>();
private final Set<DexCallSite> callSites = new LinkedHashSet<>();
private final Set<DexMethodHandle> methodHandles = new LinkedHashSet<>();
private IndexedItemTransaction(VirtualFileIndexedItemCollection base,
NamingLens namingLens) {
this.base = base;
this.namingLens = namingLens;
}
private <T extends IndexedDexItem> boolean maybeInsert(T item, Set<T> set) {
if (item.hasVirtualFileData(base.id) || set.contains(item)) {
return false;
}
set.add(item);
return true;
}
void addClassAndDependencies(DexProgramClass clazz) {
clazz.collectIndexedItems(this);
}
@Override
public boolean addClass(DexProgramClass dexProgramClass) {
if (base.seenClasses.contains(dexProgramClass) || classes.contains(dexProgramClass)) {
return false;
}
classes.add(dexProgramClass);
return true;
}
@Override
public boolean addField(DexField field) {
return maybeInsert(field, fields);
}
@Override
public boolean addMethod(DexMethod method) {
return maybeInsert(method, methods);
}
@Override
public boolean addString(DexString string) {
return maybeInsert(string, strings);
}
@Override
public boolean addProto(DexProto proto) {
return maybeInsert(proto, protos);
}
@Override
public boolean addType(DexType type) {
return maybeInsert(type, types);
}
@Override
public boolean addCallSite(DexCallSite callSite) {
return maybeInsert(callSite, callSites);
}
@Override
public boolean addMethodHandle(DexMethodHandle methodHandle) {
return maybeInsert(methodHandle, methodHandles);
}
@Override
public DexString getRenamedDescriptor(DexType type) {
return namingLens.lookupDescriptor(type);
}
@Override
public DexString getRenamedName(DexMethod method) {
assert namingLens.checkTargetCanBeTranslated(method);
return namingLens.lookupName(method);
}
@Override
public DexString getRenamedName(DexField field) {
return namingLens.lookupName(field);
}
int getNumberOfMethods() {
return methods.size() + base.getNumberOfMethods();
}
int getNumberOfFields() {
return fields.size() + base.getNumberOfFields();
}
private <T extends DexItem> void commitItemsIn(Set<T> set, Function<T, Boolean> hook) {
set.forEach((item) -> {
boolean newlyAdded = hook.apply(item);
assert newlyAdded;
});
set.clear();
}
void commit() {
commitItemsIn(classes, base::addClass);
commitItemsIn(fields, base::addField);
commitItemsIn(methods, base::addMethod);
commitItemsIn(protos, base::addProto);
commitItemsIn(types, base::addType);
commitItemsIn(strings, base::addString);
commitItemsIn(callSites, base::addCallSite);
commitItemsIn(methodHandles, base::addMethodHandle);
}
void abort() {
classes.clear();
fields.clear();
methods.clear();
protos.clear();
types.clear();
strings.clear();
}
public boolean isEmpty() {
return classes.isEmpty() && fields.isEmpty() && methods.isEmpty() && protos.isEmpty()
&& types.isEmpty() && strings.isEmpty();
}
int getNumberOfStrings() {
return strings.size() + base.getNumberOfStrings();
}
int getNumberOfClasses() {
return classes.size() + base.classes.size();
}
}
/**
* Adds all classes from the given set that are covered by a corresponding package map
* specification to the given file.
*/
private static class PackageMapPopulator implements Callable<List<DexProgramClass>> {
private final VirtualFile file;
private final Collection<DexProgramClass> classes;
private final PackageDistribution packageDistribution;
private final Map<DexProgramClass, String> originalNames;
PackageMapPopulator(
VirtualFile file,
Collection<DexProgramClass> classes,
PackageDistribution packageDistribution,
Map<DexProgramClass, String> originalNames) {
this.file = file;
this.classes = classes;
this.packageDistribution = packageDistribution;
this.originalNames = originalNames;
}
@Override
public List<DexProgramClass> call() {
String currentPrefix = null;
int currentFileId = -1;
List<DexProgramClass> inserted = new ArrayList<>();
for (DexProgramClass clazz : classes) {
String originalName = originalNames.get(clazz);
assert originalName != null;
if (!coveredByPrefix(originalName, currentPrefix)) {
if (currentPrefix != null) {
file.commitTransaction();
}
currentPrefix = lookupPrefixFor(originalName);
if (currentPrefix == null) {
currentFileId = -1;
} else {
currentFileId = packageDistribution.get(currentPrefix);
}
}
if (currentFileId == file.id) {
file.addClass(clazz);
inserted.add(clazz);
}
if (file.isFull()) {
throw new CompilationError(
"Cannot fit package " + currentPrefix
+ " in requested dex file, consider removing mapping.");
}
}
file.commitTransaction();
return inserted;
}
private String lookupPrefixFor(String originalName) {
// First, check whether we have a match on the full package name.
int lastIndexOfDot = originalName.lastIndexOf('.');
if (lastIndexOfDot < 0) {
return null;
}
String prefix = originalName.substring(0, lastIndexOfDot);
if (packageDistribution.containsFile(prefix)) {
return prefix;
}
// Second, look for .* qualified entries.
int index;
prefix = originalName;
while ((index = prefix.lastIndexOf('.')) != -1) {
prefix = prefix.substring(0, index);
if (packageDistribution.containsFile(prefix + ".*")) {
return prefix + ".*";
}
}
return null;
}
static boolean coveredByPrefix(String originalName, String currentPrefix) {
if (currentPrefix == null) {
return false;
}
if (currentPrefix.endsWith(".*")) {
return originalName.startsWith(currentPrefix.substring(0, currentPrefix.length() - 2));
} else {
return originalName.startsWith(currentPrefix)
&& originalName.lastIndexOf('.') == currentPrefix.length();
}
}
}
/**
* Helper class to cycle through the set of virtual files.
*
* Iteration starts at the first file and iterates through all files.
*
* When {@link VirtualFileCycler#restart()} is called iteration of all files is restarted at the
* current file.
*
* If the fill strategy indicate that the main dex file should be minimal, then the main dex file
* will not be part of the iteration.
*/
private static class VirtualFileCycler {
private Map<Integer, VirtualFile> files;
private final NamingLens namingLens;
private final FillStrategy fillStrategy;
private int nextFileId;
private Iterator<VirtualFile> allFilesCyclic;
private Iterator<VirtualFile> activeFiles;
VirtualFileCycler(Map<Integer, VirtualFile> files, NamingLens namingLens,
FillStrategy fillStrategy) {
this.files = files;
this.namingLens = namingLens;
this.fillStrategy = fillStrategy;
nextFileId = Collections.max(files.keySet()) + 1;
reset();
}
private void reset() {
allFilesCyclic = Iterators.cycle(files.values());
restart();
}
boolean hasNext() {
return activeFiles.hasNext();
}
VirtualFile next() {
VirtualFile next = activeFiles.next();
return next;
}
// Start a new iteration over all files, starting at the current one.
void restart() {
activeFiles = Iterators.limit(allFilesCyclic, files.size());
}
VirtualFile addFile() {
VirtualFile newFile = new VirtualFile(nextFileId, namingLens);
files.put(nextFileId, newFile);
nextFileId++;
reset();
return newFile;
}
}
/**
* Distributes the given classes over the files in package order.
*
* <p>The populator avoids package splits. Big packages are split into subpackages if their size
* exceeds 20% of the dex file. This populator also avoids filling files completely to cater for
* future growth.
*
* <p>The populator cycles through the files until all classes have been successfully placed and
* adds new files to the passed in map if it can't fit in the existing files.
*/
private static class PackageSplitPopulator implements Callable<Map<String, Integer>> {
/**
* Android suggests com.company.product for package names, so the components will be at level 4
*/
private static final int MINIMUM_PREFIX_LENGTH = 4;
private static final int MAXIMUM_PREFIX_LENGTH = 7;
/**
* We allow 1/MIN_FILL_FACTOR of a file to remain empty when moving to the next file, i.e., a
* rollback with less than 1/MAX_FILL_FACTOR of the total classes in a file will move to the
* next file.
*/
private static final int MIN_FILL_FACTOR = 5;
private final List<DexProgramClass> classes;
private final Map<DexProgramClass, String> originalNames;
private final Set<String> previousPrefixes;
private final DexItemFactory dexItemFactory;
private final FillStrategy fillStrategy;
private final VirtualFileCycler cycler;
PackageSplitPopulator(
Map<Integer, VirtualFile> files,
Set<DexProgramClass> classes,
Map<DexProgramClass, String> originalNames,
Set<String> previousPrefixes,
DexItemFactory dexItemFactory,
FillStrategy fillStrategy,
NamingLens namingLens) {
this.classes = new ArrayList<>(classes);
this.originalNames = originalNames;
this.previousPrefixes = previousPrefixes;
this.dexItemFactory = dexItemFactory;
this.fillStrategy = fillStrategy;
this.cycler = new VirtualFileCycler(files, namingLens, fillStrategy);
}
private String getOriginalName(DexProgramClass clazz) {
return originalNames != null ? originalNames.get(clazz) : clazz.toString();
}
@Override
public Map<String, Integer> call() throws IOException {
int prefixLength = MINIMUM_PREFIX_LENGTH;
int transactionStartIndex = 0;
int fileStartIndex = 0;
String currentPrefix = null;
Map<String, Integer> newPackageAssignments = new LinkedHashMap<>();
VirtualFile current = cycler.next();
List<DexProgramClass> nonPackageClasses = new ArrayList<>();
for (int classIndex = 0; classIndex < classes.size(); classIndex++) {
DexProgramClass clazz = classes.get(classIndex);
String originalName = getOriginalName(clazz);
if (!PackageMapPopulator.coveredByPrefix(originalName, currentPrefix)) {
if (currentPrefix != null) {
current.commitTransaction();
// Reset the cycler to again iterate over all files, starting with the current one.
cycler.restart();
assert !newPackageAssignments.containsKey(currentPrefix);
newPackageAssignments.put(currentPrefix, current.id);
// Try to reduce the prefix length if possible. Only do this on a successful commit.
prefixLength = MINIMUM_PREFIX_LENGTH - 1;
}
String newPrefix;
// Also, we need to avoid new prefixes that are a prefix of previously used prefixes, as
// otherwise we might generate an overlap that will trigger problems when reusing the
// package mapping generated here. For example, if an existing map contained
// com.android.foo.*
// but we now try to place some new subpackage
// com.android.bar.*,
// we locally could use
// com.android.*.
// However, when writing out the final package map, we get overlapping patterns
// com.android.* and com.android.foo.*.
do {
newPrefix = extractPrefixToken(++prefixLength, originalName, false);
} while (currentPrefix != null &&
(currentPrefix.startsWith(newPrefix)
|| conflictsWithPreviousPrefix(newPrefix, originalName)));
// Don't set the current prefix if we did not extract one.
if (!newPrefix.equals("")) {
currentPrefix = extractPrefixToken(prefixLength, originalName, true);
}
transactionStartIndex = classIndex;
}
if (currentPrefix != null) {
assert clazz.superType != null || clazz.type == dexItemFactory.objectType;
current.addClass(clazz);
} else {
assert clazz.superType != null;
// We don't have a package, add this to a list of classes that we will add last.
assert current.transaction.classes.isEmpty();
nonPackageClasses.add(clazz);
continue;
}
if (current.isFilledEnough(fillStrategy) || current.isFull()) {
current.abortTransaction();
// We allow for a final rollback that has at most 20% of classes in it.
// This is a somewhat random number that was empirically chosen.
if (classIndex - transactionStartIndex > (classIndex - fileStartIndex) / MIN_FILL_FACTOR
&& prefixLength < MAXIMUM_PREFIX_LENGTH) {
prefixLength++;
} else {
// Reset the state to after the last commit and cycle through files.
// The idea is that we do not increase the number of files, so it has to fit
// somewhere.
fileStartIndex = transactionStartIndex;
if (!cycler.hasNext()) {
// Special case where we simply will never be able to fit the current package into
// one dex file. This is currently the case for Strings in jumbo tests, see:
// b/33227518
if (current.transaction.getNumberOfClasses() == 0) {
for (int j = transactionStartIndex; j <= classIndex; j++) {
nonPackageClasses.add(classes.get(j));
}
transactionStartIndex = classIndex + 1;
}
// All files are filled up to the 20% mark.
cycler.addFile();
}
current = cycler.next();
}
currentPrefix = null;
// Go back to previous start index.
classIndex = transactionStartIndex - 1;
assert current != null;
}
}
current.commitTransaction();
assert !newPackageAssignments.containsKey(currentPrefix);
if (currentPrefix != null) {
newPackageAssignments.put(currentPrefix, current.id);
}
if (nonPackageClasses.size() > 0) {
addNonPackageClasses(cycler, nonPackageClasses);
}
return newPackageAssignments;
}
private void addNonPackageClasses(
VirtualFileCycler cycler, List<DexProgramClass> nonPackageClasses) {
cycler.restart();
VirtualFile current;
current = cycler.next();
for (DexProgramClass clazz : nonPackageClasses) {
if (current.isFilledEnough(fillStrategy)) {
current = getVirtualFile(cycler);
}
current.addClass(clazz);
while (current.isFull()) {
// This only happens if we have a huge class, that takes up more than 20% of a dex file.
current.abortTransaction();
current = getVirtualFile(cycler);
boolean wasEmpty = current.isEmpty();
current.addClass(clazz);
if (wasEmpty && current.isFull()) {
throw new InternalCompilerError(
"Class " + clazz.toString() + " does not fit into a single dex file.");
}
}
current.commitTransaction();
}
}
private VirtualFile getVirtualFile(VirtualFileCycler cycler) {
VirtualFile current = null;
while (cycler.hasNext()
&& (current = cycler.next()).isFilledEnough(fillStrategy)) {}
if (current == null || current.isFilledEnough(fillStrategy)) {
current = cycler.addFile();
}
return current;
}
private boolean conflictsWithPreviousPrefix(String newPrefix, String originalName) {
if (previousPrefixes == null) {
return false;
}
for (String previous : previousPrefixes) {
// Check whether a previous prefix starts with this new prefix and, if so,
// whether the new prefix already is maximal. So for example a new prefix of
// foo.bar
// would match
// foo.bar.goo.*
// However, if the original class is
// foo.bar.X
// then this prefix is the best we can do, and will not turn into a .* prefix and
// thus does not conflict.
if (previous.startsWith(newPrefix)
&& (originalName.lastIndexOf('.') > newPrefix.length())) {
return true;
}
}
return false;
}
}
}