Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@

public class NativeImageGeneratorRunner {

private volatile NativeImageGenerator generator;
public static final String NATIVE_IMAGE_MODULE_PREFIX = "org.graalvm.nativeimage.";
public static final String IMAGE_BUILDER_ARG_FILE_OPTION = "--image-args-file=";

private volatile NativeImageGenerator generator;

public enum BuildOutcome {
SUCCESSFUL,
FAILED,
Expand Down Expand Up @@ -189,11 +191,7 @@ public void run() {

private static void checkBootModuleDependencies(boolean verbose) {
Set<Module> allModules = ModuleLayer.boot().modules();
List<Module> builderModules = allModules.stream().filter(m -> m.isNamed() && m.getName().startsWith("org.graalvm.nativeimage.")).toList();
Set<Module> transitiveBuilderModules = new LinkedHashSet<>();
for (Module svmModule : builderModules) {
transitiveReaders(svmModule, allModules, transitiveBuilderModules);
}
Set<Module> transitiveBuilderModules = getNativeImageBuilderModules();
if (verbose) {
System.out.println(transitiveBuilderModules.stream()
.map(Module::getName)
Expand Down Expand Up @@ -234,6 +232,28 @@ private static void checkBootModuleDependencies(boolean verbose) {
}
}

/**
* Returns what are considered native-image builder modules: those are the modules with prefix
* {@value NativeImageGeneratorRunner#NATIVE_IMAGE_MODULE_PREFIX} and their reader modules.
*/
public static Set<Module> getNativeImageBuilderModules() {
final var allModules = ModuleLayer.boot().modules();
List<Module> builderModules = new ArrayList<>(allModules.size());
for (Module m : allModules) {
if (m.isNamed()) {
if (m.getName().startsWith(NATIVE_IMAGE_MODULE_PREFIX)) {
builderModules.add(m);
}
}
}

Set<Module> transitiveBuilderModules = new LinkedHashSet<>();
for (Module svmModule : builderModules) {
transitiveReaders(svmModule, allModules, transitiveBuilderModules);
}
return transitiveBuilderModules;
}

public static void transitiveReaders(Module readModule, Set<Module> potentialReaders, Set<Module> actualReaders) {
for (Module potentialReader : potentialReaders) {
if (potentialReader.canRead(readModule)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@
import java.util.TreeMap;
import java.util.function.Function;

import jdk.graal.compiler.options.Option;

import com.oracle.svm.util.OriginalClassProvider;
import com.oracle.graal.pointsto.meta.AnalysisField;
import com.oracle.graal.pointsto.meta.AnalysisMethod;
import com.oracle.graal.pointsto.meta.AnalysisType;
Expand All @@ -44,27 +41,56 @@
import com.oracle.svm.hosted.substitute.SubstitutionField;
import com.oracle.svm.hosted.substitute.SubstitutionMethod;
import com.oracle.svm.hosted.substitute.SubstitutionType;
import com.oracle.svm.util.OriginalClassProvider;

import jdk.graal.compiler.options.Option;
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaMethod;
import jdk.vm.ci.meta.ResolvedJavaType;

/// Feature that reports substitutions discovered during analysis: all with [Options#ReportPerformedSubstitutions],
/// or only user-authored with [Options#ReportPerformedUserSubstitutions].
/// > Note: The feature does not report `@Delete`, `@RecomputeFieldValue`, and `@InjectAccessors` annotations.
///
/// When enabled via [Options#ReportPerformedSubstitutions] or [Options#ReportPerformedUserSubstitutions],
/// this feature scans the analysis universe after the analysis phase completes and collects substitutions:
/// - type substitutions ([SubstitutionType])
/// - method substitutions ([SubstitutionMethod])
/// - field substitutions ([SubstitutionField])
///
/// The results are emitted as a CSV report using [ReportUtils] under the [SubstrateOptions#reportsPath()] directory.
/// The report is named:
/// `substitutions_<date>_<time>.csv` with the following columns:
/// location, category (type/method/field), original, annotated
/// where:
/// - location: the originating code source (e.g., JAR URL) of the annotated (replacement) element
/// - category: one of "type", "method", "field"
/// - original: the original element being substituted
/// - annotated: the annotated replacement element
@AutomaticallyRegisteredFeature
public class SubstitutionReportFeature implements InternalFeature {

static class Options {
@Option(help = "Report performed substitutions")//
@Option(help = "Write a CSV report of all substitutions discovered during analysis to reports/substitutions<date>-<time>.csv. Columns: location, category (type/method/field), original, annotated.") //
public static final HostedOptionKey<Boolean> ReportPerformedSubstitutions = new HostedOptionKey<>(false);

@Option(help = "Write a CSV report of user-authored substitutions originating from the application classpath or module path to reports/substitutions<date>-<time>.csv. Columns: location, category (type/method/field), original, annotated.") //
public static final HostedOptionKey<Boolean> ReportPerformedUserSubstitutions = new HostedOptionKey<>(false);
}

private final boolean enabled = Options.ReportPerformedSubstitutions.getValue();
private final boolean enabled = Options.ReportPerformedSubstitutions.getValue() || Options.ReportPerformedUserSubstitutions.getValue();

/// Aggregated substitutions grouped by the [CodeSource] location (typically the JAR URL)
/// of the annotated (replacement) elements.
private final Map<String, Substitutions> substitutions = new TreeMap<>();

/// Only participate in the image build when the reporting option is enabled.
@Override
public boolean isInConfiguration(IsInConfigurationAccess access) {
return enabled;
}

/// After analysis completes, collect all user-provided substitutions and write the CSV report.
@Override
public void afterAnalysis(AfterAnalysisAccess access) {
FeatureImpl.AfterAnalysisAccessImpl accessImpl = (FeatureImpl.AfterAnalysisAccessImpl) access;
Expand All @@ -74,48 +100,61 @@ public void afterAnalysis(AfterAnalysisAccess access) {
reportSubstitutions();
}

/// Scans all reachable, non-array [AnalysisType] types and records those that are user
/// substitutions.
/// A [SubstitutionType] represents an annotated replacement for an original type.
private void findSubstitutedTypes(FeatureImpl.AfterAnalysisAccessImpl access) {
for (AnalysisType type : access.getUniverse().getTypes()) {
if (type.isReachable() && !type.isArray()) {
ResolvedJavaType t = type.getWrapped();
if (t instanceof SubstitutionType) {
SubstitutionType subType = (SubstitutionType) t;
if (subType.isUserSubstitution()) {
String jarLocation = getTypeClassFileLocation(subType.getAnnotated());
if (t instanceof SubstitutionType substType) {
// Only report substitutions authored by users (filter out internal ones).
if (shouldReport(substType.isUserSubstitution())) {
String jarLocation = getTypeClassFileLocation(substType.getAnnotated());
substitutions.putIfAbsent(jarLocation, new Substitutions());
substitutions.get(jarLocation).addType(subType);
substitutions.get(jarLocation).addType(substType);
}
}
}
}
}

/// Scans all [AnalysisMethod] methods in the analysis universe and records those that are
/// user substitutions.
/// A [SubstitutionMethod] holds both original and annotated (replacement) methods.
private void findSubstitutedMethods(FeatureImpl.AfterAnalysisAccessImpl access) {
for (AnalysisMethod method : access.getUniverse().getMethods()) {
if (method.wrapped instanceof SubstitutionMethod) {
SubstitutionMethod subMethod = (SubstitutionMethod) method.wrapped;
if (subMethod.isUserSubstitution()) {
String jarLocation = getTypeClassFileLocation(subMethod.getAnnotated().getDeclaringClass());
if (method.wrapped instanceof SubstitutionMethod substMethod) {
if (shouldReport(substMethod.isUserSubstitution())) {
String jarLocation = getTypeClassFileLocation(substMethod.getAnnotated().getDeclaringClass());
substitutions.putIfAbsent(jarLocation, new Substitutions());
substitutions.get(jarLocation).addMethod(subMethod);
substitutions.get(jarLocation).addMethod(substMethod);
}
}
}
}

/// Scans all [AnalysisField] fields in the analysis universe and records those that are
/// user substitutions.
/// A [SubstitutionField] holds both original and annotated (replacement) fields.
private void findSubstitutedFields(FeatureImpl.AfterAnalysisAccessImpl access) {
for (AnalysisField field : access.getUniverse().getFields()) {
if (field.wrapped instanceof SubstitutionField) {
SubstitutionField subField = (SubstitutionField) field.wrapped;
if (subField.isUserSubstitution()) {
String jarLocation = getTypeClassFileLocation(subField.getAnnotated().getDeclaringClass());
if (field.wrapped instanceof SubstitutionField substField) {
if (shouldReport(substField.isUserSubstitution())) {
String jarLocation = getTypeClassFileLocation(substField.getAnnotated().getDeclaringClass());
substitutions.putIfAbsent(jarLocation, new Substitutions());
substitutions.get(jarLocation).addField(subField);
substitutions.get(jarLocation).addField(substField);
}
}
}
}

/// Emits the CSV report with one row per discovered substitution.
/// The output file will be created under [SubstrateOptions#reportsPath()] with the name
/// `substitutions<date>_<time>.csv`. Rows are grouped by code source location and sorted using
/// human-readable formatting for types, methods, and fields (see
/// [#formatType(ResolvedJavaType)], [#formatMethod(ResolvedJavaMethod)],
/// [#formatField(ResolvedJavaField)]).
private void reportSubstitutions() {
ReportUtils.report("substitutions performed by native-image", SubstrateOptions.reportsPath(), "substitutions", "csv", pw -> {
pw.println("location, category (type/method/field), original, annotated");
Expand All @@ -133,28 +172,50 @@ private void reportSubstitutions() {
});
}

private static boolean shouldReport(boolean userSubstitution) {
return userSubstitution || Options.ReportPerformedSubstitutions.getValue();
}

/// Fully-qualified type representation.
private static String formatType(ResolvedJavaType t) {
return t.toJavaName(true);
}

/// Human-readable method representation.
/// Format: `pkg.Class#methodName`
private static String formatMethod(ResolvedJavaMethod method) {
return method.format("%H#%n");
}

/// Human-readable field representation.
/// Format: `pkg.Class.fieldName`
private static String formatField(ResolvedJavaField field) {
return field.format("%H.%n");
}

/// Determines the [CodeSource] (typically the JAR URL) that contains the given type (via
/// [OriginalClassProvider]).
/// Falls back to "unknown" when no code source is available (e.g., dynamically defined
/// classes).
private static String getTypeClassFileLocation(ResolvedJavaType type) {
Class<?> annotatedClass = OriginalClassProvider.getJavaClass(type);
CodeSource source = annotatedClass.getProtectionDomain().getCodeSource();
return source == null ? "unknown" : source.getLocation().toString();
return source == null || source.getLocation() == null ? "unknown" : source.getLocation().toString();
}

/// Formats a single CSV row representing one substitution. The JAR location is quoted to guard
/// against commas in URLs; other columns are formatted using the provided formatter.
private static <T> String formatSubstitution(String jar, String type, T original, T annotated, Function<T, String> formatter) {
return '\'' + jar + "'," + type + ',' + formatter.apply(original) + ',' + formatter.apply(annotated);
}

/// Container for collected substitutions for a single code source location.
///
/// Maps are keyed by the original element and map to the annotated (replacement) element.
/// TreeMaps are used with comparators based on the human-readable formatters (see
/// [#formatType(ResolvedJavaType)], [#formatMethod(ResolvedJavaMethod)],
/// [#formatField(ResolvedJavaField)]) so the output
/// remains stable and easy to consume.
private static final class Substitutions {
private final Map<ResolvedJavaType, ResolvedJavaType> substitutedTypes = new TreeMap<>(Comparator.comparing(SubstitutionReportFeature::formatType));
private final Map<ResolvedJavaMethod, ResolvedJavaMethod> substitutedMethods = new TreeMap<>(Comparator.comparing(SubstitutionReportFeature::formatMethod));
Expand Down
Loading