blob: da825d15c02c2197992e39c3173a2af1d2a801a8 [file] [log] [blame]
// Copyright (c) 2017, 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;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.cf.code.CfInstruction;
import com.android.tools.r8.cf.code.CfPosition;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.CfCode;
import com.android.tools.r8.graph.Code;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexClassAndMethod;
import com.android.tools.r8.graph.DexCode;
import com.android.tools.r8.graph.DexDebugEvent;
import com.android.tools.r8.graph.DexDebugEvent.AdvancePC;
import com.android.tools.r8.graph.DexDebugEvent.Default;
import com.android.tools.r8.graph.DexDebugEvent.EndLocal;
import com.android.tools.r8.graph.DexDebugEvent.RestartLocal;
import com.android.tools.r8.graph.DexDebugEvent.SetEpilogueBegin;
import com.android.tools.r8.graph.DexDebugEvent.SetFile;
import com.android.tools.r8.graph.DexDebugEvent.SetPrologueEnd;
import com.android.tools.r8.graph.DexDebugEvent.StartLocal;
import com.android.tools.r8.graph.DexDebugEventBuilder;
import com.android.tools.r8.graph.DexDebugEventVisitor;
import com.android.tools.r8.graph.DexDebugInfo;
import com.android.tools.r8.graph.DexDebugPositionState;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
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.DexType;
import com.android.tools.r8.graph.DexValue.DexValueString;
import com.android.tools.r8.graph.GraphLens;
import com.android.tools.r8.ir.code.Position;
import com.android.tools.r8.kotlin.KotlinSourceDebugExtensionParser;
import com.android.tools.r8.kotlin.KotlinSourceDebugExtensionParser.Result;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.ClassNaming;
import com.android.tools.r8.naming.ClassNaming.Builder;
import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
import com.android.tools.r8.naming.MemberNaming;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.naming.Range;
import com.android.tools.r8.naming.mappinginformation.CompilerSynthesizedMappingInformation;
import com.android.tools.r8.naming.mappinginformation.FileNameInformation;
import com.android.tools.r8.naming.mappinginformation.MappingInformation;
import com.android.tools.r8.retrace.internal.RetraceUtils;
import com.android.tools.r8.shaking.KeepInfoCollection;
import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
import com.google.common.base.Suppliers;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
public class LineNumberOptimizer {
// PositionRemapper is a stateful function which takes a position (represented by a
// DexDebugPositionState) and returns a remapped Position.
private interface PositionRemapper {
Pair<Position, Position> createRemappedPosition(Position position);
}
private static class IdentityPositionRemapper implements PositionRemapper {
@Override
public Pair<Position, Position> createRemappedPosition(Position position) {
return new Pair<>(position, position);
}
}
private static class OptimizingPositionRemapper implements PositionRemapper {
private final int maxLineDelta;
private DexMethod previousMethod = null;
private int previousSourceLine = -1;
private int nextOptimizedLineNumber = 1;
OptimizingPositionRemapper(InternalOptions options) {
// TODO(113198295): For dex using "Constants.DBG_LINE_RANGE + Constants.DBG_LINE_BASE"
// instead of 1 creates a ~30% smaller map file but the dex files gets larger due to reduced
// debug info canonicalization.
maxLineDelta = options.isGeneratingClassFiles() ? Integer.MAX_VALUE : 1;
}
@Override
public Pair<Position, Position> createRemappedPosition(Position position) {
assert position.method != null;
if (previousMethod == position.method) {
assert previousSourceLine >= 0;
if (position.line > previousSourceLine
&& position.line - previousSourceLine <= maxLineDelta) {
nextOptimizedLineNumber += (position.line - previousSourceLine) - 1;
}
}
Position newPosition =
new Position(nextOptimizedLineNumber, position.file, position.method, null);
++nextOptimizedLineNumber;
previousSourceLine = position.line;
previousMethod = position.method;
return new Pair<>(position, newPosition);
}
}
private static class KotlinInlineFunctionPositionRemapper implements PositionRemapper {
private final AppView<?> appView;
private final DexItemFactory factory;
private final Map<DexType, Result> parsedKotlinSourceDebugExtensions = new IdentityHashMap<>();
private final CfLineToMethodMapper lineToMethodMapper;
private final PositionRemapper baseRemapper;
// Fields for the current context.
private DexEncodedMethod currentMethod;
private Result parsedData = null;
private KotlinInlineFunctionPositionRemapper(
AppView<?> appView,
PositionRemapper baseRemapper,
CfLineToMethodMapper lineToMethodMapper) {
this.appView = appView;
this.factory = appView.dexItemFactory();
this.baseRemapper = baseRemapper;
this.lineToMethodMapper = lineToMethodMapper;
}
@Override
public Pair<Position, Position> createRemappedPosition(Position position) {
assert currentMethod != null;
int line = position.line;
Result parsedData = getAndParseSourceDebugExtension(position.method.holder);
if (parsedData == null) {
return baseRemapper.createRemappedPosition(position);
}
Map.Entry<Integer, KotlinSourceDebugExtensionParser.Position> currentPosition =
parsedData.lookup(line);
if (currentPosition == null) {
return baseRemapper.createRemappedPosition(position);
}
int delta = line - currentPosition.getKey();
int originalPosition = currentPosition.getValue().getRange().from + delta;
try {
String binaryName = currentPosition.getValue().getSource().getPath();
String nameAndDescriptor =
lineToMethodMapper.lookupNameAndDescriptor(binaryName, originalPosition);
if (nameAndDescriptor == null) {
return baseRemapper.createRemappedPosition(position);
}
String clazzDescriptor = DescriptorUtils.getDescriptorFromClassBinaryName(binaryName);
String methodName = CfLineToMethodMapper.getName(nameAndDescriptor);
String methodDescriptor = CfLineToMethodMapper.getDescriptor(nameAndDescriptor);
String returnTypeDescriptor = DescriptorUtils.getReturnTypeDescriptor(methodDescriptor);
String[] argumentDescriptors = DescriptorUtils.getArgumentTypeDescriptors(methodDescriptor);
DexString[] argumentDexStringDescriptors = new DexString[argumentDescriptors.length];
for (int i = 0; i < argumentDescriptors.length; i++) {
argumentDexStringDescriptors[i] = factory.createString(argumentDescriptors[i]);
}
DexMethod inlinee =
factory.createMethod(
factory.createString(clazzDescriptor),
factory.createString(methodName),
factory.createString(returnTypeDescriptor),
argumentDexStringDescriptors);
if (!inlinee.equals(position.method)) {
return baseRemapper.createRemappedPosition(
new Position(originalPosition, null, inlinee, position));
}
// This is the same position, so we should really not mark this as an inline position. Fall
// through to the default case.
} catch (ResourceException ignored) {
// Intentionally left empty. Remapping of kotlin functions utility is a best effort mapping.
}
return baseRemapper.createRemappedPosition(position);
}
private Result getAndParseSourceDebugExtension(DexType holder) {
if (parsedData == null) {
parsedData = parsedKotlinSourceDebugExtensions.get(holder);
}
if (parsedData != null || parsedKotlinSourceDebugExtensions.containsKey(holder)) {
return parsedData;
}
DexClass clazz = appView.definitionFor(currentMethod.getHolderType());
DexValueString dexValueString = appView.getSourceDebugExtensionForType(clazz);
if (dexValueString != null) {
parsedData = KotlinSourceDebugExtensionParser.parse(dexValueString.value.toString());
}
parsedKotlinSourceDebugExtensions.put(holder, parsedData);
return parsedData;
}
public void setMethod(DexEncodedMethod method) {
this.currentMethod = method;
this.parsedData = null;
}
}
// PositionEventEmitter is a stateful function which converts a Position into series of
// position-related DexDebugEvents and puts them into a processedEvents list.
private static class PositionEventEmitter {
private final DexItemFactory dexItemFactory;
private int startLine = -1;
private final DexMethod method;
private int previousPc = 0;
private Position previousPosition = null;
private final List<DexDebugEvent> processedEvents;
private PositionEventEmitter(
DexItemFactory dexItemFactory, DexMethod method, List<DexDebugEvent> processedEvents) {
this.dexItemFactory = dexItemFactory;
this.method = method;
this.processedEvents = processedEvents;
}
private void emitAdvancePc(int pc) {
processedEvents.add(new AdvancePC(pc - previousPc));
previousPc = pc;
}
private void emitPositionEvents(int currentPc, Position currentPosition) {
if (previousPosition == null) {
startLine = currentPosition.line;
previousPosition = new Position(startLine, null, method, null);
}
DexDebugEventBuilder.emitAdvancementEvents(
previousPc,
previousPosition,
currentPc,
currentPosition,
processedEvents,
dexItemFactory,
true);
previousPc = currentPc;
previousPosition = currentPosition;
}
private int getStartLine() {
assert (startLine >= 0);
return startLine;
}
}
// We will be remapping positional debug events and collect them as MappedPositions.
private static class MappedPosition {
private final DexMethod method;
private final int originalLine;
private final Position caller;
private final int obfuscatedLine;
private MappedPosition(
DexMethod method, int originalLine, Position caller, int obfuscatedLine) {
this.method = method;
this.originalLine = originalLine;
this.caller = caller;
this.obfuscatedLine = obfuscatedLine;
}
}
public static ClassNameMapper run(
AppView<?> appView, DexApplication application, AndroidApp inputApp, NamingLens namingLens) {
// For finding methods in kotlin files based on SourceDebugExtensions, we use a line method map.
// We create it here to ensure it is only reading class files once.
CfLineToMethodMapper cfLineToMethodMapper = new CfLineToMethodMapper(inputApp);
ClassNameMapper.Builder classNameMapperBuilder = ClassNameMapper.builder();
// Collect which files contain which classes that need to have their line numbers optimized.
for (DexProgramClass clazz : application.classes()) {
boolean isSyntheticClass = appView.getSyntheticItems().isSyntheticClass(clazz);
IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByRenamedName =
groupMethodsByRenamedName(appView.graphLens(), namingLens, clazz);
// At this point we don't know if we really need to add this class to the builder.
// It depends on whether any methods/fields are renamed or some methods contain positions.
// Create a supplier which creates a new, cached ClassNaming.Builder on-demand.
DexType originalType = appView.graphLens().getOriginalType(clazz.type);
DexString renamedDescriptor = namingLens.lookupDescriptor(clazz.getType());
Supplier<ClassNaming.Builder> onDemandClassNamingBuilder =
Suppliers.memoize(
() ->
classNameMapperBuilder.classNamingBuilder(
DescriptorUtils.descriptorToJavaType(renamedDescriptor.toString()),
originalType.toSourceString(),
com.android.tools.r8.position.Position.UNKNOWN));
// Check if source file should be added to the map
if (clazz.sourceFile != null) {
String sourceFile = clazz.sourceFile.toString();
if (!RetraceUtils.hasPredictableSourceFileName(clazz.toSourceString(), sourceFile)) {
onDemandClassNamingBuilder
.get()
.addMappingInformation(FileNameInformation.build(sourceFile), Unreachable::raise);
}
}
if (isSyntheticClass
&& CompilerSynthesizedMappingInformation.isSupported(
appView.options().getMapFileVersion())) {
onDemandClassNamingBuilder
.get()
.addMappingInformation(
CompilerSynthesizedMappingInformation.builder().build(), Unreachable::raise);
}
// If the class is renamed add it to the classNamingBuilder.
addClassToClassNaming(originalType, renamedDescriptor, onDemandClassNamingBuilder);
// First transfer renamed fields to classNamingBuilder.
addFieldsToClassNaming(
appView.graphLens(), namingLens, clazz, originalType, onDemandClassNamingBuilder);
// Then process the methods, ordered by renamed name.
List<DexString> renamedMethodNames = new ArrayList<>(methodsByRenamedName.keySet());
renamedMethodNames.sort(DexString::compareTo);
for (DexString methodName : renamedMethodNames) {
List<DexEncodedMethod> 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);
}
boolean identityMapping =
appView.options().lineNumberOptimization == LineNumberOptimization.OFF;
PositionRemapper positionRemapper =
identityMapping
? new IdentityPositionRemapper()
: new OptimizingPositionRemapper(appView.options());
// Kotlin inline functions and arguments have their inlining information stored in the
// source debug extension annotation. Instantiate the kotlin remapper on top of the original
// remapper to allow for remapping original positions to kotlin inline positions.
KotlinInlineFunctionPositionRemapper kotlinRemapper =
new KotlinInlineFunctionPositionRemapper(
appView, positionRemapper, cfLineToMethodMapper);
for (DexEncodedMethod method : methods) {
kotlinRemapper.currentMethod = method;
List<MappedPosition> mappedPositions = new ArrayList<>();
Code code = method.getCode();
if (code != null) {
if (code.isDexCode() && doesContainPositions(code.asDexCode())) {
if (appView.options().canUseDexPcAsDebugInformation() && methods.size() == 1) {
optimizeDexCodePositionsForPc(method, appView, kotlinRemapper, mappedPositions);
} else {
optimizeDexCodePositions(
method, appView, kotlinRemapper, mappedPositions, identityMapping);
}
} else if (code.isCfCode()
&& doesContainPositions(code.asCfCode())
&& !appView.isCfByteCodePassThrough(method)) {
optimizeCfCodePositions(method, kotlinRemapper, mappedPositions, appView);
}
}
DexMethod originalMethod =
appView.graphLens().getOriginalMethodSignature(method.getReference());
MethodSignature originalSignature =
MethodSignature.fromDexMethod(originalMethod, originalMethod.holder != originalType);
DexString obfuscatedNameDexString = namingLens.lookupName(method.getReference());
String obfuscatedName = obfuscatedNameDexString.toString();
List<MappingInformation> methodMappingInfo = new ArrayList<>();
if (method.isD8R8Synthesized()
&& CompilerSynthesizedMappingInformation.isSupported(
appView.options().getMapFileVersion())) {
methodMappingInfo.add(CompilerSynthesizedMappingInformation.builder().build());
}
// Don't emit pure identity mappings.
if (mappedPositions.isEmpty()
&& methodMappingInfo.isEmpty()
&& obfuscatedNameDexString == originalMethod.name
&& originalMethod.holder == originalType) {
continue;
}
MemberNaming memberNaming = new MemberNaming(originalSignature, obfuscatedName);
onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
// Add simple "a() -> b" mapping if we won't have any other with concrete line numbers
if (mappedPositions.isEmpty()) {
MappedRange range =
onDemandClassNamingBuilder
.get()
.addMappedRange(null, originalSignature, null, obfuscatedName);
methodMappingInfo.forEach(
info -> range.addMappingInformation(info, Unreachable::raise));
continue;
}
Map<DexMethod, MethodSignature> signatures = new IdentityHashMap<>();
signatures.put(originalMethod, originalSignature);
Function<DexMethod, MethodSignature> getOriginalMethodSignature =
m ->
signatures.computeIfAbsent(
m, key -> MethodSignature.fromDexMethod(m, m.holder != clazz.getType()));
// Update memberNaming with the collected positions, merging multiple positions into a
// single region whenever possible.
for (int i = 0; i < mappedPositions.size(); /* updated in body */ ) {
MappedPosition firstPosition = mappedPositions.get(i);
int j = i + 1;
MappedPosition lastPosition = firstPosition;
for (; j < mappedPositions.size(); j++) {
// Break if this position cannot be merged with lastPosition.
MappedPosition mp = mappedPositions.get(j);
// We allow for ranges being mapped to the same line but not to other ranges:
// 1:10:void foo():42:42 -> a
// is OK since retrace(a(:7)) = 42, however, the following is not OK:
// 1:10:void foo():42:43 -> a
// since retrace(a(:7)) = 49, which is not correct.
boolean isSingleLine = mp.originalLine == firstPosition.originalLine;
boolean differentDelta =
mp.originalLine - lastPosition.originalLine
!= mp.obfuscatedLine - lastPosition.obfuscatedLine;
boolean isMappingRangeToSingleLine =
firstPosition.obfuscatedLine != lastPosition.obfuscatedLine
&& firstPosition.originalLine == lastPosition.originalLine;
// Note that mp.caller and lastPosition.class must be deep-compared since multiple
// inlining passes lose the canonical property of the positions.
if (mp.method != lastPosition.method
|| (!isSingleLine && differentDelta)
|| (!isSingleLine && isMappingRangeToSingleLine)
|| !Objects.equals(mp.caller, lastPosition.caller)) {
break;
}
// The mapped positions are not guaranteed to be in order, so maintain first and last
// position.
if (firstPosition.obfuscatedLine > mp.obfuscatedLine) {
firstPosition = mp;
}
if (lastPosition.obfuscatedLine < mp.obfuscatedLine) {
lastPosition = mp;
}
}
Range obfuscatedRange =
new Range(firstPosition.obfuscatedLine, lastPosition.obfuscatedLine);
Range originalRange = new Range(firstPosition.originalLine, lastPosition.originalLine);
ClassNaming.Builder classNamingBuilder = onDemandClassNamingBuilder.get();
MappedRange lastMappedRange =
classNamingBuilder.addMappedRange(
obfuscatedRange,
getOriginalMethodSignature.apply(firstPosition.method),
originalRange,
obfuscatedName);
Position caller = firstPosition.caller;
while (caller != null) {
lastMappedRange =
classNamingBuilder.addMappedRange(
obfuscatedRange,
getOriginalMethodSignature.apply(caller.method),
Math.max(caller.line, 0), // Prevent against "no-position".
obfuscatedName);
caller = caller.callerPosition;
}
for (MappingInformation info : methodMappingInfo) {
lastMappedRange.addMappingInformation(info, Unreachable::raise);
}
i = j;
}
} // for each method of the group
} // for each method group, grouped by name
} // for each class
return classNameMapperBuilder.build();
}
private static boolean verifyMethodsAreKeptDirectlyOrIndirectly(
AppView<?> appView, List<DexEncodedMethod> methods) {
if (appView.options().isGeneratingClassFiles() || !appView.appInfo().hasClassHierarchy()) {
return true;
}
AppInfoWithClassHierarchy appInfo = appView.appInfo().withClassHierarchy();
KeepInfoCollection keepInfo = appView.getKeepInfo();
boolean allSeenAreInstanceInitializers = true;
DexString originalName = null;
for (DexEncodedMethod method : methods) {
// We cannot rename instance initializers.
if (method.isInstanceInitializer()) {
assert allSeenAreInstanceInitializers;
continue;
}
allSeenAreInstanceInitializers = false;
// If the method is pinned, we cannot minify it.
if (!keepInfo.isMinificationAllowed(method.getReference(), appView, appView.options())) {
continue;
}
// With desugared library, call-backs names are reserved here.
if (method.isLibraryMethodOverride().isTrue()) {
continue;
}
// We use the same name for interface names even if it has different types.
DexProgramClass clazz = appView.definitionForProgramType(method.getHolderType());
DexClassAndMethod lookupResult =
appInfo.lookupMaximallySpecificMethod(clazz, 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(DexEncodedMethod method) {
Code code = method.getCode();
if (code == null) {
return 0;
}
if (code.isDexCode()) {
DexDebugInfo dexDebugInfo = code.asDexCode().getDebugInfo();
return dexDebugInfo == null ? 0 : dexDebugInfo.startLine;
} else if (code.isCfCode()) {
List<CfInstruction> instructions = code.asCfCode().getInstructions();
for (CfInstruction instruction : instructions) {
if (!(instruction instanceof CfPosition)) {
continue;
}
return ((CfPosition) instruction).getPosition().line;
}
}
return 0;
}
// Sort by startline, then DexEncodedMethod.slowCompare.
// Use startLine = 0 if no debuginfo.
private static void sortMethods(List<DexEncodedMethod> methods) {
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, rhs);
});
}
@SuppressWarnings("ReturnValueIgnored")
private static void addClassToClassNaming(
DexType originalType,
DexString renamedClassName,
Supplier<Builder> onDemandClassNamingBuilder) {
// We do know we need to create a ClassNaming.Builder if the class itself had been renamed.
if (originalType.descriptor != renamedClassName) {
// Not using return value, it's registered in classNameMapperBuilder
onDemandClassNamingBuilder.get();
}
}
private static void addFieldsToClassNaming(
GraphLens graphLens,
NamingLens namingLens,
DexProgramClass clazz,
DexType originalType,
Supplier<Builder> onDemandClassNamingBuilder) {
clazz.forEachField(
dexEncodedField -> {
DexField dexField = dexEncodedField.getReference();
DexField originalField = graphLens.getOriginalFieldSignature(dexField);
DexString renamedName = namingLens.lookupName(dexField);
if (renamedName != originalField.name || originalField.holder != originalType) {
FieldSignature originalSignature =
FieldSignature.fromDexField(originalField, originalField.holder != originalType);
MemberNaming memberNaming = new MemberNaming(originalSignature, renamedName.toString());
onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
}
});
}
private static IdentityHashMap<DexString, List<DexEncodedMethod>> groupMethodsByRenamedName(
GraphLens graphLens, NamingLens namingLens, DexProgramClass clazz) {
IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByRenamedName =
new IdentityHashMap<>(clazz.getMethodCollection().size());
for (DexEncodedMethod encodedMethod : clazz.methods()) {
// Add method only if renamed, moved, or contains positions.
DexMethod method = encodedMethod.getReference();
DexString renamedName = namingLens.lookupName(method);
if (renamedName != method.name
|| graphLens.getOriginalMethodSignature(method) != method
|| doesContainPositions(encodedMethod)) {
methodsByRenamedName
.computeIfAbsent(renamedName, key -> new ArrayList<>())
.add(encodedMethod);
}
}
return methodsByRenamedName;
}
private static boolean doesContainPositions(DexEncodedMethod method) {
Code code = method.getCode();
if (code == null) {
return false;
}
if (code.isDexCode()) {
return doesContainPositions(code.asDexCode());
} else if (code.isCfCode()) {
return doesContainPositions(code.asCfCode());
}
return false;
}
private static boolean doesContainPositions(DexCode dexCode) {
DexDebugInfo debugInfo = dexCode.getDebugInfo();
if (debugInfo == null) {
return false;
}
for (DexDebugEvent event : debugInfo.events) {
if (event instanceof DexDebugEvent.Default) {
return true;
}
}
return false;
}
private static boolean doesContainPositions(CfCode cfCode) {
List<CfInstruction> instructions = cfCode.getInstructions();
for (CfInstruction instruction : instructions) {
if (instruction instanceof CfPosition) {
return true;
}
}
return false;
}
private static void optimizeDexCodePositions(
DexEncodedMethod method,
AppView<?> appView,
PositionRemapper positionRemapper,
List<MappedPosition> mappedPositions,
boolean identityMapping) {
// Do the actual processing for each method.
final DexApplication application = appView.appInfo().app();
DexCode dexCode = method.getCode().asDexCode();
DexDebugInfo debugInfo = dexCode.getDebugInfo();
List<DexDebugEvent> processedEvents = new ArrayList<>();
PositionEventEmitter positionEventEmitter =
new PositionEventEmitter(
application.dexItemFactory,
appView.graphLens().getOriginalMethodSignature(method.getReference()),
processedEvents);
Box<Boolean> inlinedOriginalPosition = new Box<>(false);
// Debug event visitor to map line numbers.
DexDebugEventVisitor visitor =
new DexDebugPositionState(
debugInfo.startLine,
appView.graphLens().getOriginalMethodSignature(method.getReference())) {
// Keep track of what PC has been emitted.
private int emittedPc = 0;
// Force the current PC to emitted.
private void flushPc() {
if (emittedPc != getCurrentPc()) {
positionEventEmitter.emitAdvancePc(getCurrentPc());
emittedPc = getCurrentPc();
}
}
// A default event denotes a line table entry and must always be emitted. Remap its line.
@Override
public void visit(Default defaultEvent) {
super.visit(defaultEvent);
assert getCurrentLine() >= 0;
Position position =
new Position(
getCurrentLine(),
getCurrentFile(),
getCurrentMethod(),
getCurrentCallerPosition());
Position currentPosition = remapAndAdd(position, positionRemapper, mappedPositions);
positionEventEmitter.emitPositionEvents(getCurrentPc(), currentPosition);
if (currentPosition != position) {
inlinedOriginalPosition.set(true);
}
emittedPc = getCurrentPc();
}
// Non-materializing events use super, ie, AdvancePC, AdvanceLine and SetInlineFrame.
// Materializing events are just amended to the stream.
@Override
public void visit(SetFile setFile) {
processedEvents.add(setFile);
}
@Override
public void visit(SetPrologueEnd setPrologueEnd) {
processedEvents.add(setPrologueEnd);
}
@Override
public void visit(SetEpilogueBegin setEpilogueBegin) {
processedEvents.add(setEpilogueBegin);
}
// Local changes must force flush the PC ensuing they pertain to the correct point.
@Override
public void visit(StartLocal startLocal) {
flushPc();
processedEvents.add(startLocal);
}
@Override
public void visit(EndLocal endLocal) {
flushPc();
processedEvents.add(endLocal);
}
@Override
public void visit(RestartLocal restartLocal) {
flushPc();
processedEvents.add(restartLocal);
}
};
for (DexDebugEvent event : debugInfo.events) {
event.accept(visitor);
}
DexDebugInfo optimizedDebugInfo =
new DexDebugInfo(
positionEventEmitter.getStartLine(),
debugInfo.parameters,
processedEvents.toArray(DexDebugEvent.EMPTY_ARRAY));
assert !identityMapping
|| inlinedOriginalPosition.get()
|| verifyIdentityMapping(debugInfo, optimizedDebugInfo);
dexCode.setDebugInfo(optimizedDebugInfo);
}
private static void optimizeDexCodePositionsForPc(
DexEncodedMethod method,
AppView<?> appView,
PositionRemapper positionRemapper,
List<MappedPosition> mappedPositions) {
// Do the actual processing for each method.
DexCode dexCode = method.getCode().asDexCode();
DexDebugInfo debugInfo = dexCode.getDebugInfo();
Pair<Integer, Position> lastPosition = new Pair<>();
DexDebugEventVisitor visitor =
new DexDebugPositionState(
debugInfo.startLine,
appView.graphLens().getOriginalMethodSignature(method.getReference())) {
@Override
public void visit(Default defaultEvent) {
super.visit(defaultEvent);
assert getCurrentLine() >= 0;
if (lastPosition.getSecond() != null) {
remapAndAddForPc(
lastPosition.getFirst(),
getCurrentPc(),
lastPosition.getSecond(),
positionRemapper,
mappedPositions);
}
lastPosition.setFirst(getCurrentPc());
lastPosition.setSecond(
new Position(
getCurrentLine(),
getCurrentFile(),
getCurrentMethod(),
getCurrentCallerPosition()));
}
};
for (DexDebugEvent event : debugInfo.events) {
event.accept(visitor);
}
if (lastPosition.getSecond() != null) {
int lastPc = dexCode.instructions[dexCode.instructions.length - 1].getOffset();
remapAndAddForPc(
lastPosition.getFirst(),
lastPc + 1,
lastPosition.getSecond(),
positionRemapper,
mappedPositions);
}
dexCode.setDebugInfo(null);
}
private static boolean verifyIdentityMapping(
DexDebugInfo originalDebugInfo, DexDebugInfo optimizedDebugInfo) {
assert optimizedDebugInfo.startLine == originalDebugInfo.startLine;
assert optimizedDebugInfo.events.length == originalDebugInfo.events.length;
for (int i = 0; i < originalDebugInfo.events.length; ++i) {
assert optimizedDebugInfo.events[i].equals(originalDebugInfo.events[i]);
}
return true;
}
private static void optimizeCfCodePositions(
DexEncodedMethod method,
PositionRemapper positionRemapper,
List<MappedPosition> mappedPositions,
AppView<?> appView) {
// Do the actual processing for each method.
CfCode oldCode = method.getCode().asCfCode();
List<CfInstruction> oldInstructions = oldCode.getInstructions();
List<CfInstruction> newInstructions = new ArrayList<>(oldInstructions.size());
for (CfInstruction oldInstruction : oldInstructions) {
CfInstruction newInstruction;
if (oldInstruction instanceof CfPosition) {
CfPosition cfPosition = (CfPosition) oldInstruction;
newInstruction =
new CfPosition(
cfPosition.getLabel(),
remapAndAdd(cfPosition.getPosition(), positionRemapper, mappedPositions));
} else {
newInstruction = oldInstruction;
}
newInstructions.add(newInstruction);
}
method.setCode(
new CfCode(
method.getHolderType(),
oldCode.getMaxStack(),
oldCode.getMaxLocals(),
newInstructions,
oldCode.getTryCatchRanges(),
oldCode.getLocalVariables()),
appView);
}
private static Position remapAndAdd(
Position position, PositionRemapper remapper, List<MappedPosition> mappedPositions) {
Pair<Position, Position> remappedPosition = remapper.createRemappedPosition(position);
Position oldPosition = remappedPosition.getFirst();
Position newPosition = remappedPosition.getSecond();
mappedPositions.add(
new MappedPosition(
oldPosition.method, oldPosition.line, oldPosition.callerPosition, newPosition.line));
return newPosition;
}
private static void remapAndAddForPc(
int startPc,
int endPc,
Position position,
PositionRemapper remapper,
List<MappedPosition> mappedPositions) {
Pair<Position, Position> remappedPosition = remapper.createRemappedPosition(position);
Position oldPosition = remappedPosition.getFirst();
for (int currentPc = startPc; currentPc < endPc; currentPc++) {
mappedPositions.add(
new MappedPosition(
oldPosition.method, oldPosition.line, oldPosition.callerPosition, currentPc));
}
}
}