| // Copyright (c) 2026, 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.distribution; | |
| import static com.google.common.base.Predicates.alwaysTrue; | |
| import static com.google.common.base.Predicates.not; | |
| import com.android.tools.r8.dex.IndexedItemCollection; | |
| import com.android.tools.r8.dex.VirtualFile; | |
| import com.android.tools.r8.graph.AppView; | |
| 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.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.ir.conversion.LensCodeRewriterUtils; | |
| import com.android.tools.r8.utils.ThreadUtils; | |
| import com.android.tools.r8.utils.internal.ListUtils; | |
| import com.android.tools.r8.utils.internal.SetUtils; | |
| import com.android.tools.r8.utils.internal.collections.Pair; | |
| import com.android.tools.r8.utils.timing.Timing; | |
| import com.google.common.base.Predicate; | |
| import it.unimi.dsi.fastutil.objects.Reference2IntMap; | |
| import java.util.Comparator; | |
| import java.util.IdentityHashMap; | |
| import java.util.Iterator; | |
| import java.util.LinkedHashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.PriorityQueue; | |
| import java.util.Set; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.ExecutorService; | |
| public class DexDistributionRefinement { | |
| private final AppView<?> appView; | |
| private final VirtualFileCycler cycler; | |
| private final boolean enableContainerDex; | |
| private final LinkedHashSet<VirtualFile> files; | |
| private final LensCodeRewriterUtils rewriter; | |
| // Must be concurrent since we collect concurrently. | |
| private final Map<String, DexString> shortyCache = new ConcurrentHashMap<>(); | |
| // Mapping from each virtual file to the classes inside the virtual file that are candidates for | |
| // moving to other virtual files. A LinkedHashSet is used instead of a List to support efficient | |
| // removal. | |
| private final Map<VirtualFile, LinkedHashSet<DexProgramClass>> | |
| fileToClassesWithDeterministicOrder = new IdentityHashMap<>(); | |
| private DexDistributionRefinement( | |
| AppView<?> appView, VirtualFileCycler cycler, List<VirtualFile> filesSubjectToRefinement) { | |
| this.appView = appView; | |
| this.cycler = cycler; | |
| this.enableContainerDex = appView.options().enableContainerDex(); | |
| this.files = new LinkedHashSet<>(filesSubjectToRefinement); | |
| this.rewriter = new LensCodeRewriterUtils(appView, true); | |
| initialize(); | |
| } | |
| public static void run( | |
| AppView<?> appView, VirtualFileCycler cycler, ExecutorService executorService, Timing timing) | |
| throws ExecutionException { | |
| if (!appView.testing().enableClassToDexDistributionRefinementInDebugMode | |
| && appView.options().debug) { | |
| return; | |
| } | |
| int numPasses = appView.testing().classToDexDistributionRefinementPasses; | |
| if (numPasses > 0) { | |
| runOnPartition(appView, cycler, VirtualFile::isStartup, executorService, timing); | |
| runOnPartition(appView, cycler, not(VirtualFile::isStartup), executorService, timing); | |
| } | |
| } | |
| private static void runOnPartition( | |
| AppView<?> appView, | |
| VirtualFileCycler cycler, | |
| Predicate<VirtualFile> predicate, | |
| ExecutorService executorService, | |
| Timing timing) | |
| throws ExecutionException { | |
| List<VirtualFile> filesSubjectToRefinement = | |
| ListUtils.filter(cycler.getFilesForDistribution(), f -> !f.isEmpty() && predicate.test(f)); | |
| if (filesSubjectToRefinement.size() > 1) { | |
| timing.begin("Dex distribution refinement"); | |
| new DexDistributionRefinement(appView, cycler, filesSubjectToRefinement) | |
| .internalRun(executorService, timing); | |
| timing.end(); | |
| } | |
| } | |
| private void initialize() { | |
| for (VirtualFile file : files) { | |
| fileToClassesWithDeterministicOrder.put( | |
| file, | |
| new LinkedHashSet<>( | |
| ListUtils.sort(file.classes(), Comparator.comparing(DexClass::getType)))); | |
| } | |
| } | |
| private void internalRun(ExecutorService executorService, Timing timing) | |
| throws ExecutionException { | |
| // Run refinement. | |
| boolean hasEmptyFiles = false; | |
| for (int i = 0; i < appView.testing().classToDexDistributionRefinementPasses; i++) { | |
| boolean changed = false; | |
| timing.begin("Pass " + i); | |
| Iterator<VirtualFile> iterator = files.iterator(); | |
| while (iterator.hasNext()) { | |
| VirtualFile file = iterator.next(); | |
| if (refineFile(file, executorService, timing)) { | |
| changed = true; | |
| } | |
| // If the file became empty, then don't consider it for any further refinement. | |
| if (file.getIndexedItems().classes.isEmpty()) { | |
| iterator.remove(); | |
| hasEmptyFiles = true; | |
| } | |
| } | |
| timing.end(); | |
| if (!changed) { | |
| break; | |
| } | |
| } | |
| // Fixup empty files. | |
| if (hasEmptyFiles) { | |
| cycler.removeEmptyFilesAndRenumber(); | |
| } | |
| } | |
| private boolean refineFile(VirtualFile file, ExecutorService executorService, Timing timing) | |
| throws ExecutionException { | |
| timing.begin("Refine file " + file.getId()); | |
| boolean changed = false; | |
| LinkedHashSet<DexProgramClass> classesWithDeterministicOrder = | |
| fileToClassesWithDeterministicOrder.get(file); | |
| // Concurrently compute which classes to move where. | |
| timing.begin("Compute target files"); | |
| Map<DexProgramClass, PendingMoveTask> pendingMoveTasks = new ConcurrentHashMap<>(); | |
| ThreadUtils.processItemsThatMatches( | |
| classesWithDeterministicOrder, | |
| alwaysTrue(), | |
| (clazz, threadTiming) -> { | |
| Set<DexItem> items = collectItems(clazz); | |
| PriorityQueue<Pair<VirtualFile, Integer>> targetFiles = findTargetFiles(file, items); | |
| if (!targetFiles.isEmpty()) { | |
| pendingMoveTasks.put(clazz, c -> moveClass(c, file, targetFiles, items)); | |
| } | |
| }, | |
| appView.options(), | |
| executorService, | |
| timing, | |
| timing.beginMerger("Fork", executorService), | |
| (index, clazz) -> clazz.getTypeName()); | |
| timing.end(); | |
| // Move all classes single threaded. | |
| timing.begin("Move classes to target files"); | |
| Iterator<DexProgramClass> iterator = classesWithDeterministicOrder.iterator(); | |
| while (iterator.hasNext()) { | |
| DexProgramClass clazz = iterator.next(); | |
| PendingMoveTask pendingMoveTask = pendingMoveTasks.get(clazz); | |
| if (pendingMoveTask == null) { | |
| continue; | |
| } | |
| boolean moved = pendingMoveTask.tryMove(clazz); | |
| if (moved) { | |
| changed = true; | |
| // The current virtual file no longer contains the class, so remove it. | |
| // We concluded that moving the current class C from DEX file X to Y was a good idea. | |
| // Let's not consider moving class C from Y to another DEX file in the future, which is | |
| // achieved by not adding the class to the target's set of classes. | |
| iterator.remove(); | |
| } | |
| } | |
| timing.end(); // Move classes to target files | |
| timing.end(); // Refine file | |
| return changed; | |
| } | |
| private Set<DexItem> collectItems(DexProgramClass clazz) { | |
| ItemCollector collector = new ItemCollector(); | |
| clazz.collectIndexedItems(appView, collector, rewriter); | |
| return collector.getItems(); | |
| } | |
| private PriorityQueue<Pair<VirtualFile, Integer>> findTargetFiles( | |
| VirtualFile sourceFile, Set<DexItem> items) { | |
| int estimatedSavingsFromRemovalInBytes = | |
| getNumberOfItemsWithReferenceCount(items, sourceFile, 1); | |
| // TODO(b/473427453): To improve build speed, consider if we can return null here if if the | |
| // estimated savings are small compared to the number of items in the class (i.e., the class | |
| // already fits well in the current dex file). | |
| PriorityQueue<Pair<VirtualFile, Integer>> targetFiles = | |
| new PriorityQueue<>(Comparator.comparingInt(Pair::getSecond)); | |
| for (VirtualFile targetFile : files) { | |
| if (targetFile == sourceFile || cannotFit(targetFile, items)) { | |
| continue; | |
| } | |
| int estimatedCostInBytes = getNumberOfItemsWithReferenceCount(items, targetFile, 0); | |
| if (estimatedCostInBytes < estimatedSavingsFromRemovalInBytes) { | |
| targetFiles.add(new Pair<>(targetFile, estimatedCostInBytes)); | |
| } | |
| } | |
| return targetFiles; | |
| } | |
| private int getNumberOfItemsWithReferenceCount( | |
| Set<DexItem> items, VirtualFile targetFile, int theReferenceCount) { | |
| int result = 0; | |
| for (DexItem item : items) { | |
| if (enableContainerDex && item instanceof DexString) { | |
| // The container has a single DEX file, so don't include the cost/savings from moving | |
| // strings from one dex file to another. | |
| continue; | |
| } | |
| if (getReferenceCount(item, targetFile) == theReferenceCount) { | |
| result++; | |
| } | |
| } | |
| return result; | |
| } | |
| private int getReferenceCount(DexItem item, VirtualFile file) { | |
| if (item instanceof DexCallSite) { | |
| return file.indexedItems.callSites.getInt(item); | |
| } else if (item instanceof DexField) { | |
| return file.indexedItems.fields.getInt(item); | |
| } else if (item instanceof DexMethod) { | |
| return file.indexedItems.methods.getInt(item); | |
| } else if (item instanceof DexMethodHandle) { | |
| return file.indexedItems.methodHandles.getInt(item); | |
| } else if (item instanceof DexProto) { | |
| return file.indexedItems.protos.getInt(item); | |
| } else if (item instanceof DexString) { | |
| return file.indexedItems.strings.getInt(item); | |
| } else if (item instanceof DexType) { | |
| return file.indexedItems.types.getInt(item); | |
| } | |
| assert false; | |
| return 0; | |
| } | |
| private boolean cannotFit(VirtualFile file, Set<DexItem> items) { | |
| int newFields = 0; | |
| int newMethods = 0; | |
| int newTypes = 0; | |
| for (DexItem item : items) { | |
| if (getReferenceCount(item, file) == 0) { | |
| if (item instanceof DexField) { | |
| newFields++; | |
| } else if (item instanceof DexMethod) { | |
| newMethods++; | |
| } else if (item instanceof DexType) { | |
| newTypes++; | |
| } | |
| } | |
| } | |
| return file.getTransaction().getNumberOfFields() + newFields > file.getMaxNumberOfFields() | |
| || file.getTransaction().getNumberOfMethods() + newMethods > VirtualFile.MAX_ENTRIES | |
| || file.getTransaction().getNumberOfTypes() + newTypes > file.getMaxNumberOfTypes(); | |
| } | |
| private boolean moveClass( | |
| DexProgramClass clazz, | |
| VirtualFile sourceFile, | |
| PriorityQueue<Pair<VirtualFile, Integer>> targetFiles, | |
| Set<DexItem> items) { | |
| while (!targetFiles.isEmpty()) { | |
| VirtualFile targetFile = targetFiles.poll().getFirst(); | |
| if (cannotFit(targetFile, items)) { | |
| continue; | |
| } | |
| sourceFile.indexedItems.classes.remove(clazz); | |
| targetFile.indexedItems.classes.add(clazz); | |
| for (DexItem item : items) { | |
| adjustReferenceCount(sourceFile, item, -1); | |
| adjustReferenceCount(targetFile, item, 1); | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| private void adjustReferenceCount(VirtualFile file, DexItem item, int change) { | |
| if (item instanceof DexCallSite) { | |
| adjustReferenceCount(file.indexedItems.callSites, (DexCallSite) item, change); | |
| } else if (item instanceof DexField) { | |
| adjustReferenceCount(file.indexedItems.fields, (DexField) item, change); | |
| } else if (item instanceof DexMethod) { | |
| adjustReferenceCount(file.indexedItems.methods, (DexMethod) item, change); | |
| } else if (item instanceof DexMethodHandle) { | |
| adjustReferenceCount(file.indexedItems.methodHandles, (DexMethodHandle) item, change); | |
| } else if (item instanceof DexProto) { | |
| adjustReferenceCount(file.indexedItems.protos, (DexProto) item, change); | |
| } else if (item instanceof DexString) { | |
| adjustReferenceCount(file.indexedItems.strings, (DexString) item, change); | |
| } else if (item instanceof DexType) { | |
| adjustReferenceCount(file.indexedItems.types, (DexType) item, change); | |
| } else { | |
| assert false; | |
| } | |
| } | |
| private <T> void adjustReferenceCount(Reference2IntMap<T> items, T item, int change) { | |
| int count = items.containsKey(item) ? items.getInt(item) : 0; | |
| if (count == IndexedItemTransaction.NO_REF_COUNT) { | |
| // Checksum or marker. | |
| return; | |
| } | |
| int newCount = count + change; | |
| assert newCount >= 0; | |
| if (newCount == 0) { | |
| items.removeInt(item); | |
| } else { | |
| items.put(item, newCount); | |
| } | |
| } | |
| private class ItemCollector implements IndexedItemCollection { | |
| private final Set<DexItem> items = SetUtils.newIdentityHashSet(); | |
| @Override | |
| public boolean addClass(DexProgramClass clazz) { | |
| return true; | |
| } | |
| @Override | |
| public boolean addField(DexField field) { | |
| return items.add(field); | |
| } | |
| @Override | |
| public boolean addMethod(DexMethod method) { | |
| return items.add(method); | |
| } | |
| @Override | |
| public boolean addString(DexString string) { | |
| return items.add(string); | |
| } | |
| @Override | |
| public boolean addProto(DexProto proto) { | |
| if (items.add(proto)) { | |
| DexString shorty = | |
| shortyCache.computeIfAbsent( | |
| proto.createShortyString(), appView.dexItemFactory()::createString); | |
| addString(shorty); | |
| return true; | |
| } | |
| return false; | |
| } | |
| @Override | |
| public boolean addType(DexType type) { | |
| return items.add(type); | |
| } | |
| @Override | |
| public boolean addCallSite(DexCallSite callSite) { | |
| return items.add(callSite); | |
| } | |
| @Override | |
| public boolean addMethodHandle(DexMethodHandle methodHandle) { | |
| return items.add(methodHandle); | |
| } | |
| public Set<DexItem> getItems() { | |
| return items; | |
| } | |
| } | |
| private interface PendingMoveTask { | |
| boolean tryMove(DexProgramClass clazz); | |
| } | |
| } |