diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgent.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgent.java index e51247884648..9a6e0d1170d9 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgent.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgent.java @@ -74,7 +74,6 @@ import com.oracle.svm.configure.config.ConfigurationSet; import com.oracle.svm.configure.config.conditional.ConditionalConfigurationPredicate; import com.oracle.svm.configure.filters.ComplexFilter; -import com.oracle.svm.configure.filters.ConfigurationFilter; import com.oracle.svm.configure.filters.FilterConfigurationParser; import com.oracle.svm.configure.filters.HierarchyFilterNode; import com.oracle.svm.configure.trace.AccessAdvisor; @@ -137,6 +136,7 @@ protected JNIHandleSet constructJavaHandles(JNIEnvironment env) { protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks callbacks, String options) { String traceOutputFile = null; String configOutputDir = null; + String ignoredEntriesFile = null; ConfigurationFileCollection mergeConfigs = new ConfigurationFileCollection(); ConfigurationFileCollection omittedConfigs = new ConfigurationFileCollection(); boolean builtinCallerFilter = true; @@ -170,6 +170,13 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c if (token.startsWith("config-merge-dir=")) { mergeConfigs.addDirectory(Paths.get(configOutputDir)); } + } else if (token.startsWith("experimental-ignored-entries-output=")) { + if (ignoredEntriesFile != null) { + return usage("cannot specify ignored-entries-output= more than once."); + } + warn("Ignored entries logging (enabled by the \"experimental-ignored-entries-output\" argument) is " + + "experimental and will be removed in the future. Do not rely on this feature."); + ignoredEntriesFile = getTokenValue(token); } else if (token.startsWith("config-to-omit=")) { String omittedConfigDir = getTokenValue(token); omittedConfigDir = transformPath(omittedConfigDir); @@ -308,7 +315,7 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c if (experimentalOmitClasspathConfig) { ignoreConfigFromClasspath(jvmti, omittedConfigs); } - AccessAdvisor advisor = createAccessAdvisor(builtinHeuristicFilter, callerFilter, accessFilter); + AccessAdvisor advisor = new AccessAdvisor(builtinHeuristicFilter, callerFilter, accessFilter, ignoredEntriesFile); TraceProcessor processor = new TraceProcessor(advisor); ConfigurationSet omittedConfiguration = new ConfigurationSet(); Predicate shouldExcludeClassesWithHash = null; @@ -452,18 +459,6 @@ private static boolean checkJVMVersion(JvmtiEnv jvmti) { return true; } - private static AccessAdvisor createAccessAdvisor(boolean builtinHeuristicFilter, ConfigurationFilter callerFilter, ConfigurationFilter accessFilter) { - AccessAdvisor advisor = new AccessAdvisor(); - advisor.setHeuristicsEnabled(builtinHeuristicFilter); - if (callerFilter != null) { - advisor.setCallerFilterTree(callerFilter); - } - if (accessFilter != null) { - advisor.setAccessFilterTree(accessFilter); - } - return advisor; - } - private static int parseIntegerOrNegative(String number) { try { return Integer.parseInt(number); diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/TraceFileWriter.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/TraceFileWriter.java index f6e80a1a145f..e706a5e1e07f 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/TraceFileWriter.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/TraceFileWriter.java @@ -24,101 +24,27 @@ */ package com.oracle.svm.agent.tracing; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.StringWriter; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.Base64; import org.graalvm.collections.EconomicMap; -import org.graalvm.collections.MapCursor; import com.oracle.svm.agent.tracing.core.Tracer; import com.oracle.svm.agent.tracing.core.TracingResultWriter; -import com.oracle.svm.core.util.VMError; - -import jdk.graal.compiler.util.json.JsonWriter; +import com.oracle.svm.configure.trace.JsonFileWriter; public class TraceFileWriter extends Tracer implements TracingResultWriter { - private final Object lock = new Object(); - private final BufferedWriter writer; - private boolean open = true; - private int written = 0; + private final JsonFileWriter jsonFileWriter; @SuppressWarnings("this-escape") public TraceFileWriter(Path path) throws IOException { - writer = Files.newBufferedWriter(path); - JsonWriter json = new JsonWriter(writer); - json.append('[').newline(); + jsonFileWriter = new JsonFileWriter(path); traceInitialization(); - json.flush(); // avoid close() on underlying stream } @Override protected void traceEntry(EconomicMap entry) { - try { - StringWriter str = new StringWriter(); - try (JsonWriter json = new JsonWriter(str)) { - json.append('{'); - boolean first = true; - MapCursor cursor = entry.getEntries(); - while (cursor.advance()) { - if (!first) { - json.append(", "); - } - json.quote(cursor.getKey()).append(':'); - if (cursor.getValue() instanceof Object[]) { - printArray(json, (Object[]) cursor.getValue()); - } else { - printValue(json, cursor.getValue()); - } - first = false; - } - json.append('}'); - } - traceEntry(str.toString()); - } catch (IOException e) { - throw VMError.shouldNotReachHere(e); - } - } - - private static void printArray(JsonWriter json, Object[] array) throws IOException { - json.append('['); - for (int i = 0; i < array.length; i++) { - if (i > 0) { - json.append(','); - } - Object obj = array[i]; - if (obj instanceof Object[]) { - printArray(json, (Object[]) obj); - } else { - printValue(json, array[i]); - } - } - json.append(']'); - } - - private static void printValue(JsonWriter json, Object value) throws IOException { - Object s = null; - if (value instanceof byte[]) { - s = Base64.getEncoder().encodeToString((byte[]) value); - } else if (value != null) { - s = value; - } - json.printValue(s); - } - - private void traceEntry(String s) throws IOException { - synchronized (lock) { - if (open) { // late events on exit - if (written > 0) { - writer.write(",\n"); - } - writer.write(s); - written++; - } - } + jsonFileWriter.printObject(entry); } @Override @@ -133,13 +59,6 @@ public boolean supportsPeriodicTraceWriting() { @Override public void close() { - synchronized (lock) { - try { - writer.write("\n]\n"); - writer.close(); - } catch (IOException ignored) { - } - open = false; - } + jsonFileWriter.close(); } } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/command/ConfigurationGenerateCommand.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/command/ConfigurationGenerateCommand.java index b19a215fe8c6..ef5c4770a54f 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/command/ConfigurationGenerateCommand.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/command/ConfigurationGenerateCommand.java @@ -282,15 +282,7 @@ protected static void generate(Iterator argumentsIterator, boolean accep } if (!traceInputs.isEmpty()) { - AccessAdvisor advisor = new AccessAdvisor(); - advisor.setHeuristicsEnabled(builtinHeuristicFilter); - if (callersFilter != null) { - advisor.setCallerFilterTree(callersFilter); - } - if (accessFilter != null) { - advisor.setAccessFilterTree(accessFilter); - } - + AccessAdvisor advisor = new AccessAdvisor(builtinHeuristicFilter, callersFilter, accessFilter, null); TraceProcessor processor = new TraceProcessor(advisor); for (URI uri : traceInputs) { try (Reader reader = Files.newBufferedReader(Paths.get(uri))) { diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AbstractProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AbstractProcessor.java index 1249c284af13..1aa10532c4b6 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AbstractProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AbstractProcessor.java @@ -36,7 +36,7 @@ public abstract class AbstractProcessor { AbstractProcessor() { } - abstract void processEntry(EconomicMap entry, ConfigurationSet configurationSet); + abstract void processEntry(EconomicMap entry, ConfigurationSet configurationSet); void setInLivePhase(@SuppressWarnings("unused") boolean live) { } @@ -59,4 +59,17 @@ static byte[] asBinary(Object obj) { } return Base64.getDecoder().decode((String) obj); } + + /** + * Returns a fresh copy of the trace entry with an additional key-value pair identifying it. + * Useful when logging entries to the access advisor. + */ + protected EconomicMap copyWithUniqueEntry(EconomicMap entry, String key, Object value) { + if (entry.containsKey(key)) { + throw new AssertionError(String.format("Tried to set unique identifier %s but field already exists: %s%n", key, entry)); + } + EconomicMap result = EconomicMap.create(entry); + result.put(key, value); + return result; + } } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java index 178af402e548..b37b3a3a6934 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java @@ -24,9 +24,13 @@ */ package com.oracle.svm.configure.trace; +import java.io.IOException; +import java.nio.file.Path; import java.util.Set; import java.util.regex.Pattern; +import org.graalvm.collections.EconomicMap; + import com.oracle.svm.configure.filters.ConfigurationFilter; import com.oracle.svm.configure.filters.HierarchyFilterNode; @@ -34,8 +38,7 @@ import jdk.graal.compiler.phases.common.LazyValue; /** - * Decides if a recorded access should be included in a configuration. Also advises the agent's - * {@code AccessVerifier} classes which accesses to ignore when the agent is in restriction mode. + * Decides if a recorded access should be included in a configuration. */ public final class AccessAdvisor { /** @@ -160,118 +163,152 @@ public static HierarchyFilterNode copyBuiltinAccessFilterTree() { return internalAccessFilter.copy(); } - private ConfigurationFilter callerFilter = internalCallerFilter; - private ConfigurationFilter accessFilter = internalAccessFilter; - private boolean heuristicsEnabled = true; + private final boolean heuristicsEnabled; + private final ConfigurationFilter callerFilter; + private final ConfigurationFilter accessFilter; + + private final JsonFileWriter ignoredEntriesTracer; + private boolean isInLivePhase = false; private int launchPhase = 0; - public void setHeuristicsEnabled(boolean enable) { - heuristicsEnabled = enable; - } - - public void setCallerFilterTree(ConfigurationFilter rootNode) { - callerFilter = rootNode; - } + public AccessAdvisor(boolean heuristicsEnabled, ConfigurationFilter callerFilter, ConfigurationFilter accessFilter, String ignoredEntriesFile) { + this.heuristicsEnabled = heuristicsEnabled; + this.callerFilter = callerFilter == null ? internalCallerFilter : callerFilter; + this.accessFilter = accessFilter == null ? internalAccessFilter : accessFilter; - public void setAccessFilterTree(ConfigurationFilter rootNode) { - accessFilter = rootNode; + JsonFileWriter ignoredEntriesTracerLocal = null; + if (ignoredEntriesFile != null) { + try { + ignoredEntriesTracerLocal = new JsonFileWriter(Path.of(ignoredEntriesFile)); + } catch (IOException ex) { + System.err.printf("%s: could not open file \"%s\" to log ignored entries. No logging will be performed.%n", AccessAdvisor.class.getName(), ignoredEntriesFile); + } + } + this.ignoredEntriesTracer = ignoredEntriesTracerLocal; } public void setInLivePhase(boolean live) { + if (isInLivePhase && !live && ignoredEntriesTracer != null) { + ignoredEntriesTracer.close(); + } isInLivePhase = live; } - public boolean shouldIgnore(LazyValue queriedClass, LazyValue callerClass, boolean useLambdaHeuristics) { + public boolean shouldIgnore(LazyValue queriedClass, LazyValue callerClass, boolean useLambdaHeuristics, EconomicMap entry) { if (heuristicsEnabled && !isInLivePhase) { + logIgnoredEntry("not in live phase", entry); return true; } String qualifiedCaller = callerClass.get(); assert qualifiedCaller == null || qualifiedCaller.indexOf('/') == -1 : "expecting Java-format qualifiers, not internal format"; if (qualifiedCaller != null && !callerFilter.includes(qualifiedCaller)) { + logIgnoredEntry("excluded by caller filter", entry); return true; } // NOTE: queriedClass can be null for non-class accesses like resources if (callerClass.get() == null && queriedClass.get() != null && !accessWithoutCallerFilter.includes(queriedClass.get())) { + logIgnoredEntry("excluded by caller-less access filter", entry); return true; } if (accessFilter != null && queriedClass.get() != null && !accessFilter.includes(queriedClass.get())) { + logIgnoredEntry("excluded by access filter", entry); return true; } if (heuristicsEnabled && queriedClass.get() != null) { - return (useLambdaHeuristics && queriedClass.get().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING)) || - PROXY_CLASS_NAME_PATTERN.matcher(queriedClass.get()).matches(); + if (useLambdaHeuristics && queriedClass.get().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING)) { + logIgnoredEntry("lambda class", entry); + return true; + } else if (PROXY_CLASS_NAME_PATTERN.matcher(queriedClass.get()).matches()) { + logIgnoredEntry("proxy class", entry); + return true; + } } return false; } - public boolean shouldIgnore(LazyValue queriedClass, LazyValue callerClass) { - return shouldIgnore(queriedClass, callerClass, true); + public boolean shouldIgnore(LazyValue queriedClass, LazyValue callerClass, EconomicMap entry) { + return shouldIgnore(queriedClass, callerClass, true, entry); } - record JNICallDescriptor(String declaringClass, String name, String signature, boolean required) { - public boolean matches(String otherDeclaringClass, String otherName, String otherSignature) { - return (declaringClass == null || declaringClass.equals(otherDeclaringClass)) && - (otherName == null || name.equals(otherName)) && - (otherSignature == null || signature.equals(otherSignature)); + record JNICallDescriptor(String jniFunction, String declaringClass, String name, String signature, boolean required) { + public boolean matches(String otherJniFunction, String otherDeclaringClass, String otherName, String otherSignature) { + return jniFunction.equals(otherJniFunction) && + (declaringClass == null || declaringClass.equals(otherDeclaringClass)) && + name.equals(otherName) && signature.equals(otherSignature); } } private static final JNICallDescriptor[] JNI_STARTUP_SEQUENCE = new JNICallDescriptor[]{ - new JNICallDescriptor("sun.launcher.LauncherHelper", "getApplicationClass", "()Ljava/lang/Class;", true), - new JNICallDescriptor("java.lang.Class", "getCanonicalName", "()Ljava/lang/String;", false), - new JNICallDescriptor("java.lang.String", "lastIndexOf", "(I)I", false), - new JNICallDescriptor("java.lang.String", "substring", "(I)Ljava/lang/String;", false), - new JNICallDescriptor("java.lang.System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", false), - new JNICallDescriptor("java.lang.System", "setProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", false), - new JNICallDescriptor(null, "main", "([Ljava/lang/String;)V", true), + new JNICallDescriptor("GetStaticMethodID", "sun.launcher.LauncherHelper", "getApplicationClass", "()Ljava/lang/Class;", true), + new JNICallDescriptor("GetMethodID", "java.lang.Class", "getCanonicalName", "()Ljava/lang/String;", false), + new JNICallDescriptor("GetMethodID", "java.lang.String", "lastIndexOf", "(I)I", false), + new JNICallDescriptor("GetMethodID", "java.lang.String", "substring", "(I)Ljava/lang/String;", false), + new JNICallDescriptor("GetStaticMethodID", "java.lang.System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", false), + new JNICallDescriptor("GetStaticMethodID", "java.lang.System", "setProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", false), + new JNICallDescriptor("GetStaticMethodID", null, "main", "([Ljava/lang/String;)V", true), }; + private static final int JNI_STARTUP_COMPLETE = JNI_STARTUP_SEQUENCE.length; + private static final int JNI_STARTUP_MISMATCH_COMPLETE = JNI_STARTUP_COMPLETE + 1; - public boolean shouldIgnoreJniLookup(LazyValue queriedClass, LazyValue name, LazyValue signature, LazyValue callerClass) { - assert !shouldIgnore(queriedClass, callerClass) : "must have been checked before"; - if (!heuristicsEnabled) { + /** + * The JVM uses JNI calls internally when starting up. To avoid emitting configuration for these + * calls, we ignore calls until an expected sequence of calls ({@link #JNI_STARTUP_SEQUENCE}) is + * observed. + */ + public boolean shouldIgnoreJniLookup(String jniFunction, LazyValue queriedClass, LazyValue name, LazyValue signature, LazyValue callerClass, + EconomicMap entry) { + if (shouldIgnore(queriedClass, callerClass, entry)) { + throw new AssertionError("Must check shouldIgnore before shouldIgnoreJniLookup"); + } + if (!heuristicsEnabled || launchPhase >= JNI_STARTUP_COMPLETE) { + // Startup sequence completed (or we're not using the startup heuristics). return false; } - if (launchPhase >= 0) { - JNICallDescriptor expectedCall = JNI_STARTUP_SEQUENCE[launchPhase]; - while (!expectedCall.matches(queriedClass.get(), name.get(), signature.get())) { - if ("sun.launcher.LauncherHelper".equals(queriedClass.get())) { - return true; - } - if (!expectedCall.required) { - launchPhase++; - expectedCall = JNI_STARTUP_SEQUENCE[launchPhase]; - } else { - launchPhase = -1; - return false; - } - } - if (name.get() != null && signature.get() != null) { - /* - * We ignore class lookups before field/method lookups but don't skip to the next - * query - */ - launchPhase++; + if (!"GetStaticMethodID".equals(jniFunction) && !"GetMethodID".equals(jniFunction)) { + // Ignore function calls for functions not tracked by the startup sequence. + logIgnoredEntry("JNI startup sequence not completed", entry); + return true; + } + + JNICallDescriptor expectedCall = JNI_STARTUP_SEQUENCE[launchPhase]; + while (!expectedCall.matches(jniFunction, queriedClass.get(), name.get(), signature.get())) { + if ("sun.launcher.LauncherHelper".equals(queriedClass.get())) { + // Ignore mismatched calls from sun.launcher.LauncherHelper. + logIgnoredEntry("calls from sun.launcher.LauncherHelper are ignored", entry); + return true; } - if (launchPhase == JNI_STARTUP_SEQUENCE.length) { - launchPhase = -1; + + if (expectedCall.required) { + // Mismatch on a required call. Mark startup as complete and start tracing JNI + // calls. (We prefer to emit extraneous configuration than to lose configuration). + System.err.printf("%s: Warning: Observed unexpected JNI call to %s (%s). Tracing all subsequent JNI accesses.%n", AccessAdvisor.class.getName(), jniFunction, entry); + launchPhase = JNI_STARTUP_MISMATCH_COMPLETE; + return false; } - return true; + + // The call is optional (e.g., it only happens on some platforms). Skip it. + launchPhase++; + expectedCall = JNI_STARTUP_SEQUENCE[launchPhase]; } - /* - * NOTE: JVM invocations cannot be reliably filtered with callerClass == null because these - * could also be calls in a manually launched thread which is attached to JNI, but is not - * executing Java code (yet). - */ - return false; + + launchPhase++; + logIgnoredEntry(String.format("method %d in JNI startup sequence", launchPhase), entry); + return true; } - public static boolean shouldIgnoreResourceLookup(LazyValue resource) { - return Set.of("META-INF/services/jdk.vm.ci.services.JVMCIServiceLocator", "META-INF/services/java.lang.System$LoggerFinder").contains(resource.get()); + public boolean shouldIgnoreResourceLookup(LazyValue resource, EconomicMap entry) { + boolean result = Set.of("META-INF/services/jdk.vm.ci.services.JVMCIServiceLocator", "META-INF/services/java.lang.System$LoggerFinder").contains(resource.get()); + if (result) { + logIgnoredEntry("blocklisted resource", entry); + } + return result; } - public boolean shouldIgnoreLoadClass(LazyValue queriedClass, LazyValue callerClass) { - assert !shouldIgnore(queriedClass, callerClass) : "must have been checked before"; + public boolean shouldIgnoreLoadClass(LazyValue queriedClass, LazyValue callerClass, EconomicMap entry) { + if (shouldIgnore(queriedClass, callerClass, entry)) { + throw new AssertionError("Must check shouldIgnore before shouldIgnoreLoadClass"); + } if (!heuristicsEnabled) { return false; } @@ -281,6 +318,20 @@ public boolean shouldIgnoreLoadClass(LazyValue queriedClass, LazyValue entry) { + if (ignoredEntriesTracer != null) { + if (entry.containsKey("reason")) { + throw new AssertionError("Entry has unexpected \"reason\" key: " + entry); + } + entry.put("reason", reason); + ignoredEntriesTracer.printObject(entry); + } } } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ClassLoadingProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ClassLoadingProcessor.java index 3e9b0bbd3c90..73b52ff16292 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ClassLoadingProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ClassLoadingProcessor.java @@ -34,7 +34,7 @@ public class ClassLoadingProcessor extends AbstractProcessor { @Override - void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { + void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { boolean invalidResult = Boolean.FALSE.equals(entry.get("result")); if (invalidResult) { return; diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/JniProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/JniProcessor.java index 8c973e41fc6b..7423c7896970 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/JniProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/JniProcessor.java @@ -50,7 +50,7 @@ class JniProcessor extends AbstractProcessor { @Override @SuppressWarnings("fallthrough") - void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { + void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { UnresolvedConfigurationCondition condition = UnresolvedConfigurationCondition.alwaysTrue(); boolean invalidResult = Boolean.FALSE.equals(entry.get("result")); if (invalidResult) { @@ -66,9 +66,9 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe String internalName = (lookupName.charAt(0) != '[') ? ('L' + lookupName + ';') : lookupName; String forNameString = MetaUtil.internalNameToJava(internalName, true, true); LazyValue forNameStringLazyValue = lazyValue(forNameString); - if (!advisor.shouldIgnore(forNameStringLazyValue, callerClassLazyValue)) { + if (!advisor.shouldIgnore(forNameStringLazyValue, callerClassLazyValue, entry)) { if (function.equals("FindClass")) { - if (!advisor.shouldIgnoreJniLookup(forNameStringLazyValue, lazyNull(), lazyNull(), callerClassLazyValue)) { + if (!advisor.shouldIgnoreJniLookup(function, forNameStringLazyValue, lazyNull(), lazyNull(), callerClassLazyValue, entry)) { configurationSet.getJniConfiguration().getOrCreateType(condition, forNameString); } } else if (!AccessAdvisor.PROXY_CLASS_NAME_PATTERN.matcher(lookupName).matches()) { // DefineClass @@ -78,7 +78,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe return; } String clazz = (String) entry.get("class"); - if (advisor.shouldIgnore(lazyValue(clazz), callerClassLazyValue)) { + if (advisor.shouldIgnore(lazyValue(clazz), callerClassLazyValue, entry)) { return; } String declaringClass = (String) entry.get("declaring_class"); @@ -99,7 +99,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe expectSize(args, 2); String name = (String) args.get(0); String signature = (String) args.get(1); - if (!advisor.shouldIgnoreJniLookup(lazyValue(clazz), lazyValue(name), lazyValue(signature), callerClassLazyValue)) { + if (!advisor.shouldIgnoreJniLookup(function, lazyValue(clazz), lazyValue(name), lazyValue(signature), callerClassLazyValue, entry)) { config.getOrCreateType(condition, declaringClassOrClazz).addMethod(name, signature, declaration); if (!declaringClassOrClazz.equals(clazz)) { config.getOrCreateType(condition, clazz); @@ -112,7 +112,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe expectSize(args, 2); String name = (String) args.get(0); String signature = (String) args.get(1); - if (!advisor.shouldIgnoreJniLookup(lazyValue(clazz), lazyValue(name), lazyValue(signature), callerClassLazyValue)) { + if (!advisor.shouldIgnoreJniLookup(function, lazyValue(clazz), lazyValue(name), lazyValue(signature), callerClassLazyValue, entry)) { config.getOrCreateType(condition, declaringClassOrClazz).addField(name, declaration, false); if (!declaringClassOrClazz.equals(clazz)) { config.getOrCreateType(condition, clazz); @@ -124,7 +124,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe expectSize(args, 1); // exception message, ignore String name = ConfigurationMethod.CONSTRUCTOR_NAME; String signature = "(Ljava/lang/String;)V"; - if (!advisor.shouldIgnoreJniLookup(lazyValue(clazz), lazyValue(name), lazyValue(signature), callerClassLazyValue)) { + if (!advisor.shouldIgnoreJniLookup(function, lazyValue(clazz), lazyValue(name), lazyValue(signature), callerClassLazyValue, entry)) { config.getOrCreateType(condition, declaringClassOrClazz).addMethod(name, signature, declaration); assert declaringClassOrClazz.equals(clazz) : "Constructor can only be accessed via declaring class"; } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/JsonFileWriter.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/JsonFileWriter.java new file mode 100644 index 000000000000..72e238ca9b5a --- /dev/null +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/JsonFileWriter.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025, 2025, 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.configure.trace; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; + +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.MapCursor; + +import com.oracle.svm.core.util.VMError; + +import jdk.graal.compiler.util.json.JsonWriter; + +public class JsonFileWriter implements Closeable { + private final Object lock = new Object(); + private final BufferedWriter writer; + private boolean open = true; + private int written = 0; + + public JsonFileWriter(Path path) throws IOException { + writer = Files.newBufferedWriter(path); + JsonWriter json = new JsonWriter(writer); + json.append('[').newline(); + json.flush(); // avoid close() on underlying stream + } + + public void printObject(EconomicMap entry) { + try { + StringWriter str = new StringWriter(); + try (JsonWriter json = new JsonWriter(str)) { + json.append('{'); + boolean first = true; + MapCursor cursor = entry.getEntries(); + while (cursor.advance()) { + if (!first) { + json.append(", "); + } + json.quote(cursor.getKey()).append(':'); + if (cursor.getValue() instanceof Object[]) { + printArray(json, (Object[]) cursor.getValue()); + } else { + printValue(json, cursor.getValue()); + } + first = false; + } + json.append('}'); + } + writeEntry(str.toString()); + } catch (IOException e) { + throw VMError.shouldNotReachHere(e); + } + } + + private static void printArray(JsonWriter json, Object[] array) throws IOException { + json.append('['); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + json.append(','); + } + Object obj = array[i]; + if (obj instanceof Object[]) { + printArray(json, (Object[]) obj); + } else { + printValue(json, array[i]); + } + } + json.append(']'); + } + + private static void printValue(JsonWriter json, Object value) throws IOException { + Object s = null; + if (value instanceof byte[]) { + s = Base64.getEncoder().encodeToString((byte[]) value); + } else if (value != null) { + s = value; + } + json.printValue(s); + } + + private void writeEntry(String s) throws IOException { + synchronized (lock) { + if (open) { // late events on exit + if (written > 0) { + writer.write(",\n"); + } + writer.write(s); + written++; + } + } + } + + @Override + public void close() { + synchronized (lock) { + try { + writer.write("\n]\n"); + writer.close(); + } catch (IOException ignored) { + } + open = false; + } + } +} diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java index 9788d607c0bc..8baaa5f33556 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java @@ -62,7 +62,7 @@ public void setTrackReflectionMetadata(boolean trackReflectionMetadata) { @Override @SuppressWarnings("fallthrough") - public void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { + public void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { boolean invalidResult = Boolean.FALSE.equals(entry.get("result")); UnresolvedConfigurationCondition condition = UnresolvedConfigurationCondition.alwaysTrue(); if (invalidResult) { @@ -79,14 +79,14 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur expectSize(args, 2); String module = (String) args.get(0); String resource = (String) args.get(1); - if (!AccessAdvisor.shouldIgnoreResourceLookup(lazyValue(resource))) { + if (!advisor.shouldIgnoreResourceLookup(lazyValue(resource), entry)) { resourceConfiguration.addGlobPattern(condition, resource, module); } return; } case "getResource", "getSystemResource", "getSystemResourceAsStream", "getResources", "getSystemResources" -> { String literal = singleElement(args); - if (!AccessAdvisor.shouldIgnoreResourceLookup(lazyValue(literal))) { + if (!advisor.shouldIgnoreResourceLookup(lazyValue(literal), entry)) { resourceConfiguration.addGlobPattern(condition, literal, null); } return; @@ -100,15 +100,15 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur if (isLoadClass) { // different array syntax name = MetaUtil.internalNameToJava(MetaUtil.toInternalName(name), true, true); } - if (!advisor.shouldIgnore(lazyValue(name), lazyValue(callerClass)) && - !(isLoadClass && advisor.shouldIgnoreLoadClass(lazyValue(name), lazyValue(callerClass)))) { + if (!advisor.shouldIgnore(lazyValue(name), lazyValue(callerClass), entry) && + !(isLoadClass && advisor.shouldIgnoreLoadClass(lazyValue(name), lazyValue(callerClass), entry))) { configuration.getOrCreateType(condition, name); } return; } else if (function.equals("methodTypeDescriptor")) { List typeNames = singleElement(args); for (String type : typeNames) { - if (!advisor.shouldIgnore(lazyValue(type), lazyValue(callerClass))) { + if (!advisor.shouldIgnore(lazyValue(type), lazyValue(callerClass), copyWithUniqueEntry(entry, "ignoredDescriptorType", type))) { configuration.getOrCreateType(condition, type); } } @@ -116,7 +116,7 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur } ConfigurationTypeDescriptor clazz = descriptorForClass(entry.get("class")); for (String className : clazz.getAllQualifiedJavaNames()) { - if (advisor.shouldIgnore(lazyValue(className), lazyValue(callerClass))) { + if (advisor.shouldIgnore(lazyValue(className), lazyValue(callerClass), copyWithUniqueEntry(entry, "ignoredClassName", className))) { return; } } @@ -241,17 +241,17 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur case "getProxyClass": { expectSize(args, 2); - addDynamicProxy((List) args.get(1), lazyValue(callerClass), configuration); + addDynamicProxy((List) args.get(1), lazyValue(callerClass), configuration, entry); break; } case "newProxyInstance": { expectSize(args, 3); - addDynamicProxy((List) args.get(1), lazyValue(callerClass), configuration); + addDynamicProxy((List) args.get(1), lazyValue(callerClass), configuration, entry); break; } case "newMethodHandleProxyInstance": { expectSize(args, 1); - addDynamicProxyUnchecked((List) args.get(0), Collections.singletonList("sun.invoke.WrapperInstance"), lazyValue(callerClass), configuration); + addDynamicProxyUnchecked((List) args.get(0), Collections.singletonList("sun.invoke.WrapperInstance"), lazyValue(callerClass), configuration, entry); break; } @@ -309,21 +309,22 @@ private static void addFullyQualifiedDeclaredMethod(String descriptor, TypeConfi configuration.getOrCreateType(UnresolvedConfigurationCondition.alwaysTrue(), qualifiedClass).addMethod(methodName, signature, ConfigurationMemberDeclaration.DECLARED); } - private void addDynamicProxy(List interfaceList, LazyValue callerClass, TypeConfiguration configuration) { + private void addDynamicProxy(List interfaceList, LazyValue callerClass, TypeConfiguration configuration, EconomicMap entry) { ConfigurationTypeDescriptor typeDescriptor = descriptorForClass(interfaceList); for (String iface : typeDescriptor.getAllQualifiedJavaNames()) { - if (advisor.shouldIgnore(lazyValue(iface), callerClass)) { + if (advisor.shouldIgnore(lazyValue(iface), callerClass, copyWithUniqueEntry(entry, "ignoredInterface", iface))) { return; } } configuration.getOrCreateType(UnresolvedConfigurationCondition.alwaysTrue(), typeDescriptor); } - private void addDynamicProxyUnchecked(List checkedInterfaceList, List uncheckedInterfaceList, LazyValue callerClass, TypeConfiguration configuration) { + private void addDynamicProxyUnchecked(List checkedInterfaceList, List uncheckedInterfaceList, LazyValue callerClass, TypeConfiguration configuration, + EconomicMap entry) { @SuppressWarnings("unchecked") List checkedInterfaces = (List) checkedInterfaceList; for (String iface : checkedInterfaces) { - if (advisor.shouldIgnore(lazyValue(iface), callerClass)) { + if (advisor.shouldIgnore(lazyValue(iface), callerClass, copyWithUniqueEntry(entry, "ignoredInterface", iface))) { return; } } @@ -335,4 +336,5 @@ private void addDynamicProxyUnchecked(List checkedInterfaceList, List unch interfaces.addAll(uncheckedInterfaces); configuration.getOrCreateType(UnresolvedConfigurationCondition.alwaysTrue(), descriptorForClass(interfaces)); } + } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java index 406a38671f48..d2d88cd72522 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java @@ -48,7 +48,7 @@ public SerializationProcessor(AccessAdvisor advisor) { @Override @SuppressWarnings("unchecked") - void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { + void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { boolean invalidResult = Boolean.FALSE.equals(entry.get("result")); if (invalidResult) { return; @@ -62,7 +62,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe if ("ObjectStreamClass.".equals(function) || "ObjectInputStream.readClassDescriptor".equals(function)) { expectSize(args, 1); - if (advisor.shouldIgnore(LazyValueUtils.lazyValue((String) args.get(0)), LazyValueUtils.lazyValue(null), false)) { + if (advisor.shouldIgnore(LazyValueUtils.lazyValue((String) args.get(0)), LazyValueUtils.lazyValue(null), false, entry)) { return; } @@ -76,7 +76,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe } else if ("SerializedLambda.readResolve".equals(function)) { expectSize(args, 1); - if (advisor.shouldIgnore(LazyValueUtils.lazyValue((String) args.get(0)), LazyValueUtils.lazyValue(null))) { + if (advisor.shouldIgnore(LazyValueUtils.lazyValue((String) args.get(0)), LazyValueUtils.lazyValue(null), entry)) { return; } @@ -87,7 +87,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe List interfaces = (List) args.get(0); for (String iface : interfaces) { - if (advisor.shouldIgnore(lazyValue(iface), LazyValueUtils.lazyValue(null))) { + if (advisor.shouldIgnore(lazyValue(iface), LazyValueUtils.lazyValue(null), copyWithUniqueEntry(entry, "ignoredInterface", iface))) { return; } } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/TraceProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/TraceProcessor.java index 25a2cb3e423c..34a88777291a 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/TraceProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/TraceProcessor.java @@ -56,18 +56,18 @@ public TraceProcessor(AccessAdvisor accessAdvisor) { public void process(Reader reader, ConfigurationSet configurationSet) throws IOException { setInLivePhase(false); JsonParser parser = new JsonParser(reader); - List> trace = (List>) parser.parse(); + List> trace = (List>) parser.parse(); processTrace(trace, configurationSet); } - private void processTrace(List> trace, ConfigurationSet configurationSet) { - for (EconomicMap entry : trace) { + private void processTrace(List> trace, ConfigurationSet configurationSet) { + for (EconomicMap entry : trace) { processEntry(entry, configurationSet); } } @Override - public void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { + public void processEntry(EconomicMap entry, ConfigurationSet configurationSet) { try { String tracer = (String) entry.get("tracer"); switch (tracer) {