Skip to content

Commit 273bc66

Browse files
committed
Add dynamic access detection phase
1 parent 21a0c8b commit 273bc66

File tree

6 files changed

+544
-4
lines changed

6 files changed

+544
-4
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/hub/DynamicHub.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import com.oracle.svm.core.BuildPhaseProvider.AfterHostedUniverse;
9292
import com.oracle.svm.core.BuildPhaseProvider.CompileQueueFinished;
9393
import com.oracle.svm.core.NeverInline;
94+
import com.oracle.svm.core.NeverInlineTrivial;
9495
import com.oracle.svm.core.RuntimeAssertionsSupport;
9596
import com.oracle.svm.core.SubstrateUtil;
9697
import com.oracle.svm.core.Uninterruptible;
@@ -1602,27 +1603,31 @@ private static Constructor<?>[] copyConstructors(Constructor<?>[] original) {
16021603
private native Constructor<?> getEnclosingConstructor();
16031604

16041605
@Substitute
1606+
@NeverInlineTrivial("Used in dynamic access call usage analysis: DynamicAccessDetectionPhase")
16051607
@Platforms(InternalPlatform.NATIVE_ONLY.class)
16061608
@CallerSensitive
16071609
private static Class<?> forName(String className) throws Throwable {
16081610
return forName(className, Reflection.getCallerClass());
16091611
}
16101612

16111613
@Substitute
1614+
@NeverInlineTrivial("Used in dynamic access call usage analysis: DynamicAccessDetectionPhase")
16121615
@Platforms(InternalPlatform.NATIVE_ONLY.class)
16131616
@CallerSensitiveAdapter
16141617
private static Class<?> forName(String className, Class<?> caller) throws Throwable {
16151618
return forName(className, true, caller.getClassLoader(), caller);
16161619
}
16171620

16181621
@Substitute
1622+
@NeverInlineTrivial("Used in dynamic access call usage analysis: DynamicAccessDetectionPhase")
16191623
@Platforms(InternalPlatform.NATIVE_ONLY.class)
16201624
@CallerSensitive
16211625
private static Class<?> forName(Module module, String className) throws Throwable {
16221626
return forName(module, className, Reflection.getCallerClass());
16231627
}
16241628

16251629
@Substitute
1630+
@NeverInlineTrivial("Used in dynamic access call usage analysis: DynamicAccessDetectionPhase")
16261631
@Platforms(InternalPlatform.NATIVE_ONLY.class)
16271632
@CallerSensitiveAdapter
16281633
@TargetElement(onlyWith = JDK21OrEarlier.class)
@@ -1639,12 +1644,14 @@ private static Class<?> forName(@SuppressWarnings("unused") Module module, Strin
16391644
}
16401645

16411646
@Substitute
1647+
@NeverInlineTrivial("Used in dynamic access call usage analysis: DynamicAccessDetectionPhase")
16421648
@CallerSensitive
16431649
private static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws Throwable {
16441650
return forName(name, initialize, loader, Reflection.getCallerClass());
16451651
}
16461652

16471653
@Substitute
1654+
@NeverInlineTrivial("Used in dynamic access call usage analysis: DynamicAccessDetectionPhase")
16481655
@CallerSensitiveAdapter
16491656
@TargetElement(onlyWith = JDK21OrEarlier.class)
16501657
private static Class<?> forName(String name, boolean initialize, ClassLoader loader, @SuppressWarnings("unused") Class<?> caller) throws Throwable {
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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.util.UserError;
34+
import com.oracle.svm.hosted.phases.DynamicAccessDetectionPhase;
35+
import jdk.graal.compiler.options.Option;
36+
import jdk.graal.compiler.util.json.JsonBuilder;
37+
import jdk.graal.compiler.util.json.JsonPrettyWriter;
38+
import jdk.vm.ci.meta.ResolvedJavaMethod;
39+
import org.graalvm.nativeimage.ImageSingletons;
40+
41+
import java.io.IOException;
42+
import java.nio.file.Files;
43+
import java.nio.file.NoSuchFileException;
44+
import java.nio.file.Path;
45+
import java.util.ArrayList;
46+
import java.util.Locale;
47+
import java.util.Objects;
48+
import java.util.Set;
49+
import java.util.TreeMap;
50+
import java.util.List;
51+
import java.util.Map;
52+
import java.util.concurrent.ConcurrentHashMap;
53+
import java.util.concurrent.ConcurrentSkipListMap;
54+
55+
/**
56+
* This is a support class that keeps track of dynamic access calls requiring metadata usage
57+
* detected during {@link DynamicAccessDetectionPhase} and outputs them to the image-build output.
58+
*/
59+
@AutomaticallyRegisteredFeature
60+
public final class DynamicAccessDetectionFeature implements InternalFeature {
61+
62+
public static class Options {
63+
@Option(help = "Output all metadata requiring dynamic access call usages in the reached parts of the project, limited to the provided comma-separated list of class path or module path entries.")//
64+
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> TrackDynamicAccess = new HostedOptionKey<>(
65+
AccumulatingLocatableMultiOptionValue.Strings.buildWithCommaDelimiter());
66+
}
67+
68+
private record MethodsByType(Map<String, CallLocationsByMethod> methodsByType) {
69+
MethodsByType() {
70+
this(new TreeMap<>());
71+
}
72+
73+
public Set<String> getMethodTypes() {
74+
return methodsByType.keySet();
75+
}
76+
77+
public CallLocationsByMethod getCallLocationsByMethod(String methodType) {
78+
return methodsByType.getOrDefault(methodType, new CallLocationsByMethod());
79+
}
80+
}
81+
82+
private record CallLocationsByMethod(Map<String, List<String>> callLocationsByMethod) {
83+
CallLocationsByMethod() {
84+
this(new TreeMap<>());
85+
}
86+
87+
public Set<String> getMethods() {
88+
return callLocationsByMethod.keySet();
89+
}
90+
91+
public List<String> getMethodCallLocations(String methodName) {
92+
return callLocationsByMethod.getOrDefault(methodName, new ArrayList<>());
93+
}
94+
}
95+
96+
private static final String OUTPUT_DIR_NAME = "dynamic-access";
97+
98+
private final Set<String> pathEntries; // Class or module path entries
99+
private final Map<String, MethodsByType> callsByPathEntry;
100+
private final Set<FoldEntry> foldEntries = ConcurrentHashMap.newKeySet();
101+
102+
public DynamicAccessDetectionFeature() {
103+
this.callsByPathEntry = new ConcurrentSkipListMap<>();
104+
this.pathEntries = Set.copyOf(Options.TrackDynamicAccess.getValue().values().stream().map(entry -> {
105+
if (entry.endsWith("/")) {
106+
return entry.substring(0, entry.length() - 1);
107+
}
108+
return entry;
109+
}).toList());
110+
}
111+
112+
public static DynamicAccessDetectionFeature instance() {
113+
return ImageSingletons.lookup(DynamicAccessDetectionFeature.class);
114+
}
115+
116+
public void addCall(String entry, String methodType, String call, String callLocation) {
117+
MethodsByType entryContent = this.callsByPathEntry.computeIfAbsent(entry, k -> new MethodsByType());
118+
CallLocationsByMethod methodCallLocations = entryContent.methodsByType().computeIfAbsent(methodType, k -> new CallLocationsByMethod());
119+
List<String> callLocations = methodCallLocations.callLocationsByMethod().computeIfAbsent(call, k -> new ArrayList<>());
120+
callLocations.add(callLocation);
121+
}
122+
123+
public MethodsByType getMethodsByType(String entry) {
124+
return this.callsByPathEntry.getOrDefault(entry, new MethodsByType());
125+
}
126+
127+
public Set<String> getPathEntries() {
128+
return pathEntries;
129+
}
130+
131+
public static String getEntryName(String path) {
132+
String fileName = path.substring(path.lastIndexOf("/") + 1);
133+
if (fileName.endsWith(".jar")) {
134+
fileName = fileName.substring(0, fileName.lastIndexOf('.'));
135+
}
136+
return fileName;
137+
}
138+
139+
private void printReportForEntry(String entry) {
140+
System.out.println("Dynamic method usage detected in " + entry + ":");
141+
MethodsByType methodsByType = getMethodsByType(entry);
142+
for (String methodType : methodsByType.getMethodTypes()) {
143+
System.out.println(" " + methodType.substring(0, 1).toUpperCase(Locale.ROOT) + methodType.substring(1) + " calls detected:");
144+
CallLocationsByMethod methodCallLocations = methodsByType.getCallLocationsByMethod(methodType);
145+
for (String call : methodCallLocations.getMethods()) {
146+
System.out.println(" " + call + ":");
147+
for (String callLocation : methodCallLocations.getMethodCallLocations(call)) {
148+
System.out.println(" at " + callLocation);
149+
}
150+
}
151+
}
152+
}
153+
154+
public static Path getOrCreateDirectory(Path directory) throws IOException {
155+
if (Files.exists(directory)) {
156+
if (!Files.isDirectory(directory)) {
157+
throw new NoSuchFileException(directory.toString(), null,
158+
"Failed to retrieve directory: The path exists but is not a directory.");
159+
}
160+
} else {
161+
try {
162+
Files.createDirectory(directory);
163+
} catch (IOException e) {
164+
throw new IOException("Failed to create directory: " + directory, e);
165+
}
166+
}
167+
return directory;
168+
}
169+
170+
private void dumpReportForEntry(String entry) {
171+
try {
172+
Path reportDirectory = getOrCreateDirectory(NativeImageGenerator.generatedFiles(HostedOptionValues.singleton()).resolve(OUTPUT_DIR_NAME));
173+
MethodsByType methodsByType = getMethodsByType(entry);
174+
for (String methodType : methodsByType.getMethodTypes()) {
175+
Path entryDirectory = getOrCreateDirectory(reportDirectory.resolve(getEntryName(entry)));
176+
Path targetPath = entryDirectory.resolve(methodType + "_calls.json");
177+
try (var writer = new JsonPrettyWriter(targetPath)) {
178+
try (JsonBuilder.ObjectBuilder dynamicAccessBuilder = writer.objectBuilder()) {
179+
for (String methodName : methodsByType.getCallLocationsByMethod(methodType).getMethods()) {
180+
try (JsonBuilder.ArrayBuilder methodCallLocationBuilder = dynamicAccessBuilder.append(methodName).array()) {
181+
for (String methodLocation : methodsByType.getCallLocationsByMethod(methodType).getMethodCallLocations(methodName)) {
182+
methodCallLocationBuilder.append(methodLocation);
183+
}
184+
}
185+
}
186+
}
187+
BuildArtifacts.singleton().add(BuildArtifacts.ArtifactType.BUILD_INFO, targetPath);
188+
}
189+
}
190+
} catch (IOException e) {
191+
UserError.abort("Failed to dump report for entry %s:", entry);
192+
}
193+
}
194+
195+
public void reportDynamicAccess() {
196+
for (String entry : pathEntries) {
197+
if (callsByPathEntry.containsKey(entry)) {
198+
printReportForEntry(entry);
199+
dumpReportForEntry(entry);
200+
}
201+
}
202+
}
203+
204+
/*
205+
* Support data structure used to keep track of calls which don't require metadata, but can't be
206+
* folded.
207+
*/
208+
public static class FoldEntry {
209+
private final int bci;
210+
private final ResolvedJavaMethod method;
211+
212+
public FoldEntry(int bci, ResolvedJavaMethod method) {
213+
this.bci = bci;
214+
this.method = method;
215+
}
216+
217+
@Override
218+
public boolean equals(Object obj) {
219+
if (this == obj) {
220+
return true;
221+
}
222+
if (obj == null || getClass() != obj.getClass()) {
223+
return false;
224+
}
225+
FoldEntry other = (FoldEntry) obj;
226+
return bci == other.bci && Objects.equals(method, other.method);
227+
}
228+
229+
@Override
230+
public int hashCode() {
231+
return Objects.hash(bci, method);
232+
}
233+
}
234+
235+
public void addFoldEntry(int bci, ResolvedJavaMethod method) {
236+
this.foldEntries.add(new FoldEntry(bci, method));
237+
}
238+
239+
/*
240+
* If a fold entry exists for the given method, the method should be ignored by the analysis
241+
* phase.
242+
*/
243+
public boolean containsFoldEntry(int bci, ResolvedJavaMethod method) {
244+
return this.foldEntries.contains(new FoldEntry(bci, method));
245+
}
246+
247+
@Override
248+
public void beforeCompilation(BeforeCompilationAccess access) {
249+
DynamicAccessDetectionFeature.instance().reportDynamicAccess();
250+
}
251+
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import com.oracle.svm.hosted.code.SubstrateCompilationDirectives;
4444
import com.oracle.svm.hosted.imagelayer.HostedImageLayerBuildingSupport;
4545
import com.oracle.svm.hosted.meta.HostedType;
46+
47+
import com.oracle.svm.hosted.phases.DynamicAccessDetectionPhase;
4648
import com.oracle.svm.hosted.phases.AnalyzeJavaHomeAccessPhase;
4749

4850
import jdk.graal.compiler.graph.Node;
@@ -60,14 +62,22 @@
6062
import jdk.vm.ci.meta.JavaTypeProfile;
6163

6264
public class SubstrateStrengthenGraphs extends StrengthenGraphs {
63-
65+
private final Boolean trackDynamicAccess;
6466
private final Boolean trackJavaHomeAccess;
6567
private final Boolean trackJavaHomeAccessDetailed;
6668

6769
public SubstrateStrengthenGraphs(Inflation bb, Universe converter) {
6870
super(bb, converter);
69-
trackJavaHomeAccess = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccess.getValue(bb.getOptions());
70-
trackJavaHomeAccessDetailed = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccessDetailed.getValue(bb.getOptions());
71+
trackDynamicAccess = DynamicAccessDetectionFeature.Options.TrackDynamicAccess.hasBeenSet();
72+
trackJavaHomeAccess = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccess.getValue();
73+
trackJavaHomeAccessDetailed = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccessDetailed.getValue();
74+
}
75+
76+
@Override
77+
protected void preStrengthenGraphs(StructuredGraph graph, AnalysisMethod method) {
78+
if (trackDynamicAccess) {
79+
new DynamicAccessDetectionPhase().apply(graph, bb.getProviders(method));
80+
}
7181
}
7282

7383
@Override

0 commit comments

Comments
 (0)