blob: dd3184fc179fd6ee3839e4a3b8e047748b63e962 [file]
// 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.libanalyzer;
import com.android.tools.r8.BaseCompilerCommand;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.CompilationMode;
import com.android.tools.r8.D8;
import com.android.tools.r8.D8Command;
import com.android.tools.r8.R8;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.Version;
import com.android.tools.r8.blastradius.BlastRadiusKeepRuleClassifier;
import com.android.tools.r8.blastradius.RootSetBlastRadius;
import com.android.tools.r8.blastradius.RootSetBlastRadiusForRule;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
import com.android.tools.r8.libanalyzer.proto.BlockedConsumerKeepRule;
import com.android.tools.r8.libanalyzer.proto.ConfigurationSummary;
import com.android.tools.r8.libanalyzer.proto.D8CompileResult;
import com.android.tools.r8.libanalyzer.proto.ItemCollectionSummary;
import com.android.tools.r8.libanalyzer.proto.KeepRuleBlastRadiusSummary;
import com.android.tools.r8.libanalyzer.proto.LibraryAnalyzerResult;
import com.android.tools.r8.libanalyzer.proto.R8CompileResult;
import com.android.tools.r8.libanalyzer.proto.ValidateConsumerKeepRulesResult;
import com.android.tools.r8.libanalyzer.utils.DexIndexedSizeConsumer;
import com.android.tools.r8.libanalyzer.utils.LibraryAnalyzerOptions;
import com.android.tools.r8.origin.CommandLineOrigin;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.position.Position;
import com.android.tools.r8.processkeeprules.ValidateLibraryConsumerRulesKeepRuleProcessor;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.KeepInfo;
import com.android.tools.r8.shaking.ProguardConfigurationParser;
import com.android.tools.r8.shaking.ProguardConfigurationParser.ProguardConfigurationSourceParser;
import com.android.tools.r8.utils.AllEmbeddedRulesExtractor;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.ExceptionUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.IterableUtils;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.function.Predicate;
@KeepForApi
public class LibraryAnalyzer {
private final AndroidApp app;
private final LibraryAnalyzerOptions options;
private final Reporter reporter;
private LibraryAnalyzer(AndroidApp app, LibraryAnalyzerOptions options) {
this.app = app;
this.options = options;
this.reporter = options.reporter;
}
public static void main(String[] args) {
ExceptionUtils.withMainProgramHandler(() -> run(args));
}
private static void run(String[] args) throws CompilationFailedException {
LibraryAnalyzerCommand.Builder builder =
LibraryAnalyzerCommandParser.parse(args, CommandLineOrigin.INSTANCE);
run(builder.build());
}
public static void run(LibraryAnalyzerCommand command) throws CompilationFailedException {
LibraryAnalyzerOptions options = command.getInternalOptions();
ExecutorService executorService =
ThreadUtils.getExecutorService(options.threadCount, options.getThreadingModule());
try {
run(command, executorService, options);
} finally {
executorService.shutdown();
}
}
public static void run(LibraryAnalyzerCommand command, ExecutorService executorService)
throws CompilationFailedException {
run(command, executorService, command.getInternalOptions());
}
private static void run(
LibraryAnalyzerCommand command,
ExecutorService executorService,
LibraryAnalyzerOptions options)
throws CompilationFailedException {
if (command.isPrintHelp()) {
System.out.println(LibraryAnalyzerCommandParser.getUsageMessage());
return;
}
if (command.isPrintVersion()) {
System.out.println("LibraryAnalyzer " + Version.getVersionString());
return;
}
ExceptionUtils.withR8CompilationHandler(
options.reporter,
() -> new LibraryAnalyzer(command.getApp(), options).run(executorService));
}
private void run(ExecutorService executorService) {
InternalD8CompileResult d8CompileResult = runD8(executorService);
R8CompileResult r8CompileResult = runR8(executorService);
ValidateConsumerKeepRulesResult validateConsumerKeepRulesResult =
runValidateConsumerKeepRules();
writeAnalysisResult(d8CompileResult, r8CompileResult, validateConsumerKeepRulesResult);
}
private InternalD8CompileResult runD8(ExecutorService executorService) {
DexIndexedSizeConsumer sizeConsumer = new DexIndexedSizeConsumer();
D8Command.Builder commandBuilder =
D8Command.builder(reporter)
.setClassConflictResolver((reference, origins, handler) -> origins.iterator().next())
.setProgramConsumer(sizeConsumer);
configure(commandBuilder);
try {
D8.LibraryAnalyzerEntryPoint.run(
commandBuilder.build(),
executorService,
d8Options -> d8Options.libraryAnalyzerSubCompilation = true);
} catch (CompilationFailedException e) {
reporter.warning(new ExceptionDiagnostic(e));
reporter.clearAbort();
return null;
}
return new InternalD8CompileResult(sizeConsumer.size());
}
private R8CompileResult runR8(ExecutorService executorService) {
DexIndexedSizeConsumer sizeConsumer = new DexIndexedSizeConsumer();
R8Command.Builder commandBuilder =
R8Command.builder(reporter)
.addProguardConfiguration(List.of("-ignorewarnings"), Origin.unknown())
.setClassConflictResolver((reference, origins, handler) -> origins.iterator().next())
.setProgramConsumer(sizeConsumer);
configure(commandBuilder);
R8CompileResult.Builder resultBuilder = R8CompileResult.newBuilder();
try {
R8.LibraryAnalyzerEntryPoint.run(
commandBuilder.build(),
executorService,
r8Options -> {
r8Options.libraryAnalyzerSubCompilation = true;
r8Options.ignoreUnusedProguardRules = true;
if (options.blastRadiusOutputPath != null) {
r8Options.getBlastRadiusOptions().outputPath =
options.blastRadiusOutputPath.toString();
}
r8Options.getBlastRadiusOptions().blastRadiusConsumer =
(appView, appInfo, blastRadius) ->
resultBuilder
.setConfiguration(
ConfigurationSummary.newBuilder()
.addAllKeepRules(getTopBlastRadiusKeepRules(blastRadius))
.addAllUsedPackageWideKeepRules(
getPackageWideKeepRules(blastRadius, r -> !r.isEmpty()))
.addAllUnusedPackageWideKeepRules(
getPackageWideKeepRules(blastRadius, r -> r.isEmpty())))
.setClasses(
getItemCollectionSummary(
appInfo,
IterableUtils::singleton,
appInfo.getKeepInfo()::getClassInfo))
.setFields(
getItemCollectionSummary(
appInfo,
DexProgramClass::programFields,
appInfo.getKeepInfo()::getFieldInfo))
.setMethods(
getItemCollectionSummary(
appInfo,
DexProgramClass::programMethods,
appInfo.getKeepInfo()::getMethodInfo));
});
} catch (CompilationFailedException e) {
reporter.warning(new ExceptionDiagnostic(e));
reporter.clearAbort();
return null;
}
return resultBuilder.setDexSizeBytes(sizeConsumer.size()).build();
}
private ValidateConsumerKeepRulesResult runValidateConsumerKeepRules() {
ValidateConsumerKeepRulesResult.Builder resultBuilder =
ValidateConsumerKeepRulesResult.newBuilder();
try {
// TODO(b/486771488): Consider implementing this using ProcessKeepRules, similar to how runD8
// and runR8 works above.
ExceptionUtils.withCompilationHandler(
reporter, () -> internalRunValidateConsumerKeepRules(resultBuilder));
} catch (CompilationFailedException e) {
reporter.warning(new ExceptionDiagnostic(e));
reporter.clearAbort();
return null;
}
return resultBuilder.build();
}
private void internalRunValidateConsumerKeepRules(
ValidateConsumerKeepRulesResult.Builder resultBuilder) throws ResourceException {
ProguardConfigurationParser parser =
new ProguardConfigurationParser(
new DexItemFactory(),
reporter,
new ValidateLibraryConsumerRulesKeepRuleProcessor(reporter) {
@Override
protected void handleRule(
ProguardConfigurationSourceParser parser, Position position, String rule) {
resultBuilder.addBlockedKeepRules(
BlockedConsumerKeepRule.newBuilder().setSource(rule).build());
}
@Override
protected void handleKeepAttribute(
ProguardConfigurationSourceParser parser, Position position, String attribute) {
resultBuilder.addBlockedKeepRules(
BlockedConsumerKeepRule.newBuilder()
.setSource("-keepattributes " + attribute)
.build());
}
});
for (var programResourceProvider : app.getProgramResourceProviders()) {
var dataResourceProvider = programResourceProvider.getDataResourceProvider();
if (dataResourceProvider != null) {
new AllEmbeddedRulesExtractor(dataResourceProvider, reporter)
.readSources()
.parseAllRules(parser);
}
}
}
private static <T> ItemCollectionSummary getItemCollectionSummary(
AppInfoWithLiveness appInfo, Function<DexProgramClass, Iterable<T>> getItems,
Function<T, KeepInfo<?, ?>> getKeepInfo) {
int itemCount = 0;
int keptItemCount = 0;
int noObfuscationCount = 0;
int noOptimizationCount = 0;
int noShrinkingCount = 0;
InternalOptions options = appInfo.options();
for (DexProgramClass clazz : appInfo.classes()) {
for (T item : getItems.apply(clazz)) {
KeepInfo<?, ?> keepInfo = getKeepInfo.apply(item);
itemCount++;
boolean isKept = false;
if (!keepInfo.isMinificationAllowed(options)) {
noObfuscationCount++;
isKept = true;
}
if (!keepInfo.isOptimizationAllowed(options)) {
noOptimizationCount++;
isKept = true;
}
if (!keepInfo.isShrinkingAllowed(options)) {
noShrinkingCount++;
isKept = true;
}
if (isKept) {
keptItemCount++;
}
}
}
return ItemCollectionSummary.newBuilder()
.setItemCount(itemCount)
.setKeptItemCount(keptItemCount)
.setNoObfuscationCount(noObfuscationCount)
.setNoOptimizationCount(noOptimizationCount)
.setNoShrinkingCount(noShrinkingCount)
.build();
}
private static List<KeepRuleBlastRadiusSummary> getTopBlastRadiusKeepRules(
RootSetBlastRadius blastRadius) {
ArrayList<RootSetBlastRadiusForRule> keepRulesSorted =
ListUtils.sort(
blastRadius.getBlastRadius(),
(x, y) -> {
if (x.getNumberOfItems() != y.getNumberOfItems()) {
return y.getNumberOfItems() - x.getNumberOfItems();
}
// TODO(b/441055269): Sorting by source is not guaranteed to be
// deterministic.
return x.getSource().compareTo(y.getSource());
});
while (keepRulesSorted.size() >= 5) {
ListUtils.removeLast(keepRulesSorted);
}
while (!keepRulesSorted.isEmpty() && ListUtils.last(keepRulesSorted).isEmpty()) {
ListUtils.removeLast(keepRulesSorted);
}
return ListUtils.map(
keepRulesSorted,
rule ->
KeepRuleBlastRadiusSummary.newBuilder()
.setSource(rule.getSource())
.setKeptItemCount(rule.getNumberOfItems())
.build());
}
private static List<KeepRuleBlastRadiusSummary> getPackageWideKeepRules(
RootSetBlastRadius blastRadius, Predicate<RootSetBlastRadiusForRule> predicate) {
List<RootSetBlastRadiusForRule> unusedPackageWideKeepRules =
ListUtils.filter(
blastRadius.getBlastRadius(),
rule ->
BlastRadiusKeepRuleClassifier.isPackageWideKeepRule(rule.getRule())
&& predicate.test(rule));
List<RootSetBlastRadiusForRule> unusedPackageWideKeepRulesSorted =
ListUtils.sort(
unusedPackageWideKeepRules,
(x, y) -> {
if (x.getNumberOfItems() != y.getNumberOfItems()) {
return y.getNumberOfItems() - x.getNumberOfItems();
}
// TODO(b/441055269): Sorting by source is not guaranteed to be
// deterministic.
return x.getSource().compareTo(y.getSource());
});
return ListUtils.map(
unusedPackageWideKeepRulesSorted,
rule ->
KeepRuleBlastRadiusSummary.newBuilder()
.setSource(rule.getSource())
.setKeptItemCount(rule.getNumberOfItems())
.setNoObfuscation(rule.isNoObfuscationSet())
.setNoOptimization(rule.isNoOptimizationSet())
.setNoShrinking(rule.isNoShrinkingSet())
.build());
}
private void writeAnalysisResult(
InternalD8CompileResult d8CompileResult,
R8CompileResult r8CompileResult,
ValidateConsumerKeepRulesResult validateConsumerKeepRulesResult) {
LibraryAnalyzerResult.Builder resultBuilder = LibraryAnalyzerResult.newBuilder();
if (d8CompileResult != null) {
resultBuilder.setD8CompileResult(
D8CompileResult.newBuilder().setDexSizeBytes(d8CompileResult.size).build());
}
if (r8CompileResult != null) {
resultBuilder.setR8CompileResult(r8CompileResult);
}
if (validateConsumerKeepRulesResult != null) {
resultBuilder.setValidateConsumerKeepRulesResult(validateConsumerKeepRulesResult);
}
LibraryAnalyzerResult result = resultBuilder.build();
if (options.outputConsumer != null) {
options.outputConsumer.accept(result);
}
}
private void configure(BaseCompilerCommand.Builder<?, ?> commandBuilder) {
commandBuilder
.setMode(CompilationMode.RELEASE)
.setMinApiLevel(options.minApiLevel.getLevel(), options.minApiLevel.getMinor());
app.getProgramResourceProviders().forEach(commandBuilder::addProgramResourceProvider);
app.getClasspathResourceProviders().forEach(commandBuilder::addClasspathResourceProvider);
app.getLibraryResourceProviders().forEach(commandBuilder::addLibraryResourceProvider);
}
private abstract static class CompileResult {
final int size;
CompileResult(int size) {
this.size = size;
}
}
private static class InternalD8CompileResult extends CompileResult {
InternalD8CompileResult(int size) {
super(size);
}
}
}