| // 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.blastradius; |
| |
| import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull; |
| |
| import com.android.tools.r8.blastradius.proto.BlastRadius; |
| import com.android.tools.r8.blastradius.proto.BlastRadiusContainer; |
| import com.android.tools.r8.blastradius.proto.BuildInfo; |
| import com.android.tools.r8.blastradius.proto.FieldReference; |
| import com.android.tools.r8.blastradius.proto.FileOrigin; |
| import com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius; |
| import com.android.tools.r8.blastradius.proto.KeepConstraint; |
| import com.android.tools.r8.blastradius.proto.KeepConstraints; |
| import com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius; |
| import com.android.tools.r8.blastradius.proto.KeepRuleTag; |
| import com.android.tools.r8.blastradius.proto.KeptClassInfo; |
| import com.android.tools.r8.blastradius.proto.KeptFieldInfo; |
| import com.android.tools.r8.blastradius.proto.KeptMethodInfo; |
| import com.android.tools.r8.blastradius.proto.MavenCoordinate; |
| import com.android.tools.r8.blastradius.proto.MethodReference; |
| import com.android.tools.r8.blastradius.proto.ProtoReference; |
| import com.android.tools.r8.blastradius.proto.TextFileOrigin; |
| import com.android.tools.r8.blastradius.proto.TypeReference; |
| import com.android.tools.r8.blastradius.proto.TypeReferenceList; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexField; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.DexProto; |
| import com.android.tools.r8.graph.DexReference; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.DexTypeList; |
| import com.android.tools.r8.origin.MavenOrigin; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.origin.PathOrigin; |
| import com.android.tools.r8.position.Position; |
| import com.android.tools.r8.position.TextRange; |
| import com.android.tools.r8.shaking.AppInfoWithLiveness; |
| import com.android.tools.r8.shaking.EnqueuerResult; |
| import com.android.tools.r8.shaking.GlobalConfigurationRule; |
| import com.android.tools.r8.shaking.ProguardConfiguration; |
| import com.android.tools.r8.shaking.ProguardKeepRuleBase; |
| import com.android.tools.r8.shaking.ProguardKeepRuleModifiers; |
| import com.android.tools.r8.utils.ArrayUtils; |
| import com.android.tools.r8.utils.OriginUtils; |
| import com.google.common.base.Equivalence; |
| import com.google.common.base.Equivalence.Wrapper; |
| import com.google.common.collect.Iterables; |
| import it.unimi.dsi.fastutil.objects.Reference2IntMap; |
| import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| |
| public class RootSetBlastRadiusSerializer { |
| |
| private final AppView<?> appView; |
| private final AppInfoWithLiveness appInfo; |
| |
| private final BlastRadiusContainer.Builder container = BlastRadiusContainer.newBuilder(); |
| |
| // Ids. |
| private final Reference2IntMap<RootSetBlastRadiusForRule> ruleIds = |
| new Reference2IntOpenHashMap<>(); |
| |
| // Origins. |
| private final Map<Origin, FileOrigin> origins = new HashMap<>(); |
| |
| // References. Intentionally using HashMap for DexTypeList since it is not canonicalized. |
| private final Map<DexField, FieldReference> fieldReferences = new IdentityHashMap<>(); |
| private final Map<DexMethod, MethodReference> methodReferences = new IdentityHashMap<>(); |
| private final Map<DexProto, ProtoReference> protoReferences = new IdentityHashMap<>(); |
| private final Map<DexType, TypeReference> typeReferences = new IdentityHashMap<>(); |
| private final Map<DexTypeList, TypeReferenceList> typeReferenceLists = new HashMap<>(); |
| |
| // Kept items. LinkedHashMap for deterministic output when writing to the proto container. |
| private final Map<DexType, KeptClassInfo.Builder> keptClassInfos = new LinkedHashMap<>(); |
| private final Map<DexField, KeptFieldInfo.Builder> keptFieldInfos = new LinkedHashMap<>(); |
| private final Map<DexMethod, KeptMethodInfo.Builder> keptMethodInfos = new LinkedHashMap<>(); |
| |
| private final Map<Wrapper<KeepConstraints>, KeepConstraints> keepConstraints = new HashMap<>(); |
| |
| RootSetBlastRadiusSerializer(AppView<?> appView, EnqueuerResult enqueuerResult) { |
| this.appView = appView; |
| this.appInfo = enqueuerResult.getAppInfo(); |
| } |
| |
| public BlastRadiusContainer serialize( |
| RootSetBlastRadius blastRadius, BlastRadiusOptions options) { |
| Collection<RootSetBlastRadiusForRule> sortedBlastRadius = |
| blastRadius.getBlastRadiusWithDeterministicOrder(); |
| Map<RootSetBlastRadiusForRule, Collection<RootSetBlastRadiusForRule>> subsumedByInfo = |
| blastRadius.getSubsumedByInfo(options); |
| for (RootSetBlastRadiusForRule blastRadiusForRule : sortedBlastRadius) { |
| ruleIds.put(blastRadiusForRule, ruleIds.size()); |
| } |
| for (RootSetBlastRadiusForRule blastRadiusForRule : sortedBlastRadius) { |
| int ruleId = ruleIds.getInt(blastRadiusForRule); |
| KeepRuleBlastRadius.Builder ruleProto = |
| KeepRuleBlastRadius.newBuilder() |
| .setId(ruleId) |
| .setBlastRadius(serializeBlastRadius(blastRadiusForRule, ruleId, subsumedByInfo)) |
| .setConstraintsId(serializeConstraints(blastRadiusForRule).getId()) |
| .setOrigin(serializeTextFileOrigin(blastRadiusForRule.getRule())) |
| .setSource(blastRadiusForRule.getSource()); |
| if (BlastRadiusKeepRuleClassifier.isPackageWideKeepRule(blastRadiusForRule.getRule())) { |
| ruleProto.addTags(KeepRuleTag.PACKAGE_WIDE); |
| } |
| container.addKeepRuleBlastRadiusTable(ruleProto); |
| } |
| serializeGlobalKeepRuleBlastRadii(ruleIds.size()); |
| keptClassInfos.values().forEach(container::addKeptClassInfoTable); |
| keptFieldInfos.values().forEach(container::addKeptFieldInfoTable); |
| keptMethodInfos.values().forEach(container::addKeptMethodInfoTable); |
| BlastRadiusContainer result = container.setBuildInfo(serializeBuildInfo()).build(); |
| assert validate(result); |
| return result; |
| } |
| |
| private BlastRadius serializeBlastRadius( |
| RootSetBlastRadiusForRule blastRadiusForRule, |
| int ruleId, |
| Map<RootSetBlastRadiusForRule, Collection<RootSetBlastRadiusForRule>> subsumedByInfo) { |
| BlastRadius.Builder blastRadius = BlastRadius.newBuilder(); |
| // Populate subsumed by. |
| Collection<RootSetBlastRadiusForRule> dominators = subsumedByInfo.get(blastRadiusForRule); |
| if (dominators != null) { |
| Iterator<RootSetBlastRadiusForRule> iterator = dominators.iterator(); |
| int[] dominatorIds = |
| ArrayUtils.initialize(new int[dominators.size()], i -> ruleIds.getInt(iterator.next())); |
| Arrays.sort(dominatorIds); |
| for (int dominatorId : dominatorIds) { |
| blastRadius.addSubsumedBy(dominatorId); |
| } |
| } |
| // Populate blast radius. |
| for (DexType matchedClass : blastRadiusForRule.getMatchedClassesWithDeterministicOrder()) { |
| KeptClassInfo.Builder keptClassInfo = |
| keptClassInfos.computeIfAbsent( |
| matchedClass, |
| k -> |
| KeptClassInfo.newBuilder() |
| .setId(keptClassInfos.size()) |
| .setClassReferenceId(serializeTypeReference(k).getId()) |
| .setFileOriginId(serializeOrigin(k).getId())); |
| blastRadius.addClassBlastRadius(keptClassInfo.getId()); |
| keptClassInfo.addKeptBy(ruleId); |
| } |
| for (DexField matchedField : blastRadiusForRule.getMatchedFieldsWithDeterministicOrder()) { |
| KeptFieldInfo.Builder keptFieldInfo = |
| keptFieldInfos.computeIfAbsent( |
| matchedField, |
| k -> |
| KeptFieldInfo.newBuilder() |
| .setId(keptFieldInfos.size()) |
| .setFieldReferenceId(serializeFieldReference(k).getId()) |
| .setFileOriginId(serializeOrigin(k).getId())); |
| blastRadius.addFieldBlastRadius(keptFieldInfo.getId()); |
| keptFieldInfo.addKeptBy(ruleId); |
| } |
| for (DexMethod matchedMethod : blastRadiusForRule.getMatchedMethodsWithDeterministicOrder()) { |
| KeptMethodInfo.Builder keptMethodInfo = |
| keptMethodInfos.computeIfAbsent( |
| matchedMethod, |
| k -> |
| KeptMethodInfo.newBuilder() |
| .setId(keptMethodInfos.size()) |
| .setMethodReferenceId(serializeMethodReference(k).getId()) |
| .setFileOriginId(serializeOrigin(k).getId())); |
| blastRadius.addMethodBlastRadius(keptMethodInfo.getId()); |
| keptMethodInfo.addKeptBy(ruleId); |
| } |
| return blastRadius.build(); |
| } |
| |
| private BuildInfo serializeBuildInfo() { |
| int classCount = 0, fieldCount = 0, methodCount = 0; |
| int liveClassCount = 0, liveFieldCount = 0, liveMethodCount = 0; |
| for (DexProgramClass clazz : appInfo.classes()) { |
| classCount++; |
| fieldCount += clazz.getFieldCollection().size(); |
| methodCount += clazz.getMethodCollection().size(); |
| if (appInfo.isLiveProgramClass(clazz)) { |
| liveClassCount++; |
| liveFieldCount += Iterables.size(clazz.fields(appInfo::isReachableOrReferencedField)); |
| liveMethodCount += Iterables.size(clazz.methods(appInfo::isLiveOrTargetedMethod)); |
| } |
| } |
| return BuildInfo.newBuilder() |
| .setClassCount(classCount) |
| .setFieldCount(fieldCount) |
| .setMethodCount(methodCount) |
| .setLiveClassCount(liveClassCount) |
| .setLiveFieldCount(liveFieldCount) |
| .setLiveMethodCount(liveMethodCount) |
| .build(); |
| } |
| |
| private KeepConstraints serializeConstraints(RootSetBlastRadiusForRule blastRadiusForRule) { |
| KeepConstraints.Builder builder = KeepConstraints.newBuilder().setId(keepConstraints.size()); |
| ProguardKeepRuleModifiers modifiers = blastRadiusForRule.getRule().getModifiers(); |
| if (!modifiers.allowsObfuscation) { |
| builder.addConstraints(KeepConstraint.DONT_OBFUSCATE); |
| } |
| if (!modifiers.allowsOptimization) { |
| builder.addConstraints(KeepConstraint.DONT_OPTIMIZE); |
| } |
| if (!modifiers.allowsShrinking) { |
| builder.addConstraints(KeepConstraint.DONT_SHRINK); |
| } |
| KeepConstraints constraints = builder.build(); |
| Wrapper<KeepConstraints> wrapper = KeepConstraintsEquivalence.doWrap(constraints); |
| KeepConstraints previous = keepConstraints.putIfAbsent(wrapper, constraints); |
| if (previous != null) { |
| return previous; |
| } |
| container.addKeepConstraintsTable(constraints); |
| return constraints; |
| } |
| |
| private FieldReference serializeFieldReference(DexField field) { |
| return fieldReferences.computeIfAbsent( |
| field, |
| k -> { |
| FieldReference fieldReference = |
| FieldReference.newBuilder() |
| .setId(fieldReferences.size()) |
| .setClassReferenceId(serializeTypeReference(field.getHolderType()).getId()) |
| .setTypeReferenceId(serializeTypeReference(field.getType()).getId()) |
| .setName(field.getName().toString()) |
| .build(); |
| container.addFieldReferenceTable(fieldReference); |
| return fieldReference; |
| }); |
| } |
| |
| private void serializeGlobalKeepRuleBlastRadii(int nextRuleId) { |
| ProguardConfiguration proguardConfiguration = appView.options().getProguardConfiguration(); |
| if (proguardConfiguration == null) { |
| return; |
| } |
| // The iteration over the global rules should have deterministic iteration order since the |
| // global rules lists are populated in-order by the ProguardConfigurationParser. |
| for (GlobalConfigurationRule rule : proguardConfiguration.getGlobalRules()) { |
| container.addGlobalKeepRuleBlastRadiusTable( |
| GlobalKeepRuleBlastRadius.newBuilder() |
| .setId(nextRuleId++) |
| .setOrigin(serializeTextFileOrigin(rule)) |
| .setSource(rule.getSource()) |
| .build()); |
| } |
| } |
| |
| private MethodReference serializeMethodReference(DexMethod method) { |
| return methodReferences.computeIfAbsent( |
| method, |
| k -> { |
| MethodReference methodReference = |
| MethodReference.newBuilder() |
| .setId(methodReferences.size()) |
| .setClassReferenceId(serializeTypeReference(method.getHolderType()).getId()) |
| .setProtoReferenceId(serializeProtoReference(method.getProto()).getId()) |
| .setName(method.getName().toString()) |
| .build(); |
| container.addMethodReferenceTable(methodReference); |
| return methodReference; |
| }); |
| } |
| |
| private FileOrigin serializeOrigin(DexReference reference) { |
| DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(reference.getContextType())); |
| assert clazz != null; |
| return serializeOrigin(clazz.getOrigin()); |
| } |
| |
| private FileOrigin serializeOrigin(Origin origin) { |
| return origins.computeIfAbsent( |
| origin, |
| o -> { |
| // TODO(b/441055269): Set the filename correctly. |
| FileOrigin.Builder fileOriginBuilder = FileOrigin.newBuilder().setId(origins.size()); |
| if (o instanceof PathOrigin) { |
| fileOriginBuilder.setFilename(((PathOrigin) o).getPath().toString()); |
| } else { |
| fileOriginBuilder.setFilename(o.toString()); |
| } |
| MavenOrigin mavenOrigin = OriginUtils.getMavenOrigin(origin); |
| if (mavenOrigin != null) { |
| fileOriginBuilder.setMavenCoordinate( |
| MavenCoordinate.newBuilder() |
| .setArtifactId(mavenOrigin.getModule()) |
| .setGroupId(mavenOrigin.getGroup()) |
| .setVersion(mavenOrigin.getVersion()) |
| .build()); |
| } |
| FileOrigin fileOrigin = fileOriginBuilder.build(); |
| container.addFileOriginTable(fileOrigin); |
| return fileOrigin; |
| }); |
| } |
| |
| private TextFileOrigin serializeTextFileOrigin(GlobalConfigurationRule rule) { |
| return serializeTextFileOrigin(rule.getOrigin(), rule.getPosition()); |
| } |
| |
| private TextFileOrigin serializeTextFileOrigin(ProguardKeepRuleBase rule) { |
| return serializeTextFileOrigin(rule.getOrigin(), rule.getPosition()); |
| } |
| |
| private TextFileOrigin serializeTextFileOrigin(Origin origin, Position position) { |
| int line, column; |
| if (position instanceof TextRange) { |
| TextRange textRange = (TextRange) position; |
| line = textRange.getStart().getLine(); |
| column = textRange.getStart().getColumn(); |
| } else { |
| line = -1; |
| column = -1; |
| } |
| return TextFileOrigin.newBuilder() |
| .setFileOriginId(serializeOrigin(origin).getId()) |
| .setLineNumber(line) |
| .setColumnNumber(column) |
| .build(); |
| } |
| |
| private ProtoReference serializeProtoReference(DexProto proto) { |
| return protoReferences.computeIfAbsent( |
| proto, |
| k -> { |
| ProtoReference protoReference = |
| ProtoReference.newBuilder() |
| .setId(protoReferences.size()) |
| .setParametersId(serializeTypeReferenceList(proto.getParameters()).getId()) |
| .setReturnTypeId(serializeTypeReference(proto.getReturnType()).getId()) |
| .build(); |
| container.addProtoReferenceTable(protoReference); |
| return protoReference; |
| }); |
| } |
| |
| private TypeReference serializeTypeReference(DexType type) { |
| return typeReferences.computeIfAbsent( |
| type, |
| k -> { |
| TypeReference typeReference = |
| TypeReference.newBuilder() |
| .setId(typeReferences.size()) |
| .setJavaDescriptor(type.toDescriptorString()) |
| .build(); |
| container.addTypeReferenceTable(typeReference); |
| return typeReference; |
| }); |
| } |
| |
| private TypeReferenceList serializeTypeReferenceList(DexTypeList types) { |
| return typeReferenceLists.computeIfAbsent( |
| types, |
| k -> { |
| TypeReferenceList.Builder builder = |
| TypeReferenceList.newBuilder().setId(typeReferenceLists.size()); |
| for (DexType type : types) { |
| builder.addTypeReferenceIds(serializeTypeReference(type).getId()); |
| } |
| TypeReferenceList typeReferenceList = builder.build(); |
| container.addTypeReferenceListTable(typeReferenceList); |
| return typeReferenceList; |
| }); |
| } |
| |
| @SuppressWarnings("UnusedVariable") |
| private boolean validate(BlastRadiusContainer container) { |
| // TODO(b/441055269): Check that ids of ClassFileInJarOrigin and FileOrigin are non-overlapping. |
| // TODO(b/441055269): Check that ids of GlobalKeepRuleBlastRadius and KeepRuleBlastRadius are |
| // non-overlapping. |
| // TODO(b/441055269): Check that the reference constants pools do not contain duplicates. |
| return true; |
| } |
| |
| private static class KeepConstraintsEquivalence extends Equivalence<KeepConstraints> { |
| |
| private static final KeepConstraintsEquivalence INSTANCE = new KeepConstraintsEquivalence(); |
| |
| public static Wrapper<KeepConstraints> doWrap(KeepConstraints constraints) { |
| return INSTANCE.wrap(constraints); |
| } |
| |
| @Override |
| protected boolean doEquivalent(KeepConstraints a, KeepConstraints b) { |
| if (a.getConstraintsCount() != b.getConstraintsCount()) { |
| return false; |
| } |
| int flags = 0; |
| for (int i = 0; i < a.getConstraintsCount(); i++) { |
| flags ^= 1 << a.getConstraints(i).getNumber(); |
| flags ^= 1 << b.getConstraints(i).getNumber(); |
| } |
| return flags == 0; |
| } |
| |
| @Override |
| protected int doHash(KeepConstraints constraints) { |
| int hash = 0; |
| for (KeepConstraint constraint : constraints.getConstraintsList()) { |
| hash |= 1 << constraint.getNumber(); |
| } |
| return hash; |
| } |
| } |
| } |