|
| 1 | +/* |
| 2 | + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. |
| 3 | + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | + * |
| 5 | + * This code is free software; you can redistribute it and/or modify it |
| 6 | + * under the terms of the GNU General Public License version 2 only, as |
| 7 | + * published by the Free Software Foundation. Oracle designates this |
| 8 | + * particular file as subject to the "Classpath" exception as provided |
| 9 | + * by Oracle in the LICENSE file that accompanied this code. |
| 10 | + * |
| 11 | + * This code is distributed in the hope that it will be useful, but WITHOUT |
| 12 | + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | + * version 2 for more details (a copy is included in the LICENSE file that |
| 15 | + * accompanied this code). |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU General Public License version |
| 18 | + * 2 along with this work; if not, write to the Free Software Foundation, |
| 19 | + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | + * |
| 21 | + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| 22 | + * or visit www.oracle.com if you need additional information or have any |
| 23 | + * questions. |
| 24 | + */ |
| 25 | +package com.oracle.graal.pointsto.reports; |
| 26 | + |
| 27 | +import com.oracle.graal.pointsto.BigBang; |
| 28 | +import com.oracle.graal.pointsto.meta.AnalysisMethod; |
| 29 | +import com.oracle.graal.pointsto.reports.CallTreePrinter.InvokeNode; |
| 30 | +import com.oracle.graal.pointsto.reports.CallTreePrinter.MethodNode; |
| 31 | +import com.oracle.graal.pointsto.reports.CallTreePrinter.MethodNodeReference; |
| 32 | +import com.oracle.graal.pointsto.reports.CallTreePrinter.Node; |
| 33 | +import jdk.vm.ci.meta.JavaKind; |
| 34 | +import jdk.vm.ci.meta.ResolvedJavaMethod; |
| 35 | +import jdk.vm.ci.meta.ResolvedJavaType; |
| 36 | + |
| 37 | +import java.io.File; |
| 38 | +import java.io.PrintWriter; |
| 39 | +import java.util.ArrayList; |
| 40 | +import java.util.Arrays; |
| 41 | +import java.util.Collection; |
| 42 | +import java.util.HashMap; |
| 43 | +import java.util.HashSet; |
| 44 | +import java.util.Iterator; |
| 45 | +import java.util.List; |
| 46 | +import java.util.Map; |
| 47 | +import java.util.Objects; |
| 48 | +import java.util.Set; |
| 49 | +import java.util.concurrent.atomic.AtomicInteger; |
| 50 | +import java.util.regex.Matcher; |
| 51 | +import java.util.regex.Pattern; |
| 52 | +import java.util.stream.Collectors; |
| 53 | +import java.util.stream.Stream; |
| 54 | + |
| 55 | +public class CallTreeCypher { |
| 56 | + |
| 57 | + private static final AtomicInteger virtualNodeId = new AtomicInteger(-1); |
| 58 | + public static final Pattern DISPLAY_PATTERN = Pattern.compile( |
| 59 | + "\\b[a-zA-Z]|[A-Z]|\\."); |
| 60 | + |
| 61 | + public static void print(BigBang bigbang, String path, String reportName) { |
| 62 | + // Re-initialize method ids back to 0 to better diagnose disparities |
| 63 | + MethodNode.methodId = 0; |
| 64 | + |
| 65 | + CallTreePrinter printer = new CallTreePrinter(bigbang); |
| 66 | + printer.buildCallTree(); |
| 67 | + |
| 68 | + // Set virtual node at next available method id |
| 69 | + virtualNodeId.set(MethodNode.methodId); |
| 70 | + |
| 71 | + printAll(printer.methodToNode, path, reportName); |
| 72 | + } |
| 73 | + |
| 74 | + private static void printAll(Map<AnalysisMethod, MethodNode> methodToNode, String path, String reportName) { |
| 75 | + Set<Integer> entryPointIds = new HashSet<>(); |
| 76 | + Set<MethodNode> nonVirtualNodes = new HashSet<>(); |
| 77 | + Map<List<String>, Integer> virtualNodes = new HashMap<>(); |
| 78 | + |
| 79 | + Map<Integer, Set<BciEndEdge>> directEdges = new HashMap<>(); |
| 80 | + Map<Integer, Set<BciEndEdge>> virtualEdges = new HashMap<>(); |
| 81 | + Map<Integer, Set<Integer>> overridenByEdges = new HashMap<>(); |
| 82 | + |
| 83 | + final Iterator<MethodNode> iterator = methodToNode.values().stream().filter(n -> n.isEntryPoint).iterator(); |
| 84 | + while (iterator.hasNext()) { |
| 85 | + final MethodNode node = iterator.next(); |
| 86 | + entryPointIds.add(node.id); |
| 87 | + walkNodes(node, directEdges, virtualEdges, overridenByEdges, virtualNodes, nonVirtualNodes); |
| 88 | + } |
| 89 | + |
| 90 | + final String vmFileName = ReportUtils.report("call tree for vm entry point", path + File.separatorChar + "reports", "csv_call_tree_vm_" + reportName, "csv", |
| 91 | + CallTreeCypher::printVMEntryPoint); |
| 92 | + |
| 93 | + final String methodsFileName = ReportUtils.report("call tree for methods", path + File.separatorChar + "reports", "csv_call_tree_methods_" + reportName, "csv", |
| 94 | + writer -> printMethodNodes(methodToNode.values(), writer)); |
| 95 | + |
| 96 | + final String virtualMethodsFileName = ReportUtils.report("call tree for virtual methods", path + File.separatorChar + "reports", "csv_call_tree_virtual_methods_" + reportName, "csv", |
| 97 | + writer -> printVirtualNodes(virtualNodes, writer)); |
| 98 | + |
| 99 | + final String entryPointsFileName = ReportUtils.report("call tree for entry points", path + File.separatorChar + "reports", "csv_call_tree_entry_points_" + reportName, "csv", |
| 100 | + writer -> printEntryPointIds(entryPointIds, writer)); |
| 101 | + |
| 102 | + final String directEdgesFileName = ReportUtils.report("call tree for direct edges", path + File.separatorChar + "reports", "csv_call_tree_direct_edges_" + reportName, "csv", |
| 103 | + writer -> printBciEdges(directEdges, writer)); |
| 104 | + |
| 105 | + final String overridenByEdgesFileName = ReportUtils.report("call tree for overriden by edges", path + File.separatorChar + "reports", "csv_call_tree_override_by_edges_" + reportName, "csv", |
| 106 | + writer -> printNonBciEdges(overridenByEdges, writer)); |
| 107 | + |
| 108 | + final String virtualEdgesFileName = ReportUtils.report("call tree for virtual edges", path + File.separatorChar + "reports", "csv_call_tree_virtual_edges_" + reportName, "csv", |
| 109 | + writer -> printBciEdges(virtualEdges, writer)); |
| 110 | + |
| 111 | + ReportUtils.report("call tree cypher", path + File.separatorChar + "reports", "cypher_call_tree_" + reportName, "cypher", |
| 112 | + writer -> printCypher(vmFileName, methodsFileName, virtualMethodsFileName, entryPointsFileName, directEdgesFileName, overridenByEdgesFileName, virtualEdgesFileName, writer)); |
| 113 | + } |
| 114 | + |
| 115 | + private static void printCypher(String vmFileName, String methodsFileName, String virtualMethodsFileName, String entryPointsFileName, String directEdgesFileName, String overridenByEdgesFileName, |
| 116 | + String virtualEdgesFileName, PrintWriter writer) { |
| 117 | + writer.println("CREATE CONSTRAINT unique_vm_id ON (v:VM) ASSERT v.vmId IS UNIQUE;"); |
| 118 | + writer.println("CREATE CONSTRAINT unique_method_id ON (m:Method) ASSERT m.methodId IS UNIQUE;"); |
| 119 | + writer.println(""); |
| 120 | + writer.println(String.format("LOAD CSV WITH HEADERS FROM 'file:///%s' AS row", vmFileName)); |
| 121 | + writer.println("MERGE (v:VM {vmId: row.Id, name: row.Name})"); |
| 122 | + writer.println("RETURN count(v);"); |
| 123 | + writer.println(""); |
| 124 | + writer.println(String.format("LOAD CSV WITH HEADERS FROM 'file:///%s' AS row", methodsFileName)); |
| 125 | + writer.println("MERGE (m:Method {methodId: row.Id, name: row.Name, type: row.Type, parameters: row.Parameters, return: row.Return, display: row.Display})"); |
| 126 | + writer.println("RETURN count(m);"); |
| 127 | + writer.println(""); |
| 128 | + writer.println(String.format("LOAD CSV WITH HEADERS FROM 'file:///%s' AS row", virtualMethodsFileName)); |
| 129 | + writer.println("MERGE (m:Method {methodId: row.Id, name: row.Name, type: row.Type, parameters: row.Parameters, return: row.Return, display: row.Display})"); |
| 130 | + writer.println("RETURN count(m);"); |
| 131 | + writer.println(""); |
| 132 | + writer.println(String.format("LOAD CSV WITH HEADERS FROM 'file:///%s' AS row", entryPointsFileName)); |
| 133 | + writer.println("MATCH (m:Method {methodId: row.Id})"); |
| 134 | + writer.println("MATCH (v:VM {vmId: '0'})"); |
| 135 | + writer.println("MERGE (v)-[:ENTRY]->(m)"); |
| 136 | + writer.println("RETURN count(*);"); |
| 137 | + writer.println(""); |
| 138 | + writer.println(String.format("LOAD CSV WITH HEADERS FROM 'file:///%s' AS row", directEdgesFileName)); |
| 139 | + writer.println("MATCH (m1:Method {methodId: row.StartId})"); |
| 140 | + writer.println("MATCH (m2:Method {methodId: row.EndId})"); |
| 141 | + writer.println("MERGE (m1)-[:DIRECT {bci: row.BytecodeIndexes}]->(m2)"); |
| 142 | + writer.println("RETURN count(*);"); |
| 143 | + writer.println(""); |
| 144 | + writer.println(String.format("LOAD CSV WITH HEADERS FROM 'file:///%s' AS row", overridenByEdgesFileName)); |
| 145 | + writer.println("MATCH (m1:Method {methodId: row.StartId})"); |
| 146 | + writer.println("MATCH (m2:Method {methodId: row.EndId})"); |
| 147 | + writer.println("MERGE (m1)-[:OVERRIDEN_BY]->(m2)"); |
| 148 | + writer.println("RETURN count(*);"); |
| 149 | + writer.println(""); |
| 150 | + writer.println(String.format("LOAD CSV WITH HEADERS FROM 'file:///%s' AS row", virtualEdgesFileName)); |
| 151 | + writer.println("MATCH (m1:Method {methodId: row.StartId})"); |
| 152 | + writer.println("MATCH (m2:Method {methodId: row.EndId})"); |
| 153 | + writer.println("MERGE (m1)-[:VIRTUAL {bci: row.BytecodeIndexes}]->(m2)"); |
| 154 | + writer.println("RETURN count(*);"); |
| 155 | + } |
| 156 | + |
| 157 | + private static void printVMEntryPoint(PrintWriter writer) { |
| 158 | + writer.println(convertToCSV("Id", "Name")); |
| 159 | + writer.println(convertToCSV("0", "VM")); |
| 160 | + } |
| 161 | + |
| 162 | + private static void walkNodes(MethodNode methodNode, Map<Integer, Set<BciEndEdge>> directEdges, Map<Integer, Set<BciEndEdge>> virtualEdges, Map<Integer, Set<Integer>> overridenByEdges, |
| 163 | + Map<List<String>, Integer> virtualNodes, Set<MethodNode> nonVirtualNodes) { |
| 164 | + for (InvokeNode invoke : methodNode.invokes) { |
| 165 | + if (invoke.isDirectInvoke) { |
| 166 | + if (invoke.callees.size() > 0) { |
| 167 | + Node calleeNode = invoke.callees.get(0); |
| 168 | + addDirectEdge(methodNode.id, invoke, calleeNode, directEdges, nonVirtualNodes); |
| 169 | + if (calleeNode instanceof MethodNode) { |
| 170 | + walkNodes((MethodNode) calleeNode, directEdges, virtualEdges, overridenByEdges, virtualNodes, nonVirtualNodes); |
| 171 | + } |
| 172 | + } |
| 173 | + } else { |
| 174 | + final int nodeId = addVirtualNode(invoke, virtualNodes); |
| 175 | + addVirtualMethodEdge(methodNode.id, invoke, nodeId, virtualEdges); |
| 176 | + for (Node calleeNode : invoke.callees) { |
| 177 | + addOverridenByEdge(nodeId, calleeNode, overridenByEdges, nonVirtualNodes); |
| 178 | + if (calleeNode instanceof MethodNode) { |
| 179 | + walkNodes((MethodNode) calleeNode, directEdges, virtualEdges, overridenByEdges, virtualNodes, nonVirtualNodes); |
| 180 | + } |
| 181 | + } |
| 182 | + } |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + private static void printMethodNodes(Collection<MethodNode> methods, PrintWriter writer) { |
| 187 | + writer.println(convertToCSV("Id", "Name", "Type", "Parameters", "Return", "Display")); |
| 188 | + methods.stream() |
| 189 | + .map(CallTreeCypher::methodNodeInfo) |
| 190 | + .map(CallTreeCypher::convertToCSV) |
| 191 | + .forEach(writer::println); |
| 192 | + } |
| 193 | + |
| 194 | + private static List<String> methodNodeInfo(MethodNode method) { |
| 195 | + return resolvedJavaMethodInfo(method.id, method.method); |
| 196 | + } |
| 197 | + |
| 198 | + private static int addVirtualNode(InvokeNode node, Map<List<String>, Integer> virtualNodes) { |
| 199 | + final List<String> virtualMethodInfo = virtualMethodInfo(node.targetMethod); |
| 200 | + return virtualNodes.computeIfAbsent(virtualMethodInfo, k -> CallTreeCypher.virtualNodeId.getAndIncrement()); |
| 201 | + } |
| 202 | + |
| 203 | + private static void addVirtualMethodEdge(int startId, InvokeNode invoke, int endId, Map<Integer, Set<BciEndEdge>> edges) { |
| 204 | + Set<BciEndEdge> nodeEdges = edges.computeIfAbsent(startId, k -> new HashSet<>()); |
| 205 | + nodeEdges.add(new BciEndEdge(endId, bytecodeIndexes(invoke))); |
| 206 | + } |
| 207 | + |
| 208 | + private static void addDirectEdge(int nodeId, InvokeNode invoke, Node calleeNode, Map<Integer, Set<BciEndEdge>> edges, Set<MethodNode> nodes) { |
| 209 | + Set<BciEndEdge> nodeEdges = edges.computeIfAbsent(nodeId, k -> new HashSet<>()); |
| 210 | + MethodNode methodNode = calleeNode instanceof MethodNode |
| 211 | + ? (MethodNode) calleeNode |
| 212 | + : ((MethodNodeReference) calleeNode).methodNode; |
| 213 | + nodes.add(methodNode); |
| 214 | + nodeEdges.add(new BciEndEdge(methodNode.id, bytecodeIndexes(invoke))); |
| 215 | + } |
| 216 | + |
| 217 | + private static List<Integer> bytecodeIndexes(InvokeNode node) { |
| 218 | + return Stream.of(node.sourceReferences) |
| 219 | + .map(source -> source.bci) |
| 220 | + .collect(Collectors.toList()); |
| 221 | + } |
| 222 | + |
| 223 | + private static void printVirtualNodes(Map<List<String>, Integer> virtualNodes, PrintWriter writer) { |
| 224 | + writer.println(convertToCSV("Id", "Name", "Type", "Parameters", "Return", "Display")); |
| 225 | + virtualNodes.entrySet().stream() |
| 226 | + .map(CallTreeCypher::virtualMethodAndIdInfo) |
| 227 | + .map(CallTreeCypher::convertToCSV) |
| 228 | + .forEach(writer::println); |
| 229 | + } |
| 230 | + |
| 231 | + private static List<String> virtualMethodAndIdInfo(Map.Entry<List<String>, Integer> entry) { |
| 232 | + final List<String> methodInfo = entry.getKey(); |
| 233 | + final List<String> result = new ArrayList<>(methodInfo.size() + 1); |
| 234 | + result.add(String.valueOf(entry.getValue())); |
| 235 | + for (int i = 1; i < methodInfo.size(); i++) { |
| 236 | + result.add(i, methodInfo.get(i)); |
| 237 | + } |
| 238 | + return result; |
| 239 | + } |
| 240 | + |
| 241 | + private static void printEntryPointIds(Set<Integer> entryPoints, PrintWriter writer) { |
| 242 | + writer.println(convertToCSV("Id")); |
| 243 | + entryPoints.forEach(writer::println); |
| 244 | + } |
| 245 | + |
| 246 | + private static void addOverridenByEdge(int nodeId, Node calleeNode, Map<Integer, Set<Integer>> edges, Set<MethodNode> nodes) { |
| 247 | + Set<Integer> nodeEdges = edges.computeIfAbsent(nodeId, k -> new HashSet<>()); |
| 248 | + MethodNode methodNode = calleeNode instanceof MethodNode |
| 249 | + ? (MethodNode) calleeNode |
| 250 | + : ((MethodNodeReference) calleeNode).methodNode; |
| 251 | + nodes.add(methodNode); |
| 252 | + nodeEdges.add(methodNode.id); |
| 253 | + } |
| 254 | + |
| 255 | + private static void printBciEdges(Map<Integer, Set<BciEndEdge>> edges, PrintWriter writer) { |
| 256 | + final Set<BciEdge> idEdges = edges.entrySet().stream() |
| 257 | + .flatMap(entry -> entry.getValue().stream().map(endId -> new BciEdge(entry.getKey(), endId))) |
| 258 | + .collect(Collectors.toSet()); |
| 259 | + |
| 260 | + writer.println(convertToCSV("StartId", "EndId", "BytecodeIndexes")); |
| 261 | + idEdges.stream() |
| 262 | + .map(edge -> convertToCSV(String.valueOf(edge.startId), String.valueOf(edge.endEdge.id), showBytecodeIndexes(edge.endEdge.bytecodeIndexes))) |
| 263 | + .forEach(writer::println); |
| 264 | + } |
| 265 | + |
| 266 | + private static String showBytecodeIndexes(List<Integer> bytecodeIndexes) { |
| 267 | + return bytecodeIndexes.stream() |
| 268 | + .map(String::valueOf) |
| 269 | + .collect(Collectors.joining("->")); |
| 270 | + } |
| 271 | + |
| 272 | + private static void printNonBciEdges(Map<Integer, Set<Integer>> edges, PrintWriter writer) { |
| 273 | + final Set<NonBciEdge> idEdges = edges.entrySet().stream() |
| 274 | + .flatMap(entry -> entry.getValue().stream().map(endId -> new NonBciEdge(entry.getKey(), endId))) |
| 275 | + .collect(Collectors.toSet()); |
| 276 | + |
| 277 | + writer.println(convertToCSV("StartId", "EndId")); |
| 278 | + idEdges.stream() |
| 279 | + .map(edge -> convertToCSV(String.valueOf(edge.startId), String.valueOf(edge.endId))) |
| 280 | + .forEach(writer::println); |
| 281 | + } |
| 282 | + |
| 283 | + private static List<String> virtualMethodInfo(AnalysisMethod method) { |
| 284 | + return resolvedJavaMethodInfo(null, method); |
| 285 | + } |
| 286 | + |
| 287 | + private static List<String> resolvedJavaMethodInfo(Integer id, ResolvedJavaMethod method) { |
| 288 | + // TODO method parameter types are opaque, but could in the future be split out and link |
| 289 | + // together |
| 290 | + // e.g. each method could BELONG to a type, and a method could have PARAMETER relationships |
| 291 | + // with N types |
| 292 | + // see https://neo4j.com/developer/guide-import-csv/#_converting_data_values_with_load_csv |
| 293 | + // for examples |
| 294 | + final String parameters = method.getSignature().getParameterCount(false) > 0 |
| 295 | + ? method.format("%P").replace(",", "") |
| 296 | + : "empty"; |
| 297 | + |
| 298 | + return Arrays.asList( |
| 299 | + id == null ? null : Integer.toString(id), |
| 300 | + method.getName(), |
| 301 | + method.getDeclaringClass().toJavaName(true), |
| 302 | + parameters, |
| 303 | + method.getSignature().getReturnType(null).toJavaName(true), |
| 304 | + display(method)); |
| 305 | + } |
| 306 | + |
| 307 | + private static String display(ResolvedJavaMethod method) { |
| 308 | + final ResolvedJavaType type = method.getDeclaringClass(); |
| 309 | + final String typeName = type.toJavaName(true); |
| 310 | + if (type.getJavaKind() == JavaKind.Object) { |
| 311 | + List<String> matchResults = new ArrayList<>(); |
| 312 | + Matcher matcher = DISPLAY_PATTERN.matcher(typeName); |
| 313 | + while (matcher.find()) { |
| 314 | + matchResults.add(matcher.toMatchResult().group()); |
| 315 | + } |
| 316 | + |
| 317 | + return String.join("", matchResults) + "." + method.getName(); |
| 318 | + } |
| 319 | + |
| 320 | + return typeName + "." + method.getName(); |
| 321 | + } |
| 322 | + |
| 323 | + private static String convertToCSV(String... data) { |
| 324 | + return String.join(",", data); |
| 325 | + } |
| 326 | + |
| 327 | + private static String convertToCSV(List<String> data) { |
| 328 | + return String.join(",", data); |
| 329 | + } |
| 330 | + |
| 331 | + private static final class NonBciEdge { |
| 332 | + |
| 333 | + final int startId; |
| 334 | + final int endId; |
| 335 | + |
| 336 | + private NonBciEdge(int startId, int endId) { |
| 337 | + this.startId = startId; |
| 338 | + this.endId = endId; |
| 339 | + } |
| 340 | + } |
| 341 | + |
| 342 | + private static final class BciEdge { |
| 343 | + final int startId; |
| 344 | + final BciEndEdge endEdge; |
| 345 | + |
| 346 | + private BciEdge(int startId, BciEndEdge endEdge) { |
| 347 | + this.startId = startId; |
| 348 | + this.endEdge = endEdge; |
| 349 | + } |
| 350 | + } |
| 351 | + |
| 352 | + private static final class BciEndEdge { |
| 353 | + final int id; |
| 354 | + final List<Integer> bytecodeIndexes; |
| 355 | + |
| 356 | + private BciEndEdge(int id, List<Integer> bytecodeIndexes) { |
| 357 | + this.id = id; |
| 358 | + this.bytecodeIndexes = bytecodeIndexes; |
| 359 | + } |
| 360 | + |
| 361 | + @Override |
| 362 | + public boolean equals(Object o) { |
| 363 | + if (this == o) { |
| 364 | + return true; |
| 365 | + } |
| 366 | + if (o == null || getClass() != o.getClass()) { |
| 367 | + return false; |
| 368 | + } |
| 369 | + BciEndEdge endEdge = (BciEndEdge) o; |
| 370 | + return id == endEdge.id && |
| 371 | + bytecodeIndexes.equals(endEdge.bytecodeIndexes); |
| 372 | + } |
| 373 | + |
| 374 | + @Override |
| 375 | + public int hashCode() { |
| 376 | + return Objects.hash(id, bytecodeIndexes); |
| 377 | + } |
| 378 | + } |
| 379 | +} |
0 commit comments