diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java index 6425a7cd6c39..eb87c2c559f1 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java @@ -39,6 +39,7 @@ import org.graalvm.nativeimage.impl.ConfigurationCondition; import com.oracle.svm.configure.ClassNameSupport; +import com.oracle.svm.configure.config.ConfigurationType; import com.oracle.svm.core.configure.ConditionalRuntimeValue; import com.oracle.svm.core.configure.RuntimeConditionSet; import com.oracle.svm.core.feature.AutomaticallyRegisteredImageSingleton; @@ -47,6 +48,7 @@ import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.util.ImageHeapMap; @@ -329,17 +331,22 @@ private static Class forName(String className, ClassLoader classLoader, boole } private Object forName0(String className, ClassLoader classLoader) { - var conditional = knownClasses.get(className); - Object result = conditional == null ? null : conditional.getValue(); if (className.endsWith("[]")) { /* Querying array classes with their "TypeName[]" name always throws */ - result = NEGATIVE_QUERY; + return new ClassNotFoundException(className); } + var conditional = knownClasses.get(className); + Object result = conditional == null ? null : conditional.getValue(); if (result == null) { result = PredefinedClassesSupport.getLoadedForNameOrNull(className, classLoader); } if (result == null && !ClassNameSupport.isValidReflectionName(className)) { - result = NEGATIVE_QUERY; + /* Invalid class names always throw, no need for reflection data */ + return new ClassNotFoundException(className); + } + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + // NB: the early returns above ensure we do not trace calls with bad type args. + MetadataTracer.singleton().traceReflectionType(className); } return result == NEGATIVE_QUERY ? new ClassNotFoundException(className) : result; } @@ -433,7 +440,16 @@ public static boolean canUnsafeInstantiateAsInstance(DynamicHub hub) { break; } } - return conditionSet != null && conditionSet.satisfied(); + if (conditionSet != null) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + ConfigurationType type = MetadataTracer.singleton().traceReflectionType(clazz.getName()); + if (type != null) { + type.setUnsafeAllocated(); + } + } + return conditionSet.satisfied(); + } + return false; } @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index f0061b6c3e67..d8bad63d0146 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.hub; +import static com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberAccessibility; +import static com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberDeclaration; import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; import static com.oracle.svm.core.annotate.TargetElement.CONSTRUCTOR_NAME; @@ -84,19 +86,21 @@ import java.util.function.BiFunction; import java.util.function.IntFunction; -import com.oracle.svm.core.TrackDynamicAccessEnabled; import org.graalvm.nativeimage.AnnotationAccess; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import org.graalvm.word.WordBase; +import com.oracle.svm.configure.config.ConfigurationType; +import com.oracle.svm.configure.config.SignatureUtil; import com.oracle.svm.core.BuildPhaseProvider.AfterHostedUniverse; import com.oracle.svm.core.BuildPhaseProvider.CompileQueueFinished; import com.oracle.svm.core.NeverInline; import com.oracle.svm.core.NeverInlineTrivial; import com.oracle.svm.core.RuntimeAssertionsSupport; import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.TrackDynamicAccessEnabled; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.Delete; @@ -121,6 +125,7 @@ import com.oracle.svm.core.jdk.ProtectionDomainSupport; import com.oracle.svm.core.jdk.Resources; import com.oracle.svm.core.meta.SharedType; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.reflect.RuntimeMetadataDecoder; import com.oracle.svm.core.reflect.RuntimeMetadataDecoder.ConstructorDescriptor; @@ -704,6 +709,9 @@ private ReflectionMetadata reflectionMetadata() { } private void checkClassFlag(int mask, String methodName) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + traceClassFlagQuery(mask); + } if (throwMissingRegistrationErrors() && !(isClassFlagSet(mask) && getConditions().satisfied())) { MissingReflectionRegistrationUtils.forBulkQuery(DynamicHub.toClass(this), methodName); } @@ -726,6 +734,30 @@ private static boolean isClassFlagSet(int mask, ReflectionMetadata reflectionMet return reflectionMetadata != null && (reflectionMetadata.classFlags & mask) != 0; } + private void traceClassFlagQuery(int mask) { + ConfigurationType type = MetadataTracer.singleton().traceReflectionType(getName()); + if (type == null) { + return; + } + // TODO (GR-64765): We over-approximate member accessibility here because we don't trace + // accesses. Once we trace accesses, it will suffice to register the class for reflection. + switch (mask) { + case ALL_FIELDS_FLAG -> type.setAllPublicFields(ConfigurationMemberAccessibility.ACCESSED); + case ALL_DECLARED_FIELDS_FLAG -> type.setAllDeclaredFields(ConfigurationMemberAccessibility.ACCESSED); + case ALL_METHODS_FLAG -> type.setAllPublicMethods(ConfigurationMemberAccessibility.ACCESSED); + case ALL_DECLARED_METHODS_FLAG -> type.setAllDeclaredMethods(ConfigurationMemberAccessibility.ACCESSED); + case ALL_CONSTRUCTORS_FLAG -> type.setAllPublicConstructors(ConfigurationMemberAccessibility.ACCESSED); + case ALL_DECLARED_CONSTRUCTORS_FLAG -> type.setAllDeclaredConstructors(ConfigurationMemberAccessibility.ACCESSED); + case ALL_CLASSES_FLAG -> type.setAllPublicClasses(); + case ALL_DECLARED_CLASSES_FLAG -> type.setAllDeclaredClasses(); + case ALL_RECORD_COMPONENTS_FLAG -> type.setAllRecordComponents(); + case ALL_PERMITTED_SUBCLASSES_FLAG -> type.setAllPermittedSubclasses(); + case ALL_NEST_MEMBERS_FLAG -> type.setAllNestMembers(); + case ALL_SIGNERS_FLAG -> type.setAllSigners(); + default -> throw VMError.shouldNotReachHere("unknown class flag " + mask); + } + } + /** Executed at runtime. */ private static Object initEnumConstantsAtRuntime(Method values) { try { @@ -1282,6 +1314,11 @@ public Field getField(String fieldName) throws NoSuchFieldException, SecurityExc private void checkField(String fieldName, Field field, boolean publicOnly) throws NoSuchFieldException { boolean throwMissingErrors = throwMissingRegistrationErrors(); Class clazz = DynamicHub.toClass(this); + + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + traceFieldLookup(fieldName, field, publicOnly); + } + if (field == null) { if (throwMissingErrors && !allElementsRegistered(publicOnly, ALL_DECLARED_FIELDS_FLAG, ALL_FIELDS_FLAG)) { MissingReflectionRegistrationUtils.forField(clazz, fieldName); @@ -1305,6 +1342,25 @@ private void checkField(String fieldName, Field field, boolean publicOnly) throw } } + private void traceFieldLookup(String fieldName, Field field, boolean publicOnly) { + ConfigurationMemberDeclaration declaration = publicOnly ? ConfigurationMemberDeclaration.PRESENT : ConfigurationMemberDeclaration.DECLARED; + if (field != null) { + // register declaring type and field + ConfigurationType declaringType = MetadataTracer.singleton().traceReflectionType(field.getDeclaringClass().getName()); + if (declaringType != null) { + declaringType.addField(fieldName, declaration, false); + } + // register receiver type + MetadataTracer.singleton().traceReflectionType(getName()); + } else { + // register receiver type and negative field query + ConfigurationType receiverType = MetadataTracer.singleton().traceReflectionType(getName()); + if (receiverType != null) { + receiverType.addField(fieldName, declaration, false); + } + } + } + @Substitute private Method getMethod(String methodName, Class... parameterTypes) throws NoSuchMethodException { Objects.requireNonNull(methodName); @@ -1340,6 +1396,11 @@ private void checkConstructor(Class[] parameterTypes, Constructor construc private boolean checkExecutableExists(String methodName, Class[] parameterTypes, Executable method, boolean publicOnly) { boolean throwMissingErrors = throwMissingRegistrationErrors(); Class clazz = DynamicHub.toClass(this); + + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + traceMethodLookup(methodName, parameterTypes, method, publicOnly); + } + if (method == null) { boolean isConstructor = methodName.equals(CONSTRUCTOR_NAME); int allDeclaredFlag = isConstructor ? ALL_DECLARED_CONSTRUCTORS_FLAG : ALL_DECLARED_METHODS_FLAG; @@ -1366,6 +1427,38 @@ private boolean checkExecutableExists(String methodName, Class[] parameterTyp } } + private void traceMethodLookup(String methodName, Class[] parameterTypes, Executable method, boolean publicOnly) { + ConfigurationMemberDeclaration declaration = publicOnly ? ConfigurationMemberDeclaration.PRESENT : ConfigurationMemberDeclaration.DECLARED; + if (method != null) { + // register declaring type and method + ConfigurationType declaringType = MetadataTracer.singleton().traceReflectionType(method.getDeclaringClass().getName()); + if (declaringType != null) { + declaringType.addMethod(methodName, toInternalSignature(parameterTypes), declaration); + } + // register receiver type + MetadataTracer.singleton().traceReflectionType(getName()); + } else { + // register receiver type and negative method query + ConfigurationType receiverType = MetadataTracer.singleton().traceReflectionType(getName()); + if (receiverType != null) { + receiverType.addMethod(methodName, toInternalSignature(parameterTypes), declaration, ConfigurationMemberAccessibility.QUERIED); + } + } + } + + private static String toInternalSignature(Class[] classes) { + List names; + if (classes == null) { + names = List.of(); + } else { + names = new ArrayList<>(classes.length); + for (Class aClass : classes) { + names.add(aClass.getName()); + } + } + return SignatureUtil.toInternalSignature(names); + } + private boolean allElementsRegistered(boolean publicOnly, int allDeclaredElementsFlag, int allPublicElementsFlag) { return isClassFlagSet(allDeclaredElementsFlag) || (publicOnly && isClassFlagSet(allPublicElementsFlag)); } @@ -1882,6 +1975,8 @@ public DynamicHub arrayType() { } if (companion.arrayHub == null) { MissingReflectionRegistrationUtils.forClass(getTypeName() + "[]"); + } else if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceReflectionType(companion.arrayHub.getTypeName()); } return companion.arrayHub; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java index 71a52c7ccff0..f9956c1f5ce6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java @@ -35,8 +35,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; -import jdk.graal.compiler.java.LambdaUtils; - import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; @@ -45,8 +43,11 @@ import com.oracle.svm.core.annotate.TargetElement; import com.oracle.svm.core.fieldvaluetransformer.NewInstanceFieldValueTransformer; import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.serialize.MissingSerializationRegistrationUtils; +import jdk.graal.compiler.java.LambdaUtils; + @TargetClass(java.io.FileDescriptor.class) final class Target_java_io_FileDescriptor { @@ -68,8 +69,8 @@ static ObjectStreamClass lookup(Class cl, boolean all) { return null; } - if (Serializable.class.isAssignableFrom(cl)) { - if (!cl.isArray() && !DynamicHub.fromClass(cl).isRegisteredForSerialization()) { + if (Serializable.class.isAssignableFrom(cl) && !cl.isArray()) { + if (!DynamicHub.fromClass(cl).isRegisteredForSerialization()) { boolean isLambda = cl.getTypeName().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING); boolean isProxy = Proxy.isProxyClass(cl); if (isProxy || isLambda) { @@ -87,6 +88,9 @@ static ObjectStreamClass lookup(Class cl, boolean all) { MissingSerializationRegistrationUtils.missingSerializationRegistration(cl, "type " + cl.getTypeName()); } } + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceSerializationType(cl.getName()); + } } return Target_java_io_ObjectStreamClass_Caches.localDescs0.get(cl); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java index 7e5c2db054a0..652f1bcc738a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java @@ -37,6 +37,7 @@ import com.oracle.svm.core.config.ConfigurationValues; import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.hub.LayoutEncoding; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.snippets.KnownIntrinsics; @@ -386,6 +387,9 @@ private static void set(Object a, int index, Object value) { @Substitute private static Object newArray(Class componentType, int length) throws NegativeArraySizeException { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceReflectionType(componentType.arrayType().getName()); + } return KnownIntrinsics.unvalidatedNewArray(componentType, length); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index 3c1c5e164616..538889989954 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -71,6 +71,7 @@ import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.GlobUtils; @@ -385,6 +386,9 @@ public static ResourceStorageEntryBase getAtRuntime(Module module, String resour return null; } } + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceResource(resourceName, moduleName); + } if (!entry.getConditions().satisfied()) { return missingMetadata(resourceName, throwOnMissing); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java index 36d8bb4e6e21..470264065b6c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java @@ -57,6 +57,7 @@ import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.configure.RuntimeConditionSet; import com.oracle.svm.core.jdk.Resources; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.ReflectionUtil; @@ -293,6 +294,12 @@ public boolean isRegisteredBundleLookup(String baseName, Locale locale, Object c /* Those cases will throw a NullPointerException before any lookup */ return true; } - return registeredBundles.containsKey(baseName) && registeredBundles.get(baseName).satisfied(); + if (registeredBundles.containsKey(baseName)) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceResourceBundle(baseName); + } + return registeredBundles.get(baseName).satisfied(); + } + return false; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java index 2bb1fe5a58a7..3daeaeb64bab 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java @@ -98,10 +98,19 @@ public boolean isClassInitializer() { return WRAPPED_CSTRING_EQUIVALENCE.equals(name, INITIALIZER_NAME); } + /** + * Returns the method name as a String. Can be used if the descriptor is known to be a String + * (i.e., it does not come from a JNI call); otherwise, use {@link #getNameConvertToString()}. + */ public String getName() { return (String) name; } + /** + * Returns the method signature as a String. Can be used if the descriptor is known to be a + * String (i.e., it does not come from a JNI call); otherwise, use + * {@link #getSignatureConvertToString()}. + */ public String getSignature() { return (String) signature; } @@ -113,6 +122,13 @@ public String getNameConvertToString() { return name.toString(); } + /** + * Performs a potentially costly conversion to string, only for slow paths. + */ + public String getSignatureConvertToString() { + return signature.toString(); + } + public String getSignatureWithoutReturnType() { String signatureString = signature.toString(); int parametersEnd = signatureString.lastIndexOf(')'); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java index 2021f81dfbfa..574e2a7ed995 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java @@ -42,6 +42,8 @@ import org.graalvm.word.Pointer; import com.oracle.svm.configure.ClassNameSupport; +import com.oracle.svm.configure.config.ConfigurationMemberInfo; +import com.oracle.svm.configure.config.ConfigurationType; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.heap.Heap; @@ -53,6 +55,7 @@ import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.snippets.KnownIntrinsics; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.Utf8.WrappedAsciiCString; @@ -181,6 +184,9 @@ public static Class getClassObjectByName(CharSequence name) { JNIAccessibleClass clazz = dictionary.classesByName.get(name); if (clazz == null && !ClassNameSupport.isValidJNIName(name.toString())) { clazz = NEGATIVE_CLASS_LOOKUP; + } else if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + // trace if class exists (positive query) or name is valid (negative query) + MetadataTracer.singleton().traceJNIType(ClassNameSupport.jniNameToTypeName(name.toString())); } clazz = checkClass(clazz, name.toString()); if (clazz != null) { @@ -274,6 +280,12 @@ private static JNIAccessibleMethod getDeclaredMethod(Class classObject, JNIAc foundClass = true; JNIAccessibleMethod method = clazz.getMethod(descriptor); if (method != null) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + ConfigurationType clazzType = MetadataTracer.singleton().traceJNIType(classObject.getName()); + if (clazzType != null) { + clazzType.addMethod(descriptor.getNameConvertToString(), descriptor.getSignatureConvertToString(), ConfigurationMemberInfo.ConfigurationMemberDeclaration.DECLARED); + } + } return method; } } @@ -329,6 +341,12 @@ private static JNIAccessibleField getDeclaredField(Class classObject, CharSeq foundClass = true; JNIAccessibleField field = clazz.getField(name); if (field != null && (field.isStatic() == isStatic || field.isNegative())) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + ConfigurationType clazzType = MetadataTracer.singleton().traceJNIType(classObject.getName()); + if (clazzType != null) { + clazzType.addField(name.toString(), ConfigurationMemberInfo.ConfigurationMemberDeclaration.DECLARED, false); + } + } return field; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java index 60a138fe0966..ba06b66e3b61 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java @@ -28,12 +28,23 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; +import com.oracle.svm.configure.ConfigurationTypeDescriptor; +import com.oracle.svm.configure.NamedConfigurationTypeDescriptor; +import com.oracle.svm.configure.ProxyConfigurationTypeDescriptor; +import com.oracle.svm.configure.UnresolvedConfigurationCondition; import com.oracle.svm.configure.config.ConfigurationSet; +import com.oracle.svm.configure.config.ConfigurationType; +import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.RuntimeSupport; @@ -50,42 +61,125 @@ /** * Implements reachability metadata tracing during native image execution. Enabling * {@link Options#MetadataTracingSupport} at build time will generate code to trace all accesses of - * reachability metadata. When {@link Options#RecordMetadata} is specified at run time, the image - * will trace and emit metadata to the specified path. + * reachability metadata, and then the run-time option {@link Options#RecordMetadata} enables + * tracing. */ public final class MetadataTracer { public static class Options { - @Option(help = "Enables the run-time code to trace reachability metadata accesses in the produced native image by using -XX:RecordMetadata=.")// + @Option(help = "Generate an image that supports reachability metadata access tracing. " + + "When tracing is supported, use the -XX:RecordMetadata option to enable tracing at run time.")// public static final HostedOptionKey MetadataTracingSupport = new HostedOptionKey<>(false); - @Option(help = "The path of the directory to write traced metadata to. Metadata tracing is enabled only when this option is provided.")// - public static final RuntimeOptionKey RecordMetadata = new RuntimeOptionKey<>(""); + @Option(help = "file:doc-files/RecordMetadataHelp.txt")// + public static final RuntimeOptionKey RecordMetadata = new RuntimeOptionKey<>(null); } - private ConfigurationSet config; - - private Path recordMetadataPath; + private RecordOptions options; + private volatile ConfigurationSet config; @Fold public static MetadataTracer singleton() { return ImageSingletons.lookup(MetadataTracer.class); } - private static void initialize() { - assert Options.MetadataTracingSupport.getValue(); - MetadataTracer singleton = MetadataTracer.singleton(); - String recordMetadataValue = Options.RecordMetadata.getValue(); - if (recordMetadataValue.isEmpty()) { - throw new IllegalArgumentException("Empty path provided for " + Options.RecordMetadata.getName() + "."); + /** + * Returns whether tracing is enabled at run time (using {@code -XX:RecordMetadata}). + */ + public boolean enabled() { + VMError.guarantee(Options.MetadataTracingSupport.getValue()); + return options != null; + } + + /** + * Marks the given type as reachable from reflection. + * + * @return the corresponding {@link ConfigurationType} or {@code null} if tracing is not active + * (e.g., during shutdown). + */ + public ConfigurationType traceReflectionType(String className) { + return traceReflectionTypeImpl(new NamedConfigurationTypeDescriptor(className)); + } + + /** + * Marks the given proxy type as reachable from reflection. + */ + public void traceProxyType(List interfaceNames) { + traceReflectionTypeImpl(new ProxyConfigurationTypeDescriptor(interfaceNames)); + } + + private ConfigurationType traceReflectionTypeImpl(ConfigurationTypeDescriptor typeDescriptor) { + assert enabled(); + ConfigurationSet configurationSet = config; + if (configurationSet != null) { + return configurationSet.getReflectionConfiguration().getOrCreateType(UnresolvedConfigurationCondition.alwaysTrue(), typeDescriptor); } - Path recordMetadataPath = Paths.get(recordMetadataValue); + return null; + } + + /** + * Marks the given type as reachable from JNI. + * + * @return the corresponding {@link ConfigurationType} or {@code null} if tracing is not active + * (e.g., during shutdown). + */ + public ConfigurationType traceJNIType(String className) { + assert enabled(); + ConfigurationType result = traceReflectionType(className); + if (result != null) { + result.setJniAccessible(); + } + return result; + } + + /** + * Marks the given resource within the given (optional) module as reachable. + */ + public void traceResource(String resourceName, String moduleName) { + assert enabled(); + ConfigurationSet configurationSet = config; + if (configurationSet != null) { + configurationSet.getResourceConfiguration().addGlobPattern(UnresolvedConfigurationCondition.alwaysTrue(), resourceName, moduleName); + } + } + + /** + * Marks the given resource bundle within the given locale as reachable. + */ + public void traceResourceBundle(String baseName) { + assert enabled(); + ConfigurationSet configurationSet = config; + if (configurationSet != null) { + configurationSet.getResourceConfiguration().addBundle(UnresolvedConfigurationCondition.alwaysTrue(), baseName, List.of()); + } + } + + /** + * Marks the given type as serializable. + */ + public void traceSerializationType(String className) { + assert enabled(); + ConfigurationType result = traceReflectionType(className); + if (result != null) { + result.setSerializable(); + } + } + + private static void initialize(String recordMetadataValue) { + assert Options.MetadataTracingSupport.getValue(); + + RecordOptions parsedOptions = RecordOptions.parse(recordMetadataValue); try { - Files.createDirectories(recordMetadataPath); + Files.createDirectories(parsedOptions.path()); } catch (IOException ex) { - throw new IllegalArgumentException("Exception occurred creating the output directory for tracing (" + recordMetadataPath + ")", ex); + throw new IllegalArgumentException("Exception occurred creating the output directory for tracing (" + parsedOptions.path() + ")", ex); } - singleton.recordMetadataPath = recordMetadataPath; + if (parsedOptions.mode() != RecordMode.DEFAULT) { + throw new IllegalArgumentException("Mode " + parsedOptions.mode() + " is not yet supported."); + } + + MetadataTracer singleton = MetadataTracer.singleton(); + singleton.options = parsedOptions; singleton.config = new ConfigurationSet(); } @@ -93,12 +187,13 @@ private static void shutdown() { assert Options.MetadataTracingSupport.getValue(); MetadataTracer singleton = MetadataTracer.singleton(); ConfigurationSet config = singleton.config; + singleton.config = null; // clear config so that shutdown events are not traced. if (config != null) { try { - config.writeConfiguration(configFile -> singleton.recordMetadataPath.resolve(configFile.getFileName())); + config.writeConfiguration(configFile -> singleton.options.path().resolve(configFile.getFileName())); } catch (IOException ex) { Log log = Log.log(); - log.string("Failed to write out reachability metadata to directory ").string(singleton.recordMetadataPath.toString()); + log.string("Failed to write out reachability metadata to directory ").string(singleton.options.path().toString()); log.string(":").string(ex.getMessage()); log.newline(); } @@ -112,7 +207,7 @@ static RuntimeSupport.Hook initializeMetadataTracingHook() { } VMError.guarantee(Options.MetadataTracingSupport.getValue()); if (Options.RecordMetadata.hasBeenSet()) { - initialize(); + initialize(Options.RecordMetadata.getValue()); } }; } @@ -142,12 +237,86 @@ static RuntimeSupport.Hook checkImproperOptionUsageHook() { throw new IllegalArgumentException( "The option " + Options.RecordMetadata.getName() + " can only be used if metadata tracing is enabled at build time (using " + hostedOptionCommandArgument + ")."); - } }; } } +enum RecordMode { + DEFAULT, + CONDITIONAL +} + +record RecordOptions(Path path, RecordMode mode) { + + static RecordOptions parse(String recordMetadataValue) { + if (recordMetadataValue.isEmpty()) { + throw parseError("Option " + MetadataTracer.Options.RecordMetadata.getName() + " cannot be empty"); + } + + Map parsedArguments = new HashMap<>(); + Set allArguments = new LinkedHashSet<>(List.of("path", "mode")); + for (String argument : recordMetadataValue.split(",")) { + String[] parts = SubstrateUtil.split(argument, "=", 2); + if (parts.length != 2) { + throw badArgumentError(argument, "Argument should be a key-value pair separated by '='"); + } else if (!allArguments.contains(parts[0])) { + throw badArgumentError(argument, "Argument key should be one of " + allArguments); + } else if (parsedArguments.containsKey(parts[0])) { + throw badArgumentError(argument, "Argument '" + parts[0] + "' was already specified with value '" + parsedArguments.get(parts[0]) + "'"); + } else if (parts[1].isEmpty()) { + throw badArgumentError(argument, "Argument '" + parts[0] + "' cannot be empty"); + } + parsedArguments.put(parts[0], parts[1]); + } + + String path = requiredArgument(parsedArguments, "path"); + String mode = optionalArgument(parsedArguments, "mode", "default"); + return new RecordOptions(Paths.get(path), parseMode(mode)); + } + + private static IllegalArgumentException parseError(String message) { + return new IllegalArgumentException(message + ". Sample usage: -XX:" + MetadataTracer.Options.RecordMetadata.getName() + "=path=[,mode=]"); + } + + private static IllegalArgumentException badArgumentError(String option, String message) { + throw parseError("Bad argument provided for " + MetadataTracer.Options.RecordMetadata.getName() + ": '" + option + "'. " + message); + } + + private static String requiredArgument(Map arguments, String key) { + if (arguments.containsKey(key)) { + return arguments.get(key); + } + throw parseError(MetadataTracer.Options.RecordMetadata.getName() + " missing required argument '" + key + "'"); + } + + private static String optionalArgument(Map options, String key, String defaultValue) { + if (options.containsKey(key)) { + return options.get(key); + } + return defaultValue; + } + + private static RecordMode parseMode(String modeValue) { + try { + return RecordMode.valueOf(modeValue.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + StringBuilder message = new StringBuilder("Mode should be one of ["); + boolean first = true; + for (RecordMode mode : RecordMode.values()) { + if (first) { + first = false; + } else { + message.append(", "); + } + message.append(mode.toString().toLowerCase(Locale.ROOT)); + } + message.append("]"); + throw badArgumentError("mode=" + modeValue, message.toString()); + } + } +} + @AutomaticallyRegisteredFeature class MetadataTracerFeature implements InternalFeature { @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/doc-files/RecordMetadataHelp.txt b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/doc-files/RecordMetadataHelp.txt new file mode 100644 index 000000000000..2fcf45acc323 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/doc-files/RecordMetadataHelp.txt @@ -0,0 +1,9 @@ +Enables metadata tracing at run time. This option is only supported if -H:MetadataTracingSupport is enabled when building the image. +The value of this option is a comma-separated list of arguments specified as key-value pairs. The following arguments are supported: + +- path= (required): Specifies the directory to write traced metadata to. +- mode= (optional): Specifies the tracing mode, which can be one of: + - default: Traces without conditions. + - conditional: Traces and infers conditions for each metadata element. An element (a type, resource, etc.) is only reachable in the image if the associated condition is satisfied, which can reduce image size and improve analysis results. + +Example usage: -H:RecordMetadata=path=tracing_output,mode=default \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java index e03f04449681..40883789eba9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java @@ -26,8 +26,10 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.regex.Pattern; import org.graalvm.collections.EconomicMap; @@ -46,6 +48,7 @@ import com.oracle.svm.core.layeredimagesingleton.DuplicableImageSingleton; import com.oracle.svm.core.layeredimagesingleton.ImageSingletonWriter; import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonBuilderFlags; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; @@ -188,6 +191,14 @@ private static ClassLoader getCommonClassLoaderOrFail(ClassLoader loader, Class< @Override public Class getProxyClass(ClassLoader loader, Class... interfaces) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + List interfaceNames = new ArrayList<>(interfaces.length); + for (Class iface : interfaces) { + interfaceNames.add(iface.getName()); + } + MetadataTracer.singleton().traceProxyType(interfaceNames); + } + ProxyCacheKey key = new ProxyCacheKey(interfaces); ConditionalRuntimeValue clazzOrError = proxyCache.get(key); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java index 866d3be261a0..8ca407aa2cd4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java @@ -48,6 +48,7 @@ import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.SubstrateConstructorAccessor; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; @@ -269,6 +270,9 @@ public static Object getSerializationConstructorAccessor(Class serializationT for (var singleton : layeredSingletons()) { Object constructorAccessor = singleton.getSerializationConstructorAccessor0(declaringClass, targetConstructorClass); if (constructorAccessor != null) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceSerializationType(declaringClass.getName()); + } return constructorAccessor; } }