Skip to content

Commit eb2742d

Browse files
committed
[GR-71801] Fix the SubstitutionReportFeature
PullRequest: graal/22772
2 parents 2019e35 + be5af9f commit eb2742d

File tree

7 files changed

+160
-72
lines changed

7 files changed

+160
-72
lines changed

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGeneratorRunner.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,11 @@
9090

9191
public class NativeImageGeneratorRunner {
9292

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

96+
private volatile NativeImageGenerator generator;
97+
9698
public enum BuildOutcome {
9799
SUCCESSFUL,
98100
FAILED,
@@ -189,11 +191,7 @@ public void run() {
189191

190192
private static void checkBootModuleDependencies(boolean verbose) {
191193
Set<Module> allModules = ModuleLayer.boot().modules();
192-
List<Module> builderModules = allModules.stream().filter(m -> m.isNamed() && m.getName().startsWith("org.graalvm.nativeimage.")).toList();
193-
Set<Module> transitiveBuilderModules = new LinkedHashSet<>();
194-
for (Module svmModule : builderModules) {
195-
transitiveReaders(svmModule, allModules, transitiveBuilderModules);
196-
}
194+
Set<Module> transitiveBuilderModules = getNativeImageBuilderModules();
197195
if (verbose) {
198196
System.out.println(transitiveBuilderModules.stream()
199197
.map(Module::getName)
@@ -234,6 +232,28 @@ private static void checkBootModuleDependencies(boolean verbose) {
234232
}
235233
}
236234

235+
/**
236+
* Returns what are considered native-image builder modules: those are the modules with prefix
237+
* {@value NativeImageGeneratorRunner#NATIVE_IMAGE_MODULE_PREFIX} and their reader modules.
238+
*/
239+
public static Set<Module> getNativeImageBuilderModules() {
240+
final var allModules = ModuleLayer.boot().modules();
241+
List<Module> builderModules = new ArrayList<>(allModules.size());
242+
for (Module m : allModules) {
243+
if (m.isNamed()) {
244+
if (m.getName().startsWith(NATIVE_IMAGE_MODULE_PREFIX)) {
245+
builderModules.add(m);
246+
}
247+
}
248+
}
249+
250+
Set<Module> transitiveBuilderModules = new LinkedHashSet<>();
251+
for (Module svmModule : builderModules) {
252+
transitiveReaders(svmModule, allModules, transitiveBuilderModules);
253+
}
254+
return transitiveBuilderModules;
255+
}
256+
237257
public static void transitiveReaders(Module readModule, Set<Module> potentialReaders, Set<Module> actualReaders) {
238258
for (Module potentialReader : potentialReaders) {
239259
if (potentialReader.canRead(readModule)) {

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SubstitutionReportFeature.java

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@
3030
import java.util.TreeMap;
3131
import java.util.function.Function;
3232

33-
import jdk.graal.compiler.options.Option;
34-
35-
import com.oracle.svm.util.OriginalClassProvider;
3633
import com.oracle.graal.pointsto.meta.AnalysisField;
3734
import com.oracle.graal.pointsto.meta.AnalysisMethod;
3835
import com.oracle.graal.pointsto.meta.AnalysisType;
@@ -44,27 +41,56 @@
4441
import com.oracle.svm.hosted.substitute.SubstitutionField;
4542
import com.oracle.svm.hosted.substitute.SubstitutionMethod;
4643
import com.oracle.svm.hosted.substitute.SubstitutionType;
44+
import com.oracle.svm.util.OriginalClassProvider;
4745

46+
import jdk.graal.compiler.options.Option;
4847
import jdk.vm.ci.meta.ResolvedJavaField;
4948
import jdk.vm.ci.meta.ResolvedJavaMethod;
5049
import jdk.vm.ci.meta.ResolvedJavaType;
5150

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

5573
static class Options {
56-
@Option(help = "Report performed substitutions")//
74+
@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.") //
5775
public static final HostedOptionKey<Boolean> ReportPerformedSubstitutions = new HostedOptionKey<>(false);
76+
77+
@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.") //
78+
public static final HostedOptionKey<Boolean> ReportPerformedUserSubstitutions = new HostedOptionKey<>(false);
5879
}
5980

60-
private final boolean enabled = Options.ReportPerformedSubstitutions.getValue();
81+
private final boolean enabled = Options.ReportPerformedSubstitutions.getValue() || Options.ReportPerformedUserSubstitutions.getValue();
82+
83+
/// Aggregated substitutions grouped by the [CodeSource] location (typically the JAR URL)
84+
/// of the annotated (replacement) elements.
6185
private final Map<String, Substitutions> substitutions = new TreeMap<>();
6286

87+
/// Only participate in the image build when the reporting option is enabled.
6388
@Override
6489
public boolean isInConfiguration(IsInConfigurationAccess access) {
6590
return enabled;
6691
}
6792

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

103+
/// Scans all reachable, non-array [AnalysisType] types and records those that are user
104+
/// substitutions.
105+
/// A [SubstitutionType] represents an annotated replacement for an original type.
77106
private void findSubstitutedTypes(FeatureImpl.AfterAnalysisAccessImpl access) {
78107
for (AnalysisType type : access.getUniverse().getTypes()) {
79108
if (type.isReachable() && !type.isArray()) {
80109
ResolvedJavaType t = type.getWrapped();
81-
if (t instanceof SubstitutionType) {
82-
SubstitutionType subType = (SubstitutionType) t;
83-
if (subType.isUserSubstitution()) {
84-
String jarLocation = getTypeClassFileLocation(subType.getAnnotated());
110+
if (t instanceof SubstitutionType substType) {
111+
// Only report substitutions authored by users (filter out internal ones).
112+
if (shouldReport(substType.isUserSubstitution())) {
113+
String jarLocation = getTypeClassFileLocation(substType.getAnnotated());
85114
substitutions.putIfAbsent(jarLocation, new Substitutions());
86-
substitutions.get(jarLocation).addType(subType);
115+
substitutions.get(jarLocation).addType(substType);
87116
}
88117
}
89118
}
90119
}
91120
}
92121

122+
/// Scans all [AnalysisMethod] methods in the analysis universe and records those that are
123+
/// user substitutions.
124+
/// A [SubstitutionMethod] holds both original and annotated (replacement) methods.
93125
private void findSubstitutedMethods(FeatureImpl.AfterAnalysisAccessImpl access) {
94126
for (AnalysisMethod method : access.getUniverse().getMethods()) {
95-
if (method.wrapped instanceof SubstitutionMethod) {
96-
SubstitutionMethod subMethod = (SubstitutionMethod) method.wrapped;
97-
if (subMethod.isUserSubstitution()) {
98-
String jarLocation = getTypeClassFileLocation(subMethod.getAnnotated().getDeclaringClass());
127+
if (method.wrapped instanceof SubstitutionMethod substMethod) {
128+
if (shouldReport(substMethod.isUserSubstitution())) {
129+
String jarLocation = getTypeClassFileLocation(substMethod.getAnnotated().getDeclaringClass());
99130
substitutions.putIfAbsent(jarLocation, new Substitutions());
100-
substitutions.get(jarLocation).addMethod(subMethod);
131+
substitutions.get(jarLocation).addMethod(substMethod);
101132
}
102133
}
103134
}
104135
}
105136

137+
/// Scans all [AnalysisField] fields in the analysis universe and records those that are
138+
/// user substitutions.
139+
/// A [SubstitutionField] holds both original and annotated (replacement) fields.
106140
private void findSubstitutedFields(FeatureImpl.AfterAnalysisAccessImpl access) {
107141
for (AnalysisField field : access.getUniverse().getFields()) {
108-
if (field.wrapped instanceof SubstitutionField) {
109-
SubstitutionField subField = (SubstitutionField) field.wrapped;
110-
if (subField.isUserSubstitution()) {
111-
String jarLocation = getTypeClassFileLocation(subField.getAnnotated().getDeclaringClass());
142+
if (field.wrapped instanceof SubstitutionField substField) {
143+
if (shouldReport(substField.isUserSubstitution())) {
144+
String jarLocation = getTypeClassFileLocation(substField.getAnnotated().getDeclaringClass());
112145
substitutions.putIfAbsent(jarLocation, new Substitutions());
113-
substitutions.get(jarLocation).addField(subField);
146+
substitutions.get(jarLocation).addField(substField);
114147
}
115148
}
116149
}
117150
}
118151

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

175+
private static boolean shouldReport(boolean userSubstitution) {
176+
return userSubstitution || Options.ReportPerformedSubstitutions.getValue();
177+
}
178+
179+
/// Fully-qualified type representation.
136180
private static String formatType(ResolvedJavaType t) {
137181
return t.toJavaName(true);
138182
}
139183

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

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

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

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

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

0 commit comments

Comments
 (0)