From 85a8ff50205be5765829cc0684fc90836cbd2b8a Mon Sep 17 00:00:00 2001 From: Codrut Stancu Date: Wed, 1 Feb 2023 15:38:39 +0100 Subject: [PATCH] Reachability trace prototype. - Make line separator system-ignorant (Thanks Fabio) - Rename -H:ThrowOnXXXReachable to -H:AbortOnXXXReachable (Thanks Doug) - Don't hardcode option name and rename Diagnosis Report to Reachability Diagnosis Report (Thanks Fabio) --- .../native-image/StaticAnalysisReports.md | 54 ++- .../oracle/graal/pointsto/ObjectScanner.java | 83 +++-- .../graal/pointsto/PointsToAnalysis.java | 2 +- .../pointsto/flow/MethodTypeFlowBuilder.java | 5 +- .../oracle/graal/pointsto/flow/TypeFlow.java | 8 +- .../graal/pointsto/meta/AnalysisElement.java | 338 +++++++++++++++++- .../graal/pointsto/meta/AnalysisField.java | 18 +- .../graal/pointsto/meta/AnalysisMethod.java | 25 +- .../graal/pointsto/meta/AnalysisType.java | 12 + .../InlineBeforeAnalysisGraphDecoder.java | 5 +- .../pointsto/reports/AnalysisReporter.java | 12 +- .../reports/AnalysisReportsOptions.java | 10 +- .../pointsto/reports/ObjectTreePrinter.java | 4 +- .../graal/pointsto/reports/ReportUtils.java | 8 +- .../svm/hosted/NativeImageGenerator.java | 5 +- .../analysis/ReachabilityTracePrinter.java | 199 +++++++++++ 16 files changed, 700 insertions(+), 88 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ReachabilityTracePrinter.java diff --git a/docs/reference-manual/native-image/StaticAnalysisReports.md b/docs/reference-manual/native-image/StaticAnalysisReports.md index 6ff7aefaeeec..4b009ed95014 100644 --- a/docs/reference-manual/native-image/StaticAnalysisReports.md +++ b/docs/reference-manual/native-image/StaticAnalysisReports.md @@ -12,6 +12,8 @@ The points-to analysis produces two kinds of reports: an analysis call tree and This information is produced by an intermediate step in the building process and represents the static analysis view of the call graph and heap object graph. These graphs are further transformed in the building process before they are compiled ahead-of-time into the binary and written into the binary heap, respectively. +In addition to the comprehensive report of the whole analysis universe, the points-to analysis can also produce reachability reports on why certain type/method/field is reachable. + ## Call tree The call tree is a a breadth-first tree reduction of the call graph as seen by the points-to analysis. The points-to analysis eliminates calls to methods that it determines cannot be reachable at runtime, based on the analyzed receiver types. @@ -158,7 +160,46 @@ Roots suppression/expansion: - `-H:ImageObjectTreeSuppressRoots=java.util.* -H:ImageObjectTreeExpandRoots=java.util.Locale` - suppress the expansion of all roots that start with `java.util.` but not `java.util.Locale` - `-H:ImageObjectTreeExpandRoots=*` - force the expansion of all roots, including those suppressed by default -### Report Files +## Reachability Report + +In diagnosing code size or security problems, the developer often has the need to know why certain code element (type/method/field) is reachable. +Reachability reports are designed for the purpose. +There are three options for diagnosing the reachability reasons for types, methods, and fields respectively: + +- `-H:AbortOnTypeReachable=` +- `-H:AbortOnMethodReachable=` +- `-H:AbortOnFieldReachable=` + +For each option, the right-hand side specifies the pattern of the code element to be diagnosed. + +- The syntax for specifying types and fields is the same as that of suppression/expansion (See documentation for `-H:ImageObjectTreeSuppressTypes` above). +- The syntax for specifying methods is the same as that of method filters (See documentation for `-Dgraal.MethodFilter`). + +When one of the option is enabled and the corresponding code element is reachable, a reachability trace will be dumped to a TXT file and Native Image will abort. +Here is an example of the reachability report for `-H:AbortOnTypeReachable=java.io.File`: + +``` +Type java.io.File is marked as allocated +at virtual method com.oracle.svm.core.jdk.NativeLibrarySupport.loadLibraryRelative(NativeLibrarySupport.java:105), implementation invoked +├── at virtual method com.oracle.svm.core.jdk.JNIPlatformNativeLibrarySupport.loadJavaLibrary(JNIPlatformNativeLibrarySupport.java:44), implementation invoked +│ ├── at virtual method com.oracle.svm.core.posix.PosixNativeLibrarySupport.loadJavaLibrary(PosixNativeLibraryFeature.java:117), implementation invoked +│ │ ├── at virtual method com.oracle.svm.core.posix.PosixNativeLibrarySupport.initializeBuiltinLibraries(PosixNativeLibraryFeature.java:98), implementation invoked +│ │ │ ├── at static method com.oracle.svm.core.graal.snippets.CEntryPointSnippets.initializeIsolate(CEntryPointSnippets.java:346), implementation invoked +│ │ │ │ str: static root method +│ │ │ └── type com.oracle.svm.core.posix.PosixNativeLibrarySupport is marked as in-heap +│ │ │ scanning root com.oracle.svm.core.posix.PosixNativeLibrarySupport@4839bf0d: com.oracle.svm.core.posix.PosixNativeLibrarySupport@4839bf0d embedded in +│ │ │ org.graalvm.nativeimage.ImageSingletons.lookup(ImageSingletons.java) +│ │ │ at static method org.graalvm.nativeimage.ImageSingletons.lookup(Class), intrinsified +│ │ │ at static method com.oracle.svm.core.graal.snippets.CEntryPointSnippets.createIsolate(CEntryPointSnippets.java:209), implementation invoked +│ │ └── type com.oracle.svm.core.posix.PosixNativeLibrarySupport is marked as in-heap +│ └── type com.oracle.svm.core.jdk.JNIPlatformNativeLibrarySupport is reachable +└── type com.oracle.svm.core.jdk.NativeLibrarySupport is marked as in-heap + scanning root com.oracle.svm.core.jdk.NativeLibrarySupport@6e06bbea: com.oracle.svm.core.jdk.NativeLibrarySupport@6e06bbea embedded in + org.graalvm.nativeimage.ImageSingletons.lookup(ImageSingletons.java) + at static method org.graalvm.nativeimage.ImageSingletons.lookup(Class), intrinsified +``` + +## Report Files The reports are generated in the `reports` subdirectory, relative to the build directory. When executing the `native-image` executable the build directory defaults to the working directory and can be modified using the `-H:Path=` option. @@ -169,6 +210,13 @@ The object tree report name has the structure: `object_tree__` option. The `` is in the `yyyyMMdd_HHmmss` format. -### Further Reading +The reachability reports are also located in the reports directory. +They follow the same naming convention: + +- Type reachability report: `trace_types__.txt` +- Method reachability report: `trace_methods__.txt` +- Field reachability report: `trace_fields__.txt` + +## Further Reading -* [Hosted and Runtime Options](HostedvsRuntimeOptions.md) \ No newline at end of file +* [Hosted and Runtime Options](HostedvsRuntimeOptions.md) diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java index b0faf3d53398..23962ccfdd6a 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java @@ -63,6 +63,8 @@ */ public class ObjectScanner { + private static final String INDENTATION_AFTER_NEWLINE = " "; + protected final BigBang bb; private final ReusableSet scannedObjects; private final CompletionExecutor executor; @@ -104,7 +106,8 @@ public void scanBootImageHeapRoots(Comparator fieldComparator, Co // scan the constant nodes Map embeddedRoots = bb.getUniverse().getEmbeddedRoots(); if (embeddedRootComparator != null) { - embeddedRoots.entrySet().stream().sorted(Map.Entry.comparingByValue(embeddedRootComparator)) + embeddedRoots.entrySet().stream() + .sorted(Map.Entry.comparingByValue(embeddedRootComparator)) .forEach(entry -> execute(() -> scanEmbeddedRoot(entry.getKey(), entry.getValue()))); } else { embeddedRoots.forEach((key, value) -> execute(() -> scanEmbeddedRoot(key, value))); @@ -311,11 +314,11 @@ public static AnalysisMethod buildObjectBacktrace(BigBang bb, ScanReason reason, public static AnalysisMethod buildObjectBacktrace(BigBang bb, ScanReason reason, StringBuilder objectBacktrace, String header) { ScanReason cur = reason; objectBacktrace.append(header); - objectBacktrace.append(System.lineSeparator()).append(indent).append(asString(bb, cur)); + objectBacktrace.append(System.lineSeparator()).append(indent).append(cur.toString(bb)); ScanReason rootReason = cur; cur = cur.previous; while (cur != null) { - objectBacktrace.append(System.lineSeparator()).append(indent).append(asString(bb, cur)); + objectBacktrace.append(System.lineSeparator()).append(indent).append(cur.toString(bb)); rootReason = cur.previous; cur = cur.previous; } @@ -327,39 +330,6 @@ public static AnalysisMethod buildObjectBacktrace(BigBang bb, ScanReason reason, return null; } - static String asString(BigBang bb, ScanReason reason) { - if (reason instanceof MethodParsing) { - MethodParsing mp = (MethodParsing) reason; - String str = String.format("parsing method %s reachable via the parsing context", mp.getMethod().asStackTraceElement(0)); - str += ReportUtils.parsingContext(mp.getMethod(), indent + indent); - return str; - } else if (reason instanceof FieldConstantFold) { - FieldConstantFold fieldFold = (FieldConstantFold) reason; - StackTraceElement location = fieldFold.parsedMethod.asStackTraceElement(fieldFold.bci); - if (fieldFold.field.isStatic()) { - return "trying to constant fold static field " + reason + "\n at " + location; - } else { - /* Instance field scans must have a receiver, hence the 'of'. */ - return "trying to constant fold field " + reason + " of constant \n " + asString(bb, reason.constant) + "\n at " + location; - } - } else if (reason instanceof FieldScan) { - FieldScan fieldScan = (FieldScan) reason; - if (fieldScan.field.isStatic()) { - return "reading static field " + reason + "\n at " + fieldScan.location(); - } else { - /* Instance field scans must have a receiver, hence the 'of'. */ - return "reading field " + reason + " of constant \n " + asString(bb, reason.constant); - // + "\n at " + location; - } - } else if (reason instanceof EmbeddedRootScan) { - return "scanning root " + asString(bb, reason.constant) + " embedded in \n " + reason; - } else if (reason instanceof ArrayScan) { - return "indexing into array " + asString(bb, reason.constant); - } else { - return reason.toString(); - } - } - public static String asString(BigBang bb, JavaConstant constant) { return asString(bb, constant, true); } @@ -483,6 +453,11 @@ protected ScanReason(ScanReason previous, JavaConstant constant) { public ScanReason getPrevious() { return previous; } + + @SuppressWarnings("unused") + public String toString(BigBang bb) { + return toString(); + } } public static class OtherReason extends ScanReason { @@ -542,6 +517,16 @@ public String location() { } } + @Override + public String toString(BigBang bb) { + if (field.isStatic()) { + return "reading static field " + field.format("%H.%n") + System.lineSeparator() + " at " + location(); + } else { + /* Instance field scans must have a receiver, hence the 'of'. */ + return "reading field " + field.format("%H.%n") + " of constant " + System.lineSeparator() + INDENTATION_AFTER_NEWLINE + asString(bb, constant); + } + } + @Override public String toString() { return field.format("%H.%n"); @@ -560,6 +545,18 @@ public FieldConstantFold(AnalysisField field, AnalysisMethod parsedMethod, int b this.bci = bci; } + @Override + public String toString(BigBang bb) { + StackTraceElement location = parsedMethod.asStackTraceElement(bci); + if (field.isStatic()) { + return "trying to constant fold static field " + field.format("%H.%n") + System.lineSeparator() + " at " + location; + } else { + /* Instance field scans must have a receiver, hence the 'of'. */ + return "trying to constant fold field " + field.format("%H.%n") + " of constant " + System.lineSeparator() + + INDENTATION_AFTER_NEWLINE + asString(bb, constant) + System.lineSeparator() + " at " + location; + } + } + @Override public String toString() { return field.format("%H.%n"); @@ -584,8 +581,8 @@ public AnalysisMethod getMethod() { @Override public String toString() { - String str = String.format("Parsing method %s %n", method.asStackTraceElement(0)); - str += "Parsing context:" + ReportUtils.parsingContext(method); + String str = String.format("parsing method %s reachable via the parsing context", method.asStackTraceElement(0)); + str += ReportUtils.parsingContext(method, indent + indent); return str; } } @@ -598,6 +595,11 @@ public ArrayScan(AnalysisType arrayType, JavaConstant array, ScanReason previous this.arrayType = arrayType; } + @Override + public String toString(BigBang bb) { + return "indexing into array " + asString(bb, constant); + } + @Override public String toString() { return arrayType.toJavaName(true); @@ -620,6 +622,11 @@ public AnalysisMethod getMethod() { return (AnalysisMethod) position.getMethod(); } + @Override + public String toString(BigBang bb) { + return "scanning root " + asString(bb, constant) + " embedded in " + System.lineSeparator() + INDENTATION_AFTER_NEWLINE + position.getMethod().asStackTraceElement(position.getBCI()); + } + @Override public String toString() { return position.getMethod().asStackTraceElement(position.getBCI()).toString(); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java index 30d8ade31df6..9f9624235f2f 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java @@ -314,7 +314,7 @@ public AnalysisMethod addRootMethod(AnalysisMethod aMethod, boolean invokeSpecia Consumer triggerStaticMethodFlow = (pointsToMethod) -> { postTask(() -> { pointsToMethod.registerAsDirectRootMethod(); - pointsToMethod.registerAsImplementationInvoked("root method"); + pointsToMethod.registerAsImplementationInvoked("static root method"); MethodFlowsGraphInfo flowInfo = analysisPolicy.staticRootMethodGraph(this, pointsToMethod); for (int idx = 0; idx < paramCount; idx++) { AnalysisType declaredParamType = (AnalysisType) signature.getParameterType(idx, declaringClass); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/MethodTypeFlowBuilder.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/MethodTypeFlowBuilder.java index 4f5fddd714ef..cbdb4b69e931 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/MethodTypeFlowBuilder.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/MethodTypeFlowBuilder.java @@ -309,10 +309,11 @@ protected static void registerUsedElements(PointsToAnalysis bb, StructuredGraph } else if (n instanceof ConstantNode) { ConstantNode cn = (ConstantNode) n; - if (cn.hasUsages() && cn.isJavaConstant() && cn.asJavaConstant().getJavaKind() == JavaKind.Object && cn.asJavaConstant().isNonNull()) { + JavaConstant root = cn.asJavaConstant(); + if (cn.hasUsages() && cn.isJavaConstant() && root.getJavaKind() == JavaKind.Object && root.isNonNull()) { assert StampTool.isExactType(cn); AnalysisType type = (AnalysisType) StampTool.typeOrNull(cn, bb.getMetaAccess()); - type.registerAsInHeap(AbstractAnalysisEngine.sourcePosition(cn)); + type.registerAsInHeap(new EmbeddedRootScan(AbstractAnalysisEngine.sourcePosition(cn), root)); if (registerEmbeddedRoots && !ignoreConstant(bb, cn)) { registerEmbeddedRoot(bb, cn); } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/TypeFlow.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/TypeFlow.java index 5ebacf1d4fcf..d990ee21ee3c 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/TypeFlow.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/flow/TypeFlow.java @@ -464,7 +464,7 @@ private boolean doAddObserver(PointsToAnalysis bb, TypeFlow observer) { * An observer is linked even if it is already saturated itself, hence no * 'observer.isSaturated()' check is performed here. For observers the saturation state is * that of the values flowing through and not that of the objects they observe. - * + * * Some observers may need to continue to observe the state of their receiver object until * the receiver object saturates itself, e.g., instance field stores, other observers may * deregister themselves from observing the receiver object when they saturate, e.g., @@ -562,7 +562,7 @@ public TypeState declaredTypeFilter(PointsToAnalysis bb, TypeState newState, boo * * Places where interface types need to be filtered: method parameters, method return values, * and field loads (including unsafe memory loads). - * + * * Places where interface types need not be filtered: array element loads (because all array * stores have an array store check). */ @@ -704,10 +704,10 @@ public void onObservedSaturated(@SuppressWarnings("unused") PointsToAnalysis bb, * approximation, e.g., the flow of the receiver type for a special invoke operation or of the * field declaring class for a field access operation. By default the observers don't use the * null state of the observed, therefore the non-null type flow is used. - * + * * The overloaded {@link #replacedObservedWith(PointsToAnalysis, TypeFlow)} can be used for * replacing the observed with a custom type flow. - * + * */ public void replaceObservedWith(PointsToAnalysis bb, AnalysisType newObservedType) { replacedObservedWith(bb, newObservedType.getTypeFlow(bb, false)); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisElement.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisElement.java index 7ad98ea9a6c2..e95c5af501b3 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisElement.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisElement.java @@ -27,6 +27,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Executable; +import java.util.ArrayDeque; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,12 +41,19 @@ import org.graalvm.compiler.debug.GraalError; import org.graalvm.nativeimage.hosted.Feature.DuringAnalysisAccess; +import com.oracle.graal.pointsto.BigBang; import com.oracle.graal.pointsto.ObjectScanner; +import com.oracle.graal.pointsto.ObjectScanner.MethodParsing; +import com.oracle.graal.pointsto.reports.ReportUtils; +import com.oracle.graal.pointsto.util.AnalysisError; import com.oracle.graal.pointsto.util.AnalysisFuture; import com.oracle.graal.pointsto.util.ConcurrentLightHashSet; import jdk.vm.ci.code.BytecodePosition; import jdk.vm.ci.meta.ModifiersProvider; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; public abstract class AnalysisElement implements AnnotatedElement { @@ -82,8 +91,8 @@ public final Annotation[] getDeclaredAnnotations() { * Each handler is notified only once, and then it is removed from the set. */ - private static final AtomicReferenceFieldUpdater reachableNotificationsUpdater = AtomicReferenceFieldUpdater - .newUpdater(AnalysisElement.class, Object.class, "elementReachableNotifications"); + private static final AtomicReferenceFieldUpdater reachableNotificationsUpdater = AtomicReferenceFieldUpdater.newUpdater(AnalysisElement.class, Object.class, + "elementReachableNotifications"); @SuppressWarnings("unused") private volatile Object elementReachableNotifications; @@ -101,23 +110,6 @@ protected void notifyReachabilityCallbacks(AnalysisUniverse universe, List task) { */ universe.getBigbang().postTask((d) -> task.ensureDone()); } + + /** + * Used to validate the reason why an analysis element is registered as reachable. + */ + boolean isValidReason(Object reason) { + if (reason == null) { + return false; + } + if (reason instanceof String s) { + return !s.isEmpty(); + } + /* + * ModifiersProvider is a common interface of ResolvedJavaField, ResolvedJavaMethod and + * ResolvedJavaType. + */ + return reason instanceof AnalysisElement || reason instanceof ModifiersProvider || reason instanceof ObjectScanner.ScanReason || + reason instanceof BytecodePosition; + } + + public static class ReachabilityReason { + + } + + public static class ReachabilityTraceBuilder { + private final String header; + private final Object rootReason; + private final BigBang bb; + private final StringBuilder reasonTrace; + private final ArrayDeque reasonStack; + private final HashSet seen; + + ReachabilityTraceBuilder(String traceHeader, Object reason, BigBang bigBang) { + header = traceHeader; + rootReason = reason; + bb = bigBang; + reasonTrace = new StringBuilder(); + reasonStack = new ArrayDeque<>(); + seen = new HashSet<>(); + } + + public static String buildReachabilityTrace(BigBang bb, Object reason, String header) { + ReachabilityTraceBuilder builder = new ReachabilityTraceBuilder(header, reason, bb); + builder.build(); + return builder.reasonTrace.toString(); + } + + public static String buildReachabilityTrace(BigBang bb, Object reason) { + ReachabilityTraceBuilder builder = new ReachabilityTraceBuilder("Reached by", reason, bb); + builder.build(); + return builder.reasonTrace.toString(); + } + + static boolean indentAllLines = true; + + private void build() { + reasonTrace.append(header); + maybeExpandReasonStack(rootReason); + String indent = ReportUtils.EMPTY_INDENT; + String prevIndent = indent; + while (!reasonStack.isEmpty()) { + boolean expanded; + Object top = reasonStack.peekLast(); + if (top instanceof CompoundReason) { + CompoundReason compoundReason = (CompoundReason) top; + if (compoundReason.isFirst()) { + compoundReason.storeCurrentIndent(indent); + } + Object next = compoundReason.next(); + if (next != null) { + indent = compoundReason.getIndent() + (compoundReason.hasNext() ? ReportUtils.CONNECTING_INDENT : ReportUtils.EMPTY_INDENT); + String infix = compoundReason.hasNext() ? ReportUtils.CHILD : ReportUtils.LAST_CHILD; + expanded = processReason(next, compoundReason.getIndent() + infix); + } else { + reasonStack.removeLast(); + expanded = false; + if (indentAllLines) { + prevIndent = compoundReason.getIndent(); + } else { + indent = compoundReason.getIndent(); + } + } + } else { + expanded = processReason(reasonStack.pollLast(), indent); + } + if (indentAllLines) { + if (expanded) { + prevIndent = indent; + indent = indent + ReportUtils.EMPTY_INDENT; + } else { + indent = prevIndent; + } + } + } + } + + static class CompoundReason { + final Object[] reasons; + int index = 0; + String indent; + + CompoundReason(Object... reasons) { + this.reasons = reasons; + } + + boolean isFirst() { + return index == 0; + } + + Object next() { + return index < reasons.length ? reasons[index++] : null; + } + + boolean hasNext() { + return index < reasons.length; + } + + public void storeCurrentIndent(String indentStr) { + this.indent = indentStr; + } + + public String getIndent() { + return indent; + } + } + + private boolean processReason(Object current, String prefix) { + String reasonStr; + boolean expanded = false; + if (current instanceof String) { + reasonStr = "str: " + current; + + } else if (current instanceof AnalysisMethod) { + AnalysisMethod method = (AnalysisMethod) current; + reasonStr = "at " + method.format("%f method %H.%n(%p)") + ", " + methodReasonStr(method); + expanded = methodReason((AnalysisMethod) current); + + } else if (current instanceof AnalysisField) { + AnalysisField field = (AnalysisField) current; + reasonStr = "field " + field.format("%H.%n") + " " + fieldReasonStr(field); + expanded = fieldReason(field); + + } else if (current instanceof AnalysisType) { + AnalysisType type = (AnalysisType) current; + reasonStr = "type " + (type).toJavaName() + " " + typeReasonStr(type); + expanded = typeReason(type); + + } else if (current instanceof ResolvedJavaMethod) { + reasonStr = ((ResolvedJavaMethod) current).format("%f method %H.%n"); + + } else if (current instanceof ResolvedJavaField field) { + /** + * In {@link AnalysisUniverse#lookupAllowUnresolved(JavaField}} we may register a + * ResolvedJavaField as reason. + * + * We convert it to AnalysisField to print more information about why the field is + * reachable. + */ + AnalysisField analysisField = bb.getUniverse().lookup(field); + if (analysisField != null) { + return processReason(analysisField, prefix); + } else { + reasonStr = "field " + ((ResolvedJavaField) current).format("%H.%n"); + } + + } else if (current instanceof ResolvedJavaType) { + reasonStr = "type " + ((ResolvedJavaType) current).getName(); + + } else if (current instanceof BytecodePosition) { + BytecodePosition position = (BytecodePosition) current; + ResolvedJavaMethod method = position.getMethod(); + reasonStr = "at " + method.format("%f") + " method " + method.asStackTraceElement(position.getBCI()) + ", " + methodReasonStr(method); + expanded = methodReason(position.getMethod()); + + } else if (current instanceof MethodParsing) { + MethodParsing methodParsing = (MethodParsing) current; + AnalysisMethod method = methodParsing.getMethod(); + reasonStr = "at " + method.format("%f method %H.%n(%p)") + ", " + methodReasonStr(method); + expanded = methodReason(methodParsing.getMethod()); + + } else if (current instanceof ObjectScanner.ScanReason) { + ObjectScanner.ScanReason scanReason = (ObjectScanner.ScanReason) current; + reasonStr = scanReason.toString(bb); + expanded = maybeExpandReasonStack(scanReason.getPrevious()); + + } else { + throw AnalysisError.shouldNotReachHere("Unknown reachability reason."); + + } + print(prefix, reasonStr); + return expanded; + } + + private void print(String prefix, String reasonStr) { + String reasonStr2 = String.join(System.lineSeparator() + prefix, reasonStr.lines().toArray(String[]::new)); + reasonTrace.append(System.lineSeparator()).append(prefix).append(reasonStr2); + } + + private boolean typeReason(AnalysisType type) { + if (type.isInHeap()) { + return maybeExpandReasonStack(type.getInHeapReason()); + } else if (type.isAllocated()) { + return maybeExpandReasonStack(type.getAllocatedReason()); + } else { + return maybeExpandReasonStack(type.getReachableReason()); + } + } + + private static String typeReasonStr(AnalysisType type) { + if (type.isInHeap()) { + return "is marked as in-heap"; + } + if (type.isAllocated()) { + return "is marked as allocated"; + } + return "is reachable"; + } + + private boolean fieldReason(AnalysisField field) { + if (field.getWrittenReason() != null) { + return maybeExpandReasonStack(field.getWrittenReason()); + } else if (field.getReadReason() != null) { + return maybeExpandReasonStack(field.getReadReason()); + } else if (field.getAccessedReason() != null) { + return maybeExpandReasonStack(field.getAccessedReason()); + } else if (field.getFoldedReason() != null) { + return maybeExpandReasonStack(field.getFoldedReason()); + } + return false; + } + + private static String fieldReasonStr(AnalysisField field) { + if (field.getWrittenReason() != null) { + return "is written"; + } + if (field.getReadReason() != null) { + return "is read"; + } + if (field.getAccessedReason() != null) { + return "is accessed"; + } + if (field.getFoldedReason() != null) { + return "is folded"; + } + return ""; + } + + private boolean methodReason(ResolvedJavaMethod method) { + if (method instanceof AnalysisMethod) { + AnalysisMethod aMethod = (AnalysisMethod) method; + if (aMethod.isSimplyImplementationInvoked()) { + if (aMethod.isStatic()) { + return maybeExpandReasonStack(aMethod.getImplementationInvokedReason()); + } else { + /* For virtual methods we follow back type and caller reachability. */ + return maybeExpandReasonStack(new CompoundReason(aMethod.getImplementationInvokedReason(), aMethod.getDeclaringClass())); + } + } else if (aMethod.isInlined()) { + if (aMethod.isStatic()) { + return maybeExpandReasonStack(aMethod.getInlinedReason()); + } else { + /* For virtual methods we follow back type and caller reachability. */ + return maybeExpandReasonStack(new CompoundReason(aMethod.getInlinedReason(), aMethod.getDeclaringClass())); + } + } else if (aMethod.isIntrinsicMethod()) { + return maybeExpandReasonStack(aMethod.getIntrinsicMethodReason()); + } else { + return maybeExpandReasonStack(aMethod.getInvokedReason()); + } + } + return false; + } + + private boolean maybeExpandReasonStack(Object reason) { + if (reason != null && seen.add(reason)) { + return reasonStack.add(reason); + } + return false; + } + + private static String methodReasonStr(ResolvedJavaMethod method) { + if (method instanceof AnalysisMethod) { + AnalysisMethod aMethod = (AnalysisMethod) method; + if (aMethod.isSimplyImplementationInvoked()) { + if (aMethod.isStatic()) { + return "implementation invoked"; + } else { + /* For virtual methods we follow back type reachability. */ + AnalysisType declaringClass = aMethod.getDeclaringClass(); + assert declaringClass.isInstantiated() || declaringClass.isInHeap() || declaringClass.isAbstract() || + (declaringClass.isInterface() && aMethod.isDefault()) || declaringClass.isReachable() : declaringClass + " is not reachable"; + return "implementation invoked"; + } + } else if (aMethod.isInlined()) { + if (aMethod.isStatic()) { + return "inlined"; + } else { + AnalysisType declaringClass = aMethod.getDeclaringClass(); + assert declaringClass.isInstantiated() || declaringClass.isInHeap() || declaringClass.isAbstract() || + (declaringClass.isInterface() && aMethod.isDefault()) || declaringClass.isReachable() : declaringClass + " is not reachable"; + return "inlined"; + } + } else if (aMethod.isIntrinsicMethod()) { + return "intrinsified"; + } + } + return ""; + } + } } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java index d0d554c91983..6d0e783cfea8 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java @@ -383,6 +383,10 @@ public Object getReadBy() { return isReadUpdater.get(this); } + public Object getAccessedReason() { + return isAccessed; + } + /** * Returns true if the field is reachable. Fields that are read or manually registered as * reachable are always reachable. For fields that are write-only, more cases need to be @@ -407,14 +411,26 @@ public boolean isRead() { return AtomicUtils.isSet(this, isAccessedUpdater) || AtomicUtils.isSet(this, isReadUpdater); } + public Object getReadReason() { + return isRead; + } + public boolean isWritten() { return AtomicUtils.isSet(this, isAccessedUpdater) || AtomicUtils.isSet(this, isWrittenUpdater); } + public Object getWrittenReason() { + return isWritten; + } + public boolean isFolded() { return AtomicUtils.isSet(this, isFoldedUpdater); } + public Object getFoldedReason() { + return isFolded; + } + @Override public boolean isReachable() { return AtomicUtils.isSet(this, isAccessedUpdater) || AtomicUtils.isSet(this, isReadUpdater) || @@ -511,7 +527,7 @@ public boolean isStatic() { @Override public String toString() { return "AnalysisField<" + format("%h.%n") + " -> " + wrapped.toString() + ", accessed: " + (isAccessed != null) + - ", read: " + (isRead != null) + ", written: " + (isWritten != null) + ", folded: " + (isFolded != null) + ">"; + ", read: " + (isRead != null) + ", written: " + (isWritten != null) + ", folded: " + isFolded() + ">"; } @Override diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java index 2883a9041c45..9ae7d5af77ef 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisMethod.java @@ -46,6 +46,7 @@ import java.util.stream.Collectors; import org.graalvm.compiler.debug.DebugContext; +import org.graalvm.compiler.graph.NodeSourcePosition; import org.graalvm.compiler.java.BytecodeParser.BytecodeParserError; import org.graalvm.compiler.java.StableMethodNameFormatter; import org.graalvm.compiler.nodes.EncodedGraph; @@ -387,7 +388,7 @@ public boolean registerAsImplementationInvoked(Object reason) { } public void registerAsInlined(Object reason) { - assert isValidReason(reason) : "Registering a method as inlined needs to provide a valid reason, found: " + reason; + assert reason instanceof NodeSourcePosition || reason instanceof ResolvedJavaMethod : "Registering a method as inlined needs to provide the inline location as reason, found: " + reason; AtomicUtils.atomicSetAndRun(this, reason, isInlinedUpdater, this::onReachable); } @@ -435,6 +436,10 @@ public boolean isIntrinsicMethod() { return AtomicUtils.isSet(this, isIntrinsicMethodUpdater); } + public Object getIntrinsicMethodReason() { + return isIntrinsicMethod; + } + /** * Registers this method as a virtual root for the analysis. * @@ -487,6 +492,10 @@ public boolean isInvoked() { return isIntrinsicMethod() || isVirtualRootMethod() || isDirectRootMethod() || AtomicUtils.isSet(this, isInvokedUpdater); } + protected Object getInvokedReason() { + return isInvoked; + } + /** * Returns true if the method body can ever be executed. Methods registered as root are also * registered as implementation invoked when they are linked. @@ -495,9 +504,21 @@ public boolean isImplementationInvoked() { return !Modifier.isAbstract(getModifiers()) && (isIntrinsicMethod() || AtomicUtils.isSet(this, isImplementationInvokedUpdater)); } + protected Object getImplementationInvokedReason() { + return isImplementationInvoked; + } + + public boolean isInlined() { + return AtomicUtils.isSet(this, isInlinedUpdater); + } + + protected Object getInlinedReason() { + return isInlined; + } + @Override public boolean isReachable() { - return isImplementationInvoked() || AtomicUtils.isSet(this, isInlinedUpdater); + return isImplementationInvoked() || isInlined(); } @Override diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java index 4f9f84d57f78..ee7d6eddebdb 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java @@ -830,6 +830,10 @@ public boolean isReachable() { return AtomicUtils.isSet(this, isReachableUpdater); } + public Object getReachableReason() { + return isReachable; + } + /** * The kind of the field in memory (in contrast to {@link #getJavaKind()}, which is the kind of * the field on the Java type system level). For example {@link WordBase word types} have a @@ -1271,10 +1275,18 @@ public boolean isInHeap() { return AtomicUtils.isSet(this, isInHeapUpdater); } + public Object getInHeapReason() { + return isInHeap; + } + public boolean isAllocated() { return AtomicUtils.isSet(this, isAllocatedUpdater); } + public Object getAllocatedReason() { + return isAllocated; + } + @Override public void link() { wrapped.link(); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java index 678a153ac79e..469e66802221 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java @@ -31,6 +31,7 @@ import org.graalvm.compiler.bytecode.BytecodeProvider; import org.graalvm.compiler.debug.GraalError; import org.graalvm.compiler.graph.Node; +import org.graalvm.compiler.graph.NodeSourcePosition; import org.graalvm.compiler.nodes.AbstractEndNode; import org.graalvm.compiler.nodes.AbstractMergeNode; import org.graalvm.compiler.nodes.ControlSinkNode; @@ -233,8 +234,10 @@ protected void finishInlining(MethodScope is) { if (callerScope.policyScope != null) { callerScope.policyScope.commitCalleeScope(inlineScope.policyScope); } - Object reason = graph.currentNodeSourcePosition() != null ? graph.currentNodeSourcePosition() : graph.method(); + NodeSourcePosition callerBytecodePosition = callerScope.getCallerNodeSourcePosition(); + Object reason = callerBytecodePosition != null ? callerBytecodePosition : callerScope.method; + reason = reason == null ? graph.method() : reason; ((AnalysisMethod) invokeData.callTarget.targetMethod()).registerAsInlined(reason); super.finishInlining(inlineScope); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReporter.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReporter.java index f94aa6a7439d..7584386d4144 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReporter.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReporter.java @@ -40,21 +40,23 @@ public class AnalysisReporter { public static void printAnalysisReports(String imageName, OptionValues options, String reportsPath, BigBang bb) { if (bb != null) { + String baseImageName = ReportUtils.extractImageName(imageName); + if (AnalysisReportsOptions.PrintAnalysisStatistics.getValue(options)) { - StatisticsPrinter.print(bb, reportsPath, ReportUtils.extractImageName(imageName)); + StatisticsPrinter.print(bb, reportsPath, baseImageName); } if (AnalysisReportsOptions.PrintAnalysisCallTree.getValue(options)) { - CallTreePrinter.print(bb, reportsPath, ReportUtils.extractImageName(imageName)); + CallTreePrinter.print(bb, reportsPath, baseImageName); } if (AnalysisReportsOptions.PrintImageObjectTree.getValue(options)) { - ObjectTreePrinter.print(bb, reportsPath, ReportUtils.extractImageName(imageName)); - AnalysisHeapHistogramPrinter.print(bb, reportsPath, ReportUtils.extractImageName(imageName)); + ObjectTreePrinter.print(bb, reportsPath, baseImageName); + AnalysisHeapHistogramPrinter.print(bb, reportsPath, baseImageName); } if (PointstoOptions.PrintPointsToStatistics.getValue(options)) { - PointsToStats.report(bb, ReportUtils.extractImageName(imageName)); + PointsToStats.report(bb, baseImageName); } if (PointstoOptions.PrintSynchronizedAnalysis.getValue(options)) { diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReportsOptions.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReportsOptions.java index c4a3a0e28224..7572f8f06b61 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReportsOptions.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/AnalysisReportsOptions.java @@ -62,20 +62,20 @@ protected void onValueUpdate(EconomicMap, Object> values, CallTreeT @Option(help = "Print image object hierarchy.")// public static final OptionKey PrintImageObjectTree = new OptionKey<>(false); - @Option(help = "Override the default suppression of specified roots. See: Reports.md.")// + @Option(help = "Override the default suppression of specified roots. See: StaticAnalysisReports.md.")// public static final OptionKey ImageObjectTreeExpandRoots = new OptionKey<>(""); - @Option(help = "Suppress the expansion of specified roots. See: Reports.md.")// + @Option(help = "Suppress the expansion of specified roots. See: StaticAnalysisReports.md.")// public static final OptionKey ImageObjectTreeSuppressRoots = new OptionKey<>(""); - @Option(help = "Override the default suppression of specified types. See: Reports.md.")// + @Option(help = "Override the default suppression of specified types. See: StaticAnalysisReports.md.")// public static final OptionKey ImageObjectTreeExpandTypes = new OptionKey<>(""); - @Option(help = "Suppress the expansion of specified types. See: Reports.md.")// + @Option(help = "Suppress the expansion of specified types. See: StaticAnalysisReports.md.")// public static final OptionKey ImageObjectTreeSuppressTypes = new OptionKey<>(""); enum CallTreeType { TXT, - CSV; + CSV } } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ObjectTreePrinter.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ObjectTreePrinter.java index d1874546aa2f..6d6cc1f324ee 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ObjectTreePrinter.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ObjectTreePrinter.java @@ -246,10 +246,10 @@ public boolean equals(Object obj) { } } - static class SimpleMatcher { + public static final class SimpleMatcher { private final String[] patterns; - SimpleMatcher(String[] patterns) { + public SimpleMatcher(String[] patterns) { this.patterns = patterns; } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ReportUtils.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ReportUtils.java index d7478cae61de..fff11832cf30 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ReportUtils.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/reports/ReportUtils.java @@ -53,10 +53,10 @@ public class ReportUtils { - static final String CONNECTING_INDENT = "\u2502 "; // "| " - static final String EMPTY_INDENT = " "; - static final String CHILD = "\u251c\u2500\u2500 "; // "|-- " - static final String LAST_CHILD = "\u2514\u2500\u2500 "; // "`-- " + public static final String CONNECTING_INDENT = "\u2502 "; // "| " + public static final String EMPTY_INDENT = " "; + public static final String CHILD = "\u251c\u2500\u2500 "; // "|-- " + public static final String LAST_CHILD = "\u2514\u2500\u2500 "; // "`-- " public static final Comparator methodComparator = Comparator.comparing(m -> m.format("%H.%n(%P):%R")); static final Comparator fieldComparator = Comparator.comparing(f -> f.format("%H.%n")); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java index 1e677b6276b6..ab8fc267715c 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java @@ -56,6 +56,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; +import com.oracle.svm.hosted.analysis.ReachabilityTracePrinter; import org.graalvm.collections.EconomicMap; import org.graalvm.collections.EconomicSet; import org.graalvm.collections.MapCursor; @@ -818,11 +819,13 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De OnAnalysisExitAccess onExitConfig = new OnAnalysisExitAccessImpl(featureHandler, loader, bb, debug); featureHandler.forEachFeature(feature -> feature.onAnalysisExit(onExitConfig)); + String reportsPath = SubstrateOptions.reportsPath(); /* * Execute analysis reporting here. This code is executed even if unsupported features * are reported or the analysis fails due to any other reasons. */ - AnalysisReporter.printAnalysisReports(imageName, options, SubstrateOptions.reportsPath(), bb); + AnalysisReporter.printAnalysisReports(imageName, options, reportsPath, bb); + ReachabilityTracePrinter.report(imageName, options, reportsPath, bb); } if (NativeImageOptions.ReturnAfterAnalysis.getValue()) { return true; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ReachabilityTracePrinter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ReachabilityTracePrinter.java new file mode 100644 index 000000000000..983fc3381447 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ReachabilityTracePrinter.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.analysis; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; + +import org.graalvm.compiler.debug.MethodFilter; +import org.graalvm.compiler.options.Option; +import org.graalvm.compiler.options.OptionValues; + +import com.oracle.graal.pointsto.BigBang; +import com.oracle.graal.pointsto.meta.AnalysisElement; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.reports.ObjectTreePrinter; +import com.oracle.graal.pointsto.reports.ReportUtils; +import com.oracle.graal.pointsto.util.AnalysisError; +import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.option.LocatableMultiOptionValue; +import com.oracle.svm.core.option.SubstrateOptionsParser; + +public final class ReachabilityTracePrinter { + public static class Options { + @Option(help = "Print a trace and abort the build process if any type matching the specified pattern becomes reachable.")// + public static final HostedOptionKey AbortOnTypeReachable = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build()); + + @Option(help = "Print a trace and abort the build process if any method matching the specified pattern becomes reachable.")// + public static final HostedOptionKey AbortOnMethodReachable = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build()); + + @Option(help = "Print a trace and abort the build process if any field matching the specified pattern becomes reachable.")// + public static final HostedOptionKey AbortOnFieldReachable = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build()); + } + + public static void report(String imageName, OptionValues options, String reportsPath, BigBang bb) { + String baseImageName = ReportUtils.extractImageName(imageName); + + StringBuilder consoleMessageBuilder = new StringBuilder(); + + List typePatterns = ReachabilityTracePrinter.Options.AbortOnTypeReachable.getValue(options).values(); + if (!typePatterns.isEmpty()) { + StringWriter stringWriter = new StringWriter(); + int count = ReachabilityTracePrinter.printTraceForTypesImpl(typePatterns, bb, new PrintWriter(stringWriter)); + if (count > 0) { + String trace = stringWriter.toString(); + ReportUtils.report("trace for types", reportsPath, "trace_types_" + baseImageName, "txt", + writer -> writer.print(trace)); + String abortOnTypeReachableOption = SubstrateOptionsParser.commandArgument(Options.AbortOnTypeReachable, String.join(",", typePatterns)); + String message = "Image building is interrupted as the types specified via " + abortOnTypeReachableOption + + " are reachable. See the generated report for a complete reachability trace."; + consoleMessageBuilder.append(message); + } + } + + List methodPatterns = ReachabilityTracePrinter.Options.AbortOnMethodReachable.getValue(options).values(); + if (!methodPatterns.isEmpty()) { + StringWriter stringWriter = new StringWriter(); + int count = ReachabilityTracePrinter.printTraceForMethodsImpl(methodPatterns, bb, new PrintWriter(stringWriter)); + if (count > 0) { + String trace = stringWriter.toString(); + ReportUtils.report("trace for methods", reportsPath, "trace_methods_" + baseImageName, "txt", + writer -> writer.print(trace)); + String abortOnMethodReachableOption = SubstrateOptionsParser.commandArgument(Options.AbortOnMethodReachable, String.join(",", methodPatterns)); + String message = "Image building is interrupted as the methods specified via " + abortOnMethodReachableOption + + " are reachable. See the generated report for a complete reachability trace."; + consoleMessageBuilder.append(message); + } + } + + List fieldPatterns = ReachabilityTracePrinter.Options.AbortOnFieldReachable.getValue(options).values(); + if (!fieldPatterns.isEmpty()) { + StringWriter stringWriter = new StringWriter(); + int count = ReachabilityTracePrinter.printTraceForFieldsImpl(fieldPatterns, bb, new PrintWriter(stringWriter)); + if (count > 0) { + String trace = stringWriter.toString(); + ReportUtils.report("trace for fields", reportsPath, "trace_fields_" + baseImageName, "txt", + writer -> writer.print(trace)); + String abortOnFieldReachableOption = SubstrateOptionsParser.commandArgument(Options.AbortOnFieldReachable, String.join(",", fieldPatterns)); + String message = "Image building is interrupted as the fields specified via " + abortOnFieldReachableOption + + " are reachable. See the generated report for a complete reachability trace."; + consoleMessageBuilder.append(message); + } + } + + if (!consoleMessageBuilder.isEmpty()) { + throw AnalysisError.interruptAnalysis(consoleMessageBuilder.toString()); + } + } + + private static int printTraceForTypesImpl(List typePatterns, BigBang bb, PrintWriter writer) { + ObjectTreePrinter.SimpleMatcher matcher = new ObjectTreePrinter.SimpleMatcher(typePatterns.toArray(new String[0])); + int count = 0; + for (AnalysisType type : bb.getUniverse().getTypes()) { + if (!type.isReachable() || !matcher.matches(type.toJavaName(true))) { + continue; + } + + if (type.isAllocated()) { + String header = "Type " + type.toJavaName() + " is marked as allocated"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, type.getAllocatedReason(), header); + writer.println(trace); + } else if (type.isInHeap()) { + String header = "Type " + type.toJavaName() + " is marked as in-heap"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, type.getInHeapReason(), header); + writer.println(trace); + } else { + String header = "Type " + type.toJavaName() + " is marked as reachable"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, type.getReachableReason(), header); + writer.println(trace); + } + + count++; + } + + return count; + } + + private static int printTraceForMethodsImpl(List methodPatterns, BigBang bb, PrintWriter writer) { + MethodFilter matcher = MethodFilter.parse(String.join(",", methodPatterns)); + int count = 0; + for (AnalysisMethod method : bb.getUniverse().getMethods()) { + if (!method.isReachable() || !matcher.matches(method)) { + continue; + } + + if (method.isIntrinsicMethod()) { + String header = "Method " + method.format("%H.%n(%p)") + " is intrinsic"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, method.getIntrinsicMethodReason(), header); + writer.println(trace); + } else { + String header = "Method " + method.format("%H.%n(%p)") + " is called"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, method.getParsingReason(), header); + writer.println(trace); + } + + count++; + } + + return count; + } + + private static int printTraceForFieldsImpl(List fieldPatterns, BigBang bb, PrintWriter writer) { + ObjectTreePrinter.SimpleMatcher matcher = new ObjectTreePrinter.SimpleMatcher(fieldPatterns.toArray(new String[0])); + int count = 0; + + for (AnalysisField field : bb.getUniverse().getFields()) { + if (!field.isReachable() || !matcher.matches(field.getWrapped().format("%H.%n"))) { + continue; + } + + if (field.isWritten()) { + String header = "Field " + field.getName() + " is written"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, field.getWrittenReason(), header); + writer.println(trace); + } else if (field.isRead()) { + String header = "Field " + field.getName() + " is read"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, field.getReadReason(), header); + writer.println(trace); + } else if (field.isAccessed()) { + String header = "Field " + field.getName() + " is accessed unsafely"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, field.getAccessedReason(), header); + writer.println(trace); + } else { + assert field.isFolded(); + String header = "Field " + field.getName() + " is folded"; + String trace = AnalysisElement.ReachabilityTraceBuilder.buildReachabilityTrace(bb, field.getFoldedReason(), header); + writer.println(trace); + } + + count++; + } + + return count; + } +}