Skip to content

Commit 416d017

Browse files
committed
Add dynamic access detection phase
1 parent d37573f commit 416d017

File tree

9 files changed

+705
-5
lines changed

9 files changed

+705
-5
lines changed

substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ public final void applyResults(AnalysisMethod method) {
252252
return;
253253
}
254254

255+
preStrengthenGraphs(graph, method);
256+
255257
graph.resetDebug(debug);
256258
if (beforeCounters != null) {
257259
beforeCounters.collect(graph);
@@ -279,6 +281,8 @@ public final void applyResults(AnalysisMethod method) {
279281
}
280282
}
281283

284+
protected abstract void preStrengthenGraphs(StructuredGraph graph, AnalysisMethod method);
285+
282286
protected abstract void postStrengthenGraphs(StructuredGraph graph, AnalysisMethod method);
283287

284288
protected abstract void persistStrengthenGraph(AnalysisMethod method);

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/NeverInlineTrivial.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,10 @@
4545
* Documents the reason why the annotated code must not be inlined.
4646
*/
4747
String value();
48+
49+
/**
50+
* Specifies the condition under which the annotated code is not inlined.
51+
* If left at the default, the code is never inlined.
52+
*/
53+
String onlyWith() default "";
4854
}

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import com.oracle.svm.core.BuildPhaseProvider.AfterHostedUniverse;
9090
import com.oracle.svm.core.BuildPhaseProvider.CompileQueueFinished;
9191
import com.oracle.svm.core.NeverInline;
92+
import com.oracle.svm.core.NeverInlineTrivial;
9293
import com.oracle.svm.core.RuntimeAssertionsSupport;
9394
import com.oracle.svm.core.SubstrateUtil;
9495
import com.oracle.svm.core.Uninterruptible;
@@ -1566,24 +1567,28 @@ private static Constructor<?>[] copyConstructors(Constructor<?>[] original) {
15661567
private native Constructor<?> getEnclosingConstructor();
15671568

15681569
@Substitute
1570+
@NeverInlineTrivial(value = "Used in dynamic access call usage analysis: DynamicAccessDetectionPhase", onlyWith = "TrackDynamicAccess")
15691571
@CallerSensitive
15701572
private static Class<?> forName(String className) throws Throwable {
15711573
return forName(className, Reflection.getCallerClass());
15721574
}
15731575

15741576
@Substitute
1577+
@NeverInlineTrivial(value = "Used in dynamic access call usage analysis: DynamicAccessDetectionPhase", onlyWith = "TrackDynamicAccess")
15751578
@CallerSensitiveAdapter
15761579
private static Class<?> forName(String className, Class<?> caller) throws Throwable {
15771580
return forName(className, true, caller == null ? ClassLoader.getSystemClassLoader() : caller.getClassLoader(), caller);
15781581
}
15791582

15801583
@Substitute
1584+
@NeverInlineTrivial(value = "Used in dynamic access call usage analysis: DynamicAccessDetectionPhase", onlyWith = "TrackDynamicAccess")
15811585
@CallerSensitive
15821586
private static Class<?> forName(Module module, String className) throws Throwable {
15831587
return forName(module, className, Reflection.getCallerClass());
15841588
}
15851589

15861590
@Substitute
1591+
@NeverInlineTrivial(value = "Used in dynamic access call usage analysis: DynamicAccessDetectionPhase", onlyWith = "TrackDynamicAccess")
15871592
@CallerSensitiveAdapter
15881593
@TargetElement(onlyWith = JDK21OrEarlier.class)
15891594
private static Class<?> forName(@SuppressWarnings("unused") Module module, String className, Class<?> caller) throws Throwable {
@@ -1599,12 +1604,14 @@ private static Class<?> forName(@SuppressWarnings("unused") Module module, Strin
15991604
}
16001605

16011606
@Substitute
1607+
@NeverInlineTrivial(value = "Used in dynamic access call usage analysis: DynamicAccessDetectionPhase", onlyWith = "TrackDynamicAccess")
16021608
@CallerSensitive
16031609
private static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws Throwable {
16041610
return forName(name, initialize, loader, Reflection.getCallerClass());
16051611
}
16061612

16071613
@Substitute
1614+
@NeverInlineTrivial(value = "Used in dynamic access call usage analysis: DynamicAccessDetectionPhase", onlyWith = "TrackDynamicAccess")
16081615
@CallerSensitiveAdapter
16091616
@TargetElement(onlyWith = JDK21OrEarlier.class)
16101617
private static Class<?> forName(String name, boolean initialize, ClassLoader loader, @SuppressWarnings("unused") Class<?> caller) throws Throwable {
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/*
2+
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package com.oracle.svm.hosted;
26+
27+
import com.oracle.svm.core.BuildArtifacts;
28+
import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature;
29+
import com.oracle.svm.core.feature.InternalFeature;
30+
import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue;
31+
import com.oracle.svm.core.option.HostedOptionKey;
32+
import com.oracle.svm.core.option.HostedOptionValues;
33+
import com.oracle.svm.core.option.LocatableMultiOptionValue;
34+
import com.oracle.svm.core.option.SubstrateOptionsParser;
35+
import com.oracle.svm.core.util.UserError;
36+
import com.oracle.svm.hosted.driver.IncludeOptionsSupport;
37+
import com.oracle.svm.hosted.phases.DynamicAccessDetectionPhase;
38+
import jdk.graal.compiler.options.Option;
39+
import jdk.graal.compiler.options.OptionKey;
40+
import jdk.graal.compiler.options.OptionValues;
41+
import jdk.graal.compiler.util.json.JsonBuilder;
42+
import jdk.graal.compiler.util.json.JsonPrettyWriter;
43+
import jdk.vm.ci.meta.ResolvedJavaMethod;
44+
import org.graalvm.collections.EconomicMap;
45+
import org.graalvm.nativeimage.ImageSingletons;
46+
47+
import java.io.IOException;
48+
import java.nio.file.Files;
49+
import java.nio.file.NoSuchFileException;
50+
import java.nio.file.Path;
51+
import java.util.ArrayList;
52+
import java.util.Arrays;
53+
import java.util.HashSet;
54+
import java.util.Objects;
55+
import java.util.Set;
56+
import java.util.TreeMap;
57+
import java.util.List;
58+
import java.util.Map;
59+
import java.util.concurrent.ConcurrentHashMap;
60+
import java.util.concurrent.ConcurrentSkipListMap;
61+
import java.util.stream.Collectors;
62+
import java.util.stream.Stream;
63+
64+
import static com.oracle.svm.hosted.driver.IncludeOptionsSupport.parseIncludeSelector;
65+
66+
/**
67+
* This is a support class that keeps track of dynamic access calls requiring metadata usage
68+
* detected during {@link DynamicAccessDetectionPhase} and outputs them to the image-build output.
69+
*/
70+
@AutomaticallyRegisteredFeature
71+
public final class DynamicAccessDetectionFeature implements InternalFeature {
72+
73+
public static class Options {
74+
@Option(help = "Serialize all method calls requiring metadata for dynamic access in the reached parts of the project, limited to the provided comma-separated list of class path entries and module or package names.")//
75+
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> TrackDynamicAccess = new HostedOptionKey<>(
76+
AccumulatingLocatableMultiOptionValue.Strings.buildWithCommaDelimiter());
77+
78+
@Option(help = "Output all method calls requiring metadata for dynamic access found by -H:TrackDynamicAccess to the console.")
79+
public static final HostedOptionKey<Boolean> ReportDynamicAccessToConsole = new HostedOptionKey<>(false);
80+
}
81+
82+
private record MethodsByAccessKind(Map<DynamicAccessDetectionPhase.DynamicAccessKind, CallLocationsByMethod> methodsByAccessKind) {
83+
MethodsByAccessKind() {
84+
this(new TreeMap<>());
85+
}
86+
87+
public Set<DynamicAccessDetectionPhase.DynamicAccessKind> getAccessKinds() {
88+
return methodsByAccessKind.keySet();
89+
}
90+
91+
public CallLocationsByMethod getCallLocationsByMethod(DynamicAccessDetectionPhase.DynamicAccessKind accessKind) {
92+
return methodsByAccessKind.getOrDefault(accessKind, new CallLocationsByMethod());
93+
}
94+
}
95+
96+
private record CallLocationsByMethod(Map<String, List<String>> callLocationsByMethod) {
97+
CallLocationsByMethod() {
98+
this(new TreeMap<>());
99+
}
100+
101+
public Set<String> getMethods() {
102+
return callLocationsByMethod.keySet();
103+
}
104+
105+
public List<String> getMethodCallLocations(String methodName) {
106+
return callLocationsByMethod.getOrDefault(methodName, new ArrayList<>());
107+
}
108+
}
109+
110+
private static final String OUTPUT_DIR_NAME = "dynamic-access";
111+
112+
private Set<String> sourceEntries; // Class path entries and module or package names
113+
private final Map<String, MethodsByAccessKind> callsBySourceEntry;
114+
private final Set<FoldEntry> foldEntries = ConcurrentHashMap.newKeySet();
115+
private final boolean printToConsole;
116+
117+
public DynamicAccessDetectionFeature() {
118+
callsBySourceEntry = new ConcurrentSkipListMap<>();
119+
printToConsole = Options.ReportDynamicAccessToConsole.getValue();
120+
}
121+
122+
public static DynamicAccessDetectionFeature instance() {
123+
return ImageSingletons.lookup(DynamicAccessDetectionFeature.class);
124+
}
125+
126+
public void addCall(String entry, DynamicAccessDetectionPhase.DynamicAccessKind accessKind, String call, String callLocation) {
127+
MethodsByAccessKind entryContent = callsBySourceEntry.computeIfAbsent(entry, k -> new MethodsByAccessKind());
128+
CallLocationsByMethod methodCallLocations = entryContent.methodsByAccessKind().computeIfAbsent(accessKind, k -> new CallLocationsByMethod());
129+
List<String> callLocations = methodCallLocations.callLocationsByMethod().computeIfAbsent(call, k -> new ArrayList<>());
130+
callLocations.add(callLocation);
131+
}
132+
133+
public MethodsByAccessKind getMethodsByAccessKind(String entry) {
134+
return callsBySourceEntry.getOrDefault(entry, new MethodsByAccessKind());
135+
}
136+
137+
public Set<String> getSourceEntries() {
138+
return sourceEntries;
139+
}
140+
141+
public static String getEntryName(String path) {
142+
String fileName = path.substring(path.lastIndexOf("/") + 1);
143+
if (fileName.endsWith(".jar")) {
144+
fileName = fileName.substring(0, fileName.lastIndexOf('.'));
145+
}
146+
return fileName;
147+
}
148+
149+
private void printReportForEntry(String entry) {
150+
System.out.println("Dynamic method usage detected in " + entry + ":");
151+
MethodsByAccessKind methodsByAccessKind = getMethodsByAccessKind(entry);
152+
for (DynamicAccessDetectionPhase.DynamicAccessKind accessKind : methodsByAccessKind.getAccessKinds()) {
153+
System.out.println(" " + accessKind + " calls detected:");
154+
CallLocationsByMethod methodCallLocations = methodsByAccessKind.getCallLocationsByMethod(accessKind);
155+
for (String call : methodCallLocations.getMethods()) {
156+
System.out.println(" " + call + ":");
157+
for (String callLocation : methodCallLocations.getMethodCallLocations(call)) {
158+
System.out.println(" at " + callLocation);
159+
}
160+
}
161+
}
162+
}
163+
164+
public static Path getOrCreateDirectory(Path directory) throws IOException {
165+
if (Files.exists(directory)) {
166+
if (!Files.isDirectory(directory)) {
167+
throw new NoSuchFileException(directory.toString(), null,
168+
"Failed to retrieve directory: The path exists but is not a directory.");
169+
}
170+
} else {
171+
try {
172+
Files.createDirectory(directory);
173+
} catch (IOException e) {
174+
throw new IOException("Failed to create directory: " + directory, e);
175+
}
176+
}
177+
return directory;
178+
}
179+
180+
private void dumpReportForEntry(String entry) {
181+
try {
182+
Path reportDirectory = getOrCreateDirectory(NativeImageGenerator.generatedFiles(HostedOptionValues.singleton()).resolve(OUTPUT_DIR_NAME));
183+
MethodsByAccessKind methodsByAccessKind = getMethodsByAccessKind(entry);
184+
for (DynamicAccessDetectionPhase.DynamicAccessKind accessKind : methodsByAccessKind.getAccessKinds()) {
185+
Path entryDirectory = getOrCreateDirectory(reportDirectory.resolve(getEntryName(entry)));
186+
Path targetPath = entryDirectory.resolve(accessKind.fileName);
187+
try (var writer = new JsonPrettyWriter(targetPath)) {
188+
try (JsonBuilder.ObjectBuilder dynamicAccessBuilder = writer.objectBuilder()) {
189+
for (String methodName : methodsByAccessKind.getCallLocationsByMethod(accessKind).getMethods()) {
190+
try (JsonBuilder.ArrayBuilder methodCallLocationBuilder = dynamicAccessBuilder.append(methodName).array()) {
191+
for (String methodLocation : methodsByAccessKind.getCallLocationsByMethod(accessKind).getMethodCallLocations(methodName)) {
192+
methodCallLocationBuilder.append(methodLocation);
193+
}
194+
}
195+
}
196+
}
197+
BuildArtifacts.singleton().add(BuildArtifacts.ArtifactType.BUILD_INFO, targetPath);
198+
}
199+
}
200+
} catch (IOException e) {
201+
UserError.abort("Failed to dump report for entry %s:", entry);
202+
}
203+
}
204+
205+
public void reportDynamicAccess() {
206+
for (String entry : sourceEntries) {
207+
if (callsBySourceEntry.containsKey(entry)) {
208+
dumpReportForEntry(entry);
209+
if (printToConsole) {
210+
printReportForEntry(entry);
211+
}
212+
}
213+
}
214+
}
215+
216+
/*
217+
* Support data structure used to keep track of calls which don't require metadata, but can't be
218+
* folded.
219+
*/
220+
public static class FoldEntry {
221+
private final int bci;
222+
private final ResolvedJavaMethod method;
223+
224+
public FoldEntry(int bci, ResolvedJavaMethod method) {
225+
this.bci = bci;
226+
this.method = method;
227+
}
228+
229+
@Override
230+
public boolean equals(Object obj) {
231+
if (this == obj) {
232+
return true;
233+
}
234+
if (obj == null || getClass() != obj.getClass()) {
235+
return false;
236+
}
237+
FoldEntry other = (FoldEntry) obj;
238+
return bci == other.bci && Objects.equals(method, other.method);
239+
}
240+
241+
@Override
242+
public int hashCode() {
243+
return Objects.hash(bci, method);
244+
}
245+
}
246+
247+
public void addFoldEntry(int bci, ResolvedJavaMethod method) {
248+
foldEntries.add(new FoldEntry(bci, method));
249+
}
250+
251+
/*
252+
* If a fold entry exists for the given method, the method should be ignored by the analysis
253+
* phase.
254+
*/
255+
public boolean containsFoldEntry(int bci, ResolvedJavaMethod method) {
256+
return foldEntries.contains(new FoldEntry(bci, method));
257+
}
258+
259+
@Override
260+
public void afterRegistration(AfterRegistrationAccess access) {
261+
ImageClassLoader imageClassLoader = ((FeatureImpl.AfterRegistrationAccessImpl) access).getImageClassLoader();
262+
NativeImageClassLoaderSupport support = imageClassLoader.classLoaderSupport;
263+
NativeImageClassLoaderSupport.IncludeSelectors dynamicAccessSelectors = support.getDynamicAccessSelectors();
264+
265+
Set<String> tmpSet = new HashSet<>();
266+
tmpSet.addAll(dynamicAccessSelectors.classpathEntries().stream()
267+
.map(path -> path.toAbsolutePath().toString())
268+
.collect(Collectors.toSet()));
269+
tmpSet.addAll(dynamicAccessSelectors.moduleNames());
270+
tmpSet.addAll(dynamicAccessSelectors.packages().stream()
271+
.map(Object::toString)
272+
.collect(Collectors.toSet()));
273+
sourceEntries = Set.copyOf(tmpSet);
274+
}
275+
276+
@Override
277+
public void beforeCompilation(BeforeCompilationAccess access) {
278+
DynamicAccessDetectionFeature.instance().reportDynamicAccess();
279+
}
280+
281+
private static String dynamicAccessPossibleOptions() {
282+
return "[" + IncludeOptionsSupport.possibleExtendedOptions() + "]";
283+
}
284+
285+
public static void parseDynamicAccessOptions(EconomicMap<OptionKey<?>, Object> hostedValues, NativeImageClassLoaderSupport classLoaderSupport) {
286+
AccumulatingLocatableMultiOptionValue.Strings trackDynamicAccess = Options.TrackDynamicAccess.getValue(new OptionValues(hostedValues));
287+
Stream<LocatableMultiOptionValue.ValueWithOrigin<String>> valuesWithOrigins = trackDynamicAccess.getValuesWithOrigins();
288+
valuesWithOrigins.forEach(valueWithOrigin -> {
289+
String optionArgument = SubstrateOptionsParser.commandArgument(Options.TrackDynamicAccess, valueWithOrigin.value(), true, false);
290+
var options = Arrays.stream(valueWithOrigin.value().split(",")).toList();
291+
for (String option : options) {
292+
UserError.guarantee(!option.isEmpty(), "Option %s from %s cannot be passed an empty string",
293+
optionArgument, valueWithOrigin.origin());
294+
parseIncludeSelector(optionArgument, valueWithOrigin, classLoaderSupport.getDynamicAccessSelectors(), IncludeOptionsSupport.ExtendedOption.parse(option), dynamicAccessPossibleOptions());
295+
}
296+
});
297+
}
298+
}

0 commit comments

Comments
 (0)