blob: c5068678f69c206acc3504c337d927addb031d76 [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.ir.conversion;
import com.android.tools.r8.graph.AppInfoWithSubtyping;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
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.DexType;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.graph.UseRegistry;
import com.android.tools.r8.ir.code.Invoke;
import com.android.tools.r8.ir.code.Invoke.Type;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Call graph representation.
* <p>
* Each node in the graph contain the methods called and the calling methods. For virtual and
* interface calls all potential calls from subtypes are recorded.
* <p>
* Only methods in the program - not library methods - are represented.
* <p>
* The directional edges are represented as sets of nodes in each node (called methods and callees).
* <p>
* A call from method <code>a</code> to method <code>b</code> is only present once no matter how
* many calls of <code>a</code> there are in <code>a</code>.
* <p>
* Recursive calls are not present.
*/
public class CallGraph {
private class Node {
public final DexEncodedMethod method;
private int invokeCount = 0;
private boolean isRecursive = false;
// Outgoing calls from this method.
public final Set<Node> calls = new LinkedHashSet<>();
// Incoming calls to this method.
public final Set<Node> callees = new LinkedHashSet<>();
private Node(DexEncodedMethod method) {
this.method = method;
}
public boolean isBridge() {
return method.accessFlags.isBridge();
}
private void addCalls(Node method) {
calls.add(method);
}
private void addCaller(Node method) {
callees.add(method);
}
boolean isRecursive() {
return isRecursive;
}
boolean isLeaf() {
return calls.isEmpty();
}
int callDegree() {
return calls.size();
}
@Override
public int hashCode() {
return method.hashCode();
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("MethodNode for: ");
builder.append(method.qualifiedName());
builder.append(" (");
builder.append(calls.size());
builder.append(" calls, ");
builder.append(callees.size());
builder.append(" callees");
if (isBridge()) {
builder.append(", bridge");
}
if (isRecursive()) {
builder.append(", recursive");
}
builder.append(", invoke count " + invokeCount);
builder.append(").\n");
if (calls.size() > 0) {
builder.append("Calls:\n");
for (Node call : calls) {
builder.append(" ");
builder.append(call.method.qualifiedName());
builder.append("\n");
}
}
if (callees.size() > 0) {
builder.append("Callees:\n");
for (Node callee : callees) {
builder.append(" ");
builder.append(callee.method.qualifiedName());
builder.append("\n");
}
}
return builder.toString();
}
}
public class Leaves {
private final List<DexEncodedMethod> leaves;
private final boolean brokeCycles;
private final Map<DexEncodedMethod, Set<DexEncodedMethod>> cycleBreakingCalls;
private Leaves(List<DexEncodedMethod> leaves, boolean brokeCycles,
Map<DexEncodedMethod, Set<DexEncodedMethod>> cycleBreakingCalls) {
this.leaves = leaves;
this.brokeCycles = brokeCycles;
this.cycleBreakingCalls = cycleBreakingCalls;
assert brokeCycles == (cycleBreakingCalls.size() != 0);
}
public int size() {
return leaves.size();
}
public List<DexEncodedMethod> getLeaves() {
return leaves;
}
public boolean brokeCycles() {
return brokeCycles;
}
/**
* Calls that were broken to produce the leaves.
*
* If {@link Leaves#breakCycles()} return <code>true</code> this provides the calls for each
* leaf that were broken.
*
* NOTE: The broken calls are not confined to the set of leaves.
*/
public Map<DexEncodedMethod, Set<DexEncodedMethod>> getCycleBreakingCalls() {
return cycleBreakingCalls;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Leaves: ");
builder.append(leaves.size());
builder.append("\n");
builder.append(brokeCycles ? "Call cycles broken" : "No call cycles broken");
return builder.toString();
}
}
private final Map<DexEncodedMethod, Node> nodes = new LinkedHashMap<>();
private List<Node> leaves = null;
private Set<DexEncodedMethod> singleCallSite = Sets.newIdentityHashSet();
private Set<DexEncodedMethod> doubleCallSite = Sets.newIdentityHashSet();
public static CallGraph build(DexApplication application, AppInfoWithSubtyping appInfo,
GraphLense graphLense) {
CallGraph graph = new CallGraph();
for (DexClass clazz : application.classes()) {
for (DexEncodedMethod method : clazz.directMethods()) {
Node node = graph.ensureMethodNode(method);
InvokeExtractor extractor = new InvokeExtractor(appInfo, graphLense, node, graph);
method.registerReachableDefinitions(extractor);
}
for (DexEncodedMethod method : clazz.virtualMethods()) {
Node node = graph.ensureMethodNode(method);
InvokeExtractor extractor = new InvokeExtractor(appInfo, graphLense, node, graph);
method.registerReachableDefinitions(extractor);
}
}
assert allMethodsExists(application, graph);
graph.fillCallSiteSets(appInfo);
graph.fillInitialLeaves();
return graph;
}
/**
* Check if the <code>method</code> is guaranteed to only have a single call site.
* <p>
* For pinned methods (methods kept through Proguard keep rules) this will always answer
* <code>false</code>.
*/
public boolean hasSingleCallSite(DexEncodedMethod method) {
return singleCallSite.contains(method);
}
public boolean hasDoubleCallSite(DexEncodedMethod method) {
return doubleCallSite.contains(method);
}
private void fillCallSiteSets(AppInfoWithSubtyping appInfo) {
assert singleCallSite.isEmpty();
AppInfoWithLiveness liveAppInfo = appInfo.withLiveness();
if (liveAppInfo == null) {
return;
}
for (Node value : nodes.values()) {
// For non-pinned methods we know the exact number of call sites.
if (!appInfo.withLiveness().pinnedItems.contains(value.method)) {
if (value.invokeCount == 1) {
singleCallSite.add(value.method);
} else if (value.invokeCount == 2) {
doubleCallSite.add(value.method);
}
}
}
}
private void fillInitialLeaves() {
assert leaves == null;
leaves = new ArrayList<>();
for (Node node : nodes.values()) {
if (node.isLeaf()) {
leaves.add(node);
}
}
}
private static boolean allMethodsExists(DexApplication application, CallGraph graph) {
for (DexProgramClass clazz : application.classes()) {
for (DexEncodedMethod method : clazz.directMethods()) {
assert graph.nodes.get(method) != null;
}
for (DexEncodedMethod method : clazz.virtualMethods()) {
assert graph.nodes.get(method) != null;
}
}
return true;
}
/**
* Remove all leaves (nodes with an call (outgoing) degree of 0).
* <p>
*
* @return List of {@link DexEncodedMethod} of the leaves removed.
*/
private List<DexEncodedMethod> removeLeaves() {
List<DexEncodedMethod> result = new ArrayList<>();
List<Node> newLeaves = new ArrayList<>();
for (Node leaf : leaves) {
assert nodes.containsKey(leaf.method) && nodes.get(leaf.method).calls.isEmpty();
remove(leaf, newLeaves);
result.add(leaf.method);
}
leaves = newLeaves;
return result;
}
/**
* Pick the next set of leaves (nodes with an call (outgoing) degree of 0) if any.
* <p>
* If the graph has no leaves then some cycles in the graph will be broken to create a set of
* leaves. See {@link #breakCycles} on how cycles are broken. This ensures that at least one
* leave is returned if the graph is not empty.
* <p>
*
* @return object with the leaves as a List of {@link DexEncodedMethod} and <code>boolean</code>
* indication of whether cycles were broken to produce leaves. <code>null</code> if the graph is
* empty.
*/
public Leaves pickLeaves() {
boolean cyclesBroken = false;
Map<DexEncodedMethod, Set<DexEncodedMethod>> brokenCalls = Collections.emptyMap();
if (isEmpty()) {
return null;
}
List<DexEncodedMethod> leaves = removeLeaves();
if (leaves.size() == 0) {
// The graph had no more leaves, so break cycles to construct leaves.
brokenCalls = breakCycles();
cyclesBroken = true;
leaves = removeLeaves();
}
assert leaves.size() > 0;
for (DexEncodedMethod leaf : leaves) {
assert !leaf.isProcessed();
}
return new Leaves(leaves, cyclesBroken, brokenCalls);
}
/**
* Break some cycles in the graph by removing edges.
* <p>
* This will find the lowest call (outgoing) degree in the graph. Then go through all nodes with
* that call (outgoing) degree, and remove all call (outgoing) edges from nodes with that call
* (outgoing) degree.
* <p>
* It will avoid removing edges from bridge-methods if possible.
* <p>
* Returns the calls that were broken.
*/
private Map<DexEncodedMethod, Set<DexEncodedMethod>> breakCycles() {
Map<DexEncodedMethod, Set<DexEncodedMethod>> brokenCalls = new IdentityHashMap<>();
// Break non bridges with degree 1.
int minDegree = nodes.size();
for (Node node : nodes.values()) {
// Break cycles and add all leaves created in the process.
if (!node.isBridge() && node.callDegree() <= 1) {
assert node.callDegree() == 1;
Set<DexEncodedMethod> calls = removeAllCalls(node);
leaves.add(node);
brokenCalls.put(node.method, calls);
} else {
minDegree = Integer.min(minDegree, node.callDegree());
}
}
// Return if new leaves were created.
if (leaves.size() > 0) {
return brokenCalls;
}
// Break methods with the found minimum degree and add all leaves created in the process.
for (Node node : nodes.values()) {
if (node.callDegree() <= minDegree) {
assert node.callDegree() == minDegree;
Set<DexEncodedMethod> calls = removeAllCalls(node);
leaves.add(node);
brokenCalls.put(node.method, calls);
}
}
assert leaves.size() > 0;
return brokenCalls;
}
synchronized private Node ensureMethodNode(DexEncodedMethod method) {
return nodes.computeIfAbsent(method, k -> new Node(method));
}
synchronized private void addCall(Node caller, Node callee) {
assert caller != null;
assert callee != null;
if (caller != callee) {
caller.addCalls(callee);
callee.addCaller(caller);
} else {
caller.isRecursive = true;
}
callee.invokeCount++;
}
private Set<DexEncodedMethod> removeAllCalls(Node node) {
Set<DexEncodedMethod> calls = Sets.newIdentityHashSet();
for (Node call : node.calls) {
calls.add(call.method);
call.callees.remove(node);
}
node.calls.clear();
return calls;
}
private void remove(Node node, List<Node> leaves) {
assert node != null;
for (Node callee : node.callees) {
boolean removed = callee.calls.remove(node);
if (callee.isLeaf()) {
leaves.add(callee);
}
assert removed;
}
nodes.remove(node.method);
}
public boolean isEmpty() {
return nodes.size() == 0;
}
public void dump() {
nodes.forEach((m, n) -> System.out.println(n + "\n"));
}
private static class InvokeExtractor extends UseRegistry {
AppInfoWithSubtyping appInfo;
GraphLense graphLense;
Node caller;
CallGraph graph;
InvokeExtractor(AppInfoWithSubtyping appInfo, GraphLense graphLense, Node caller,
CallGraph graph) {
this.appInfo = appInfo;
this.graphLense = graphLense;
this.caller = caller;
this.graph = graph;
}
private void processInvoke(DexEncodedMethod source, Invoke.Type type, DexMethod method) {
method = graphLense.lookupMethod(method, source);
DexEncodedMethod definition = appInfo.lookup(type, method);
if (definition != null) {
assert !source.accessFlags.isBridge() || definition != caller.method;
DexType definitionHolder = definition.method.getHolder();
assert definitionHolder.isClassType();
if (!appInfo.definitionFor(definitionHolder).isLibraryClass()) {
Node callee = graph.ensureMethodNode(definition);
graph.addCall(caller, callee);
// For virtual and interface calls add all potential targets that could be called.
if (type == Type.VIRTUAL || type == Type.INTERFACE) {
Set<DexEncodedMethod> possibleTargets;
if (definitionHolder.isInterface()) {
possibleTargets = appInfo.lookupInterfaceTargets(definition.method);
} else {
possibleTargets = appInfo.lookupVirtualTargets(definition.method);
}
for (DexEncodedMethod possibleTarget : possibleTargets) {
if (possibleTarget != definition) {
DexClass possibleTargetClass =
appInfo.definitionFor(possibleTarget.method.getHolder());
if (possibleTargetClass != null && !possibleTargetClass.isLibraryClass()) {
callee = graph.ensureMethodNode(possibleTarget);
graph.addCall(caller, callee);
}
}
}
}
}
}
}
@Override
public boolean registerInvokeVirtual(DexMethod method) {
processInvoke(caller.method, Type.VIRTUAL, method);
return false;
}
@Override
public boolean registerInvokeDirect(DexMethod method) {
processInvoke(caller.method, Type.DIRECT, method);
return false;
}
@Override
public boolean registerInvokeStatic(DexMethod method) {
processInvoke(caller.method, Type.STATIC, method);
return false;
}
@Override
public boolean registerInvokeInterface(DexMethod method) {
processInvoke(caller.method, Type.INTERFACE, method);
return false;
}
@Override
public boolean registerInvokeSuper(DexMethod method) {
processInvoke(caller.method, Type.SUPER, method);
return false;
}
@Override
public boolean registerInstanceFieldWrite(DexField field) {
return false;
}
@Override
public boolean registerInstanceFieldRead(DexField field) {
return false;
}
@Override
public boolean registerNewInstance(DexType type) {
return false;
}
@Override
public boolean registerStaticFieldRead(DexField field) {
return false;
}
@Override
public boolean registerStaticFieldWrite(DexField field) {
return false;
}
@Override
public boolean registerTypeReference(DexType type) {
return false;
}
}
}