| // 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.debuginfo; |
| |
| import com.android.tools.r8.code.Instruction; |
| import com.android.tools.r8.dex.VirtualFile; |
| import com.android.tools.r8.graph.DexCode; |
| import com.android.tools.r8.graph.DexDebugInfo; |
| import com.android.tools.r8.graph.DexDebugInfo.PcBasedDebugInfo; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.DexString; |
| import com.android.tools.r8.graph.GraphLens; |
| import com.android.tools.r8.naming.NamingLens; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.LebUtils; |
| import com.android.tools.r8.utils.LineNumberOptimizer; |
| import com.android.tools.r8.utils.StringUtils; |
| import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; |
| import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; |
| import it.unimi.dsi.fastutil.ints.IntIterators; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| public class DebugRepresentation { |
| |
| public interface DebugRepresentationPredicate { |
| |
| boolean useDexPcEncoding(DexProgramClass holder, DexEncodedMethod method); |
| } |
| |
| public static DebugRepresentationPredicate none(InternalOptions options) { |
| assert !options.canUseDexPc2PcAsDebugInformation(); |
| return (holder, method) -> false; |
| } |
| |
| public static DebugRepresentationPredicate fromFiles( |
| List<VirtualFile> files, InternalOptions options) { |
| if (!options.canUseDexPc2PcAsDebugInformation()) { |
| return none(options); |
| } |
| if (options.canUseNativeDexPcInsteadOfDebugInfo() || options.testing.forcePcBasedEncoding) { |
| return (holder, method) -> true; |
| } |
| // TODO(b/220999985): Avoid the need to maintain a class-to-file map. |
| Map<DexProgramClass, VirtualFile> classMapping = new IdentityHashMap<>(); |
| for (VirtualFile file : files) { |
| file.classes().forEach(c -> classMapping.put(c, file)); |
| } |
| return (holder, method) -> { |
| if (!isPcCandidate(method)) { |
| return false; |
| } |
| VirtualFile file = classMapping.get(holder); |
| DebugRepresentation cutoffs = file.getDebugRepresentation(); |
| return cutoffs.usesPcEncoding(method); |
| }; |
| } |
| |
| private final Int2ReferenceMap<CostSummary> paramToInfo; |
| |
| private DebugRepresentation(Int2ReferenceMap<CostSummary> paramToInfo) { |
| this.paramToInfo = paramToInfo; |
| } |
| |
| public static void computeForFile( |
| VirtualFile file, GraphLens graphLens, NamingLens namingLens, InternalOptions options) { |
| if (!options.canUseDexPc2PcAsDebugInformation() |
| || options.canUseNativeDexPcInsteadOfDebugInfo() |
| || options.testing.forcePcBasedEncoding) { |
| return; |
| } |
| // First collect all of the per-pc costs |
| // (the sum of the normal debug info for all methods sharing the same max pc and param count.) |
| Int2ReferenceMap<CostSummary> paramCountToCosts = new Int2ReferenceOpenHashMap<>(); |
| for (DexProgramClass clazz : file.classes()) { |
| IdentityHashMap<DexString, List<DexEncodedMethod>> overloads = |
| LineNumberOptimizer.groupMethodsByRenamedName(graphLens, namingLens, clazz); |
| for (List<DexEncodedMethod> methods : overloads.values()) { |
| if (methods.size() != 1) { |
| // Never use PC info for overloaded methods. They need distinct lines to disambiguate. |
| continue; |
| } |
| DexEncodedMethod method = methods.get(0); |
| if (!isPcCandidate(method)) { |
| continue; |
| } |
| DexCode code = method.getCode().asDexCode(); |
| DexDebugInfo debugInfo = code.getDebugInfo(); |
| Instruction lastInstruction = getLastExecutableInstruction(code); |
| if (lastInstruction == null) { |
| continue; |
| } |
| int lastPc = lastInstruction.getOffset(); |
| int debugInfoCost = estimatedDebugInfoSize(debugInfo); |
| paramCountToCosts |
| .computeIfAbsent(debugInfo.getParameterCount(), DebugRepresentation.CostSummary::new) |
| .addCost(lastPc, debugInfoCost); |
| } |
| } |
| // Second compute the cost of converting to a pc encoding. |
| paramCountToCosts.forEach((ignored, summary) -> summary.computeConversionCosts()); |
| // The result is stored on the virtual files for thread safety. |
| // TODO(b/220999985): Consider just passing this to the line number optimizer once fixed. |
| file.setDebugRepresentation(new DebugRepresentation(paramCountToCosts)); |
| } |
| |
| private boolean usesPcEncoding(DexEncodedMethod method) { |
| DexCode code = method.getCode().asDexCode(); |
| DexDebugInfo debugInfo = code.getDebugInfo(); |
| int paramCount = debugInfo.getParameterCount(); |
| CostSummary conversionInfo = paramToInfo.get(paramCount); |
| if (conversionInfo.cutoff < 0) { |
| return false; |
| } |
| Instruction lastInstruction = getLastExecutableInstruction(code); |
| if (lastInstruction == null) { |
| return false; |
| } |
| int maxPc = lastInstruction.getOffset(); |
| return maxPc <= conversionInfo.cutoff; |
| } |
| |
| @Override |
| public String toString() { |
| List<CostSummary> sorted = new ArrayList<>(paramToInfo.values()); |
| sorted.sort(Comparator.comparing(i -> i.paramCount)); |
| return StringUtils.join("\n", sorted, CostSummary::toString); |
| } |
| |
| private static boolean isPcCandidate(DexEncodedMethod method) { |
| if (!method.hasCode() || !method.getCode().isDexCode()) { |
| return false; |
| } |
| DexCode code = method.getCode().asDexCode(); |
| return LineNumberOptimizer.doesContainPositions(code); |
| } |
| |
| /** The cost of representing normal debug info for all methods with this max pc value. */ |
| private static class PcNormalCost { |
| |
| final int pc; |
| int cost; |
| int methods; |
| |
| public PcNormalCost(int pc) { |
| assert pc >= 0; |
| this.pc = pc; |
| } |
| |
| void add(int cost) { |
| assert cost >= 0; |
| methods++; |
| this.cost += cost; |
| } |
| } |
| |
| /** The summary of normal costs for all debug info with a particular parameter size. */ |
| private static class CostSummary { |
| |
| private final int paramCount; |
| |
| // Values for the normal encoding costs per-pc. |
| private final Int2ReferenceMap<PcNormalCost> pcToCost = new Int2ReferenceOpenHashMap<>(); |
| private int minPc = Integer.MAX_VALUE; |
| private int maxPc = Integer.MIN_VALUE; |
| |
| // Values for the conversion costs. These are computed only after all per-pc costs are known. |
| private int cutoff; |
| private int normalPreCutoffCost; |
| private int normalPostCutoffCost; |
| |
| private CostSummary(int paramCount) { |
| assert paramCount >= 0; |
| this.paramCount = paramCount; |
| } |
| |
| private void addCost(int pc, int cost) { |
| assert pc >= 0; |
| pcToCost.computeIfAbsent(pc, PcNormalCost::new).add(cost); |
| minPc = Math.min(minPc, pc); |
| maxPc = Math.max(maxPc, pc); |
| } |
| |
| private void computeConversionCosts() { |
| assert !pcToCost.isEmpty(); |
| // Point at which it is estimated that conversion to PC-encoding is viable. |
| int currentConvertedPc = -1; |
| // The normal cost of the part that is viable for conversion (this is just for debugging). |
| int normalConvertedCost = 0; |
| // The normal cost of the part that is not yet part of the converted range. |
| int normalOutstandingCost = 0; |
| // Iterate in ascending order as the point conversion cost is the sum of the preceding costs. |
| int[] sortedPcs = new int[pcToCost.size()]; |
| IntIterators.unwrap(pcToCost.keySet().iterator(), sortedPcs); |
| Arrays.sort(sortedPcs); |
| for (int currentPc : sortedPcs) { |
| PcNormalCost pcSummary = pcToCost.get(currentPc); |
| // The cost of the debug info unconverted is the sum of the unconverted up to this point. |
| normalOutstandingCost += pcSummary.cost; |
| // The cost of the conversion is the delta between the already converted and the current. |
| // This does not account for the header overhead on converting the first point. However, |
| // the few bytes overhead per param-count should not affect much. |
| int costToConvert = currentPc - currentConvertedPc; |
| // If the estimated cost is larger we convert. The order here could be either way as |
| // both the normal cost and converted cost are estimates. Canonicalization could reduce |
| // the former and compaction could reduce the latter. |
| if (normalOutstandingCost > costToConvert) { |
| normalConvertedCost += normalOutstandingCost; |
| normalOutstandingCost = 0; |
| currentConvertedPc = currentPc; |
| } |
| } |
| cutoff = currentConvertedPc; |
| normalPreCutoffCost = normalConvertedCost; |
| normalPostCutoffCost = normalOutstandingCost; |
| assert cutoff >= -1; |
| assert normalPreCutoffCost >= 0; |
| assert normalPostCutoffCost >= 0; |
| assert preCutoffPcCost() >= 0; |
| assert postCutoffPcCost() >= 0; |
| } |
| |
| private int preCutoffPcCost() { |
| return cutoff > 0 ? PcBasedDebugInfo.estimatedWriteSize(paramCount, cutoff) : 0; |
| } |
| |
| private int postCutoffPcCost() { |
| return cutoff < maxPc ? PcBasedDebugInfo.estimatedWriteSize(paramCount, maxPc - cutoff) : 0; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| builder |
| .append("p:") |
| .append(paramCount) |
| .append(", c:") |
| .append(cutoff) |
| .append(", m:") |
| .append(maxPc); |
| if (cutoff > 0) { |
| builder |
| .append(", preCutNormal:") |
| .append(normalPreCutoffCost) |
| .append(", preCutPC:") |
| .append(preCutoffPcCost()); |
| } |
| if (cutoff < maxPc) { |
| builder |
| .append(", postCutNormal:") |
| .append(normalPostCutoffCost) |
| .append(", postCutPC:") |
| .append(postCutoffPcCost()); |
| } |
| return builder.toString(); |
| } |
| } |
| |
| private static Instruction getLastExecutableInstruction(DexCode code) { |
| Instruction lastInstruction = null; |
| for (Instruction instruction : code.instructions) { |
| if (!instruction.isPayload()) { |
| lastInstruction = instruction; |
| } |
| } |
| return lastInstruction; |
| } |
| |
| private static int estimatedDebugInfoSize(DexDebugInfo info) { |
| if (info.isPcBasedInfo()) { |
| return info.asPcBasedInfo().estimatedWriteSize(); |
| } |
| // Each event is a single byte so we take the event length as the cost of the info. |
| // Note that the line number optimizer could reduce the line diffs such that deltas are |
| // smaller, but this is likely a very good estimate of the actual cost. |
| int parameterCount = info.getParameterCount(); |
| int eventCount = info.asEventBasedInfo().events.length; |
| // Size: startline(0) + paramCount + null-array[paramCount] + eventCount + 1(end-event) |
| return LebUtils.sizeAsUleb128(0) |
| + LebUtils.sizeAsUleb128(parameterCount) |
| + LebUtils.sizeAsUleb128(0) * parameterCount |
| + eventCount |
| + 1; |
| } |
| } |