blob: 804364191773d17e54fe5593454d93ddb8f82938 [file] [log] [blame]
// Copyright (c) 2022, 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.positions;
import static com.android.tools.r8.utils.positions.PositionUtils.mustHaveResidualDebugInfo;
import com.android.tools.r8.cf.code.CfInstruction;
import com.android.tools.r8.cf.code.CfPosition;
import com.android.tools.r8.debuginfo.DebugRepresentation.DebugRepresentationPredicate;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.Code;
import com.android.tools.r8.graph.DexClassAndMethod;
import com.android.tools.r8.graph.DexDebugInfo;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.MapVersion;
import com.android.tools.r8.naming.MappingComposeException;
import com.android.tools.r8.naming.MappingComposer;
import com.android.tools.r8.naming.ProguardMapSupplier;
import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapSupplierResult;
import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation;
import com.android.tools.r8.naming.mappinginformation.ResidualSignatureMappingInformation;
import com.android.tools.r8.shaking.KeepInfoCollection;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ObjectUtils;
import com.android.tools.r8.utils.OriginalSourceFiles;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.positions.MappedPositionToClassNameMapperBuilder.MappedPositionToClassNamingBuilder;
import com.android.tools.r8.utils.timing.Timing;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
public class LineNumberOptimizer {
public static ProguardMapSupplierResult runAndWriteMap(
AndroidApp inputApp,
AppView<?> appView,
ExecutorService executorService,
Timing timing,
OriginalSourceFiles originalSourceFiles,
DebugRepresentationPredicate representation)
throws ExecutionException {
assert appView.options().hasMappingFileSupport();
if (shouldEmitOriginalMappingFile(appView)) {
appView.options().reporter.warning(new NotSupportedMapVersionForMappingComposeDiagnostic());
timing.begin("Spawn write proguard map - emitting original mapping file");
ProguardMapSupplierResult result =
ProguardMapSupplier.create(appView.appInfo().app().getProguardMap(), appView.options())
.writeProguardMap(appView, executorService, timing);
timing.end();
return result;
}
// When line number optimization is turned off the identity mapping for line numbers is
// used. We still run the line number optimizer to collect line numbers and inline frame
// information for the mapping file.
timing.begin("Line number remapping");
ClassNameMapper mapper =
run(appView, inputApp, originalSourceFiles, representation, executorService, timing);
timing.end();
if (appView.options().mappingComposeOptions().generatedClassNameMapperConsumer != null) {
appView.options().mappingComposeOptions().generatedClassNameMapperConsumer.accept(mapper);
}
if (appView.options().mappingComposeOptions().enableExperimentalMappingComposition
&& appView.appInfo().app().getProguardMap() != null) {
try (Timing t0 = timing.begin("Compose proguard map")) {
mapper =
ClassNameMapper.mapperFromStringWithPreamble(
MappingComposer.compose(
appView.options(), appView.appInfo().app().getProguardMap(), mapper));
} catch (IOException | MappingComposeException e) {
throw new CompilationError(e.getMessage(), e);
}
}
timing.begin("Spawn write proguard map");
ProguardMapSupplierResult result =
ProguardMapSupplier.create(mapper, appView.options())
.writeProguardMap(appView, executorService, timing);
timing.end();
return result;
}
private static boolean shouldEmitOriginalMappingFile(AppView<?> appView) {
if (!appView.options().mappingComposeOptions().enableExperimentalMappingComposition
|| appView.appInfo().app().getProguardMap() == null) {
return false;
}
MapVersionMappingInformation mapVersionInfo =
appView.appInfo().app().getProguardMap().getFirstMapVersionInformation();
if (mapVersionInfo == null) {
return true;
}
MapVersion newMapVersion = mapVersionInfo.getMapVersion();
return !ResidualSignatureMappingInformation.isSupported(newMapVersion)
|| newMapVersion.isUnknown();
}
public static ClassNameMapper run(
AppView<?> appView,
AndroidApp inputApp,
OriginalSourceFiles originalSourceFiles,
DebugRepresentationPredicate representation,
ExecutorService executorService,
Timing timing)
throws ExecutionException {
PositionToMappedRangeMapper positionToMappedRangeMapper =
PositionToMappedRangeMapper.create(appView);
MappedPositionToClassNameMapperBuilder builder =
MappedPositionToClassNameMapperBuilder.builder(appView, originalSourceFiles);
// Collect which files contain which classes that need to have their line numbers optimized.
timing.begin("Process classes");
AppPositionRemapper positionRemapper = AppPositionRemapper.create(appView, inputApp, timing);
Deque<MappedPositionsForClassResult> worklist = new ConcurrentLinkedDeque<>();
ThreadUtils.processItemsThatMatches(
appView.appInfo().classes(),
clazz -> shouldRun(clazz, appView),
(clazz, threadTiming) -> {
MappedPositionsForClassResult classResult =
runForClass(
clazz,
appView,
representation,
positionRemapper,
positionToMappedRangeMapper,
threadTiming);
worklist.addLast(classResult);
},
appView.options(),
executorService,
timing,
timing.beginMerger("Map positions concurrently", executorService));
timing.end();
timing.begin("Add class naming");
while (!worklist.isEmpty()) {
MappedPositionsForClassResult classResult = worklist.removeFirst();
MappedPositionToClassNamingBuilder classNamingBuilder =
builder.addClassNaming(classResult.getClazz());
for (MappedPositionsForMethodResult methodResult :
classResult.getMappedPositionsForMethods()) {
classNamingBuilder.addMappedPositions(
methodResult.method,
methodResult.mappedPositions,
methodResult.positionRemapper,
methodResult.canUsePc);
}
}
timing.end();
// Update all the debug-info objects.
timing.begin("Update debug info in code objects");
positionToMappedRangeMapper.updateDebugInfoInCodeObjects();
timing.end();
return builder.build();
}
private static boolean shouldRun(DexProgramClass clazz, AppView<?> appView) {
InternalOptions options = appView.options();
if (options.partialSubCompilationConfiguration == null) {
return true;
} else {
return !options.partialSubCompilationConfiguration.asR8().hasD8DefinitionFor(clazz.getType());
}
}
private static MappedPositionsForClassResult runForClass(
DexProgramClass clazz,
AppView<?> appView,
DebugRepresentationPredicate representation,
AppPositionRemapper positionRemapper,
PositionToMappedRangeMapper positionToMappedRangeMapper,
Timing timing) {
timing.begin("Prelude");
IdentityHashMap<DexString, List<ProgramMethod>> methodsByRenamedName =
groupMethodsByRenamedName(appView, clazz);
// Process methods ordered by renamed name.
List<DexString> renamedMethodNames = new ArrayList<>(methodsByRenamedName.keySet());
renamedMethodNames.sort(DexString::compareTo);
timing.end();
ClassPositionRemapper classPositionRemapper =
positionRemapper.createClassPositionRemapper(clazz);
List<MappedPositionsForMethodResult> mappedPositionsForMethodResults = new ArrayList<>();
for (DexString methodName : renamedMethodNames) {
List<ProgramMethod> methods = methodsByRenamedName.get(methodName);
if (methods.size() > 1) {
// If there are multiple methods with the same name (overloaded) then sort them for
// deterministic behaviour: the algorithm will assign new line numbers in this order.
// Methods with different names can share the same line numbers, that's why they don't
// need to be sorted.
// If we are compiling to DEX we will try to not generate overloaded names. This saves
// space by allowing more debug-information to be canonicalized. If we have overloaded
// methods, we either did not rename them, we renamed them according to a supplied map or
// they may be bridges for interface methods with covariant return types.
sortMethods(methods);
assert verifyMethodsAreKeptDirectlyOrIndirectly(appView, methods);
}
timing.begin("Process methods");
// We must reuse the same MethodPositionRemapper for methods with the same name.
MethodPositionRemapper methodPositionRemapper =
classPositionRemapper.createMethodPositionRemapper();
for (ProgramMethod method : methods) {
if (shouldRunForMethod(method, appView, methodName, methods)) {
MappedPositionsForMethodResult mappedPositionsForMethodResult =
runForMethod(
method,
appView,
methods,
methodPositionRemapper,
positionToMappedRangeMapper,
representation,
timing);
mappedPositionsForMethodResults.add(mappedPositionsForMethodResult);
}
}
timing.end();
}
return new MappedPositionsForClassResult(clazz, mappedPositionsForMethodResults);
}
private static boolean shouldRunForMethod(
ProgramMethod method, AppView<?> appView, DexString methodName, List<ProgramMethod> methods) {
DexEncodedMethod definition = method.getDefinition();
return !method.getName().isIdenticalTo(methodName)
|| mustHaveResidualDebugInfo(appView.options(), definition)
|| definition.isD8R8Synthesized()
|| methods.size() > 1;
}
private static MappedPositionsForMethodResult runForMethod(
ProgramMethod method,
AppView<?> appView,
List<ProgramMethod> methods,
MethodPositionRemapper positionRemapper,
PositionToMappedRangeMapper positionToMappedRangeMapper,
DebugRepresentationPredicate representation,
Timing timing) {
Code code = method.getDefinition().getCode();
if (code == null
|| !(code.isCfCode() || code.isDexCode())
|| appView.isCfByteCodePassThrough(method)) {
return new MappedPositionsForMethodResult(
method, Collections.emptyList(), positionRemapper, representation.canUseDexPc(methods));
}
try (Timing t0 = timing.begin("Get mapped positions")) {
int pcEncodingCutoff =
ObjectUtils.identical(method, methods.get(0))
? representation.getDexPcEncodingCutoff(method)
: -1;
boolean canUseDexPc = pcEncodingCutoff > 0;
List<MappedPosition> mappedPositions =
positionToMappedRangeMapper.getMappedPositions(
method, positionRemapper, methods.size() > 1, canUseDexPc, pcEncodingCutoff, timing);
return new MappedPositionsForMethodResult(
method, mappedPositions, positionRemapper, canUseDexPc);
}
}
@SuppressWarnings("ComplexBooleanConstant")
private static boolean verifyMethodsAreKeptDirectlyOrIndirectly(
AppView<?> appView, List<ProgramMethod> methods) {
if (appView.options().isGeneratingClassFiles() || !appView.appInfo().hasClassHierarchy()) {
return true;
}
AppInfoWithClassHierarchy appInfo = appView.appInfo().withClassHierarchy();
KeepInfoCollection keepInfo = appView.getKeepInfo();
boolean allSeenAreInstanceInitializers = true;
DexString originalName;
for (ProgramMethod method : methods) {
// We cannot rename instance initializers.
if (method.getDefinition().isInstanceInitializer()) {
assert allSeenAreInstanceInitializers;
continue;
}
allSeenAreInstanceInitializers = false;
// If the method is pinned, we cannot minify it.
if (!keepInfo.isMinificationAllowed(method, appView.options())) {
continue;
}
// With desugared library, call-backs names are reserved here.
if (method.getDefinition().isLibraryMethodOverride().isTrue()) {
continue;
}
// We use the same name for interface names even if it has different types.
DexClassAndMethod lookupResult =
appInfo.lookupMaximallySpecificMethod(method.getHolder(), method.getReference());
if (lookupResult == null) {
// We cannot rename methods we cannot look up.
continue;
}
String errorString = method.getReference().qualifiedName() + " is not kept but is overloaded";
assert lookupResult.getHolder().isInterface() : errorString;
// TODO(b/159113601): Reenable assert.
assert true || originalName == null || originalName.equals(method.getReference().name)
: errorString;
originalName = method.getReference().name;
}
return true;
}
private static int getMethodStartLine(ProgramMethod method) {
Code code = method.getDefinition().getCode();
if (code == null) {
return 0;
}
if (code.isDexCode()) {
DexDebugInfo dexDebugInfo = code.asDexCode().getDebugInfo();
return dexDebugInfo == null ? 0 : dexDebugInfo.getStartLine();
} else if (code.isCfCode()) {
List<CfInstruction> instructions = code.asCfCode().getInstructions();
for (CfInstruction instruction : instructions) {
if (!(instruction instanceof CfPosition)) {
continue;
}
return ((CfPosition) instruction).getPosition().getLine();
}
}
return 0;
}
public static void sortMethods(List<ProgramMethod> methods) {
// Sort by startline, then DexEncodedMethod.slowCompare. Use startLine = 0 if no debuginfo.
methods.sort(
(lhs, rhs) -> {
int lhsStartLine = getMethodStartLine(lhs);
int rhsStartLine = getMethodStartLine(rhs);
int startLineDiff = lhsStartLine - rhsStartLine;
if (startLineDiff != 0) return startLineDiff;
return DexEncodedMethod.slowCompare(lhs.getDefinition(), rhs.getDefinition());
});
// Insert the largest method first since we can use pc encoding for this method.
int largestIndex = -1;
int largestCode = -1;
for (int i = 0; i < methods.size(); i++) {
ProgramMethod method = methods.get(i);
if (method.getDefinition().hasCode() && method.getDefinition().getCode().isDexCode()) {
int codeSizeInBytes = method.getDefinition().getCode().asDexCode().codeSizeInBytes();
if (codeSizeInBytes > largestCode) {
largestIndex = i;
largestCode = codeSizeInBytes;
}
}
}
if (largestIndex > 0) {
Collections.swap(methods, 0, largestIndex);
}
}
public static IdentityHashMap<DexString, List<ProgramMethod>> groupMethodsByRenamedName(
AppView<?> appView, DexProgramClass clazz) {
IdentityHashMap<DexString, List<ProgramMethod>> methodsByRenamedName =
new IdentityHashMap<>(clazz.getMethodCollection().size());
for (ProgramMethod programMethod : clazz.programMethods()) {
// Add method only if renamed, moved, or if it has debug info to map.
DexMethod method = programMethod.getReference();
DexString renamedName = appView.getNamingLens().lookupName(method);
methodsByRenamedName
.computeIfAbsent(renamedName, key -> new ArrayList<>())
.add(programMethod);
}
return methodsByRenamedName;
}
private static class MappedPositionsForClassResult {
private final DexProgramClass clazz;
private final List<MappedPositionsForMethodResult> mappedPositionsForMethods;
private MappedPositionsForClassResult(
DexProgramClass clazz, List<MappedPositionsForMethodResult> mappedPositionsForMethods) {
this.clazz = clazz;
this.mappedPositionsForMethods = mappedPositionsForMethods;
}
DexProgramClass getClazz() {
return clazz;
}
List<MappedPositionsForMethodResult> getMappedPositionsForMethods() {
return mappedPositionsForMethods;
}
}
private static class MappedPositionsForMethodResult {
private final ProgramMethod method;
private final List<MappedPosition> mappedPositions;
private final MethodPositionRemapper positionRemapper;
private final boolean canUsePc;
private MappedPositionsForMethodResult(
ProgramMethod method,
List<MappedPosition> mappedPositions,
MethodPositionRemapper positionRemapper,
boolean canUsePc) {
this.method = method;
this.mappedPositions = mappedPositions;
this.positionRemapper = positionRemapper;
this.canUsePc = canUsePc;
}
}
}