Skip to content

Commit

Permalink
Add reflection detection phase
Browse files Browse the repository at this point in the history
  • Loading branch information
jormundur00 committed Dec 16, 2024
1 parent 0c2731c commit 3eaf0db
Show file tree
Hide file tree
Showing 7 changed files with 485 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ public final void applyResults(AnalysisMethod method) {
return;
}

preStrengthenGraphs(graph, method);

graph.resetDebug(debug);
if (beforeCounters != null) {
beforeCounters.collect(graph);
Expand Down Expand Up @@ -274,6 +276,8 @@ public final void applyResults(AnalysisMethod method) {
}
}

protected abstract void preStrengthenGraphs(StructuredGraph graph, AnalysisMethod method);

protected abstract void postStrengthenGraphs(StructuredGraph graph, AnalysisMethod method);

protected abstract void persistStrengthenGraph(AnalysisMethod method);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import java.util.Optional;
import java.util.StringJoiner;

import com.oracle.svm.core.NeverInlineTrivial;
import org.graalvm.nativeimage.AnnotationAccess;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.Platform;
Expand Down Expand Up @@ -1602,27 +1603,31 @@ private static Constructor<?>[] copyConstructors(Constructor<?>[] original) {
private native Constructor<?> getEnclosingConstructor();

@Substitute
@NeverInlineTrivial("Used in metadata requiring call usage analysis: AnalyzeMethodsRequiringMetadataUsagePhase")
@Platforms(InternalPlatform.NATIVE_ONLY.class)
@CallerSensitive
private static Class<?> forName(String className) throws Throwable {
return forName(className, Reflection.getCallerClass());
}

@Substitute
@NeverInlineTrivial("Used in metadata requiring call usage analysis: AnalyzeMethodsRequiringMetadataUsagePhase")
@Platforms(InternalPlatform.NATIVE_ONLY.class)
@CallerSensitiveAdapter
private static Class<?> forName(String className, Class<?> caller) throws Throwable {
return forName(className, true, caller.getClassLoader(), caller);
}

@Substitute
@NeverInlineTrivial("Used in metadata requiring call usage analysis: AnalyzeMethodsRequiringMetadataUsagePhase")
@Platforms(InternalPlatform.NATIVE_ONLY.class)
@CallerSensitive
private static Class<?> forName(Module module, String className) throws Throwable {
return forName(module, className, Reflection.getCallerClass());
}

@Substitute
@NeverInlineTrivial("Used in metadata requiring call usage analysis: AnalyzeMethodsRequiringMetadataUsagePhase")
@Platforms(InternalPlatform.NATIVE_ONLY.class)
@CallerSensitiveAdapter
@TargetElement(onlyWith = JDK21OrEarlier.class)
Expand All @@ -1639,12 +1644,14 @@ private static Class<?> forName(@SuppressWarnings("unused") Module module, Strin
}

@Substitute
@NeverInlineTrivial("Used in metadata requiring call usage analysis: AnalyzeMethodsRequiringMetadataUsagePhase")
@CallerSensitive
private static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws Throwable {
return forName(name, initialize, loader, Reflection.getCallerClass());
}

@Substitute
@NeverInlineTrivial("Used in metadata requiring call usage analysis: AnalyzeMethodsRequiringMetadataUsagePhase")
@CallerSensitiveAdapter
@TargetElement(onlyWith = JDK21OrEarlier.class)
private static Class<?> forName(String name, boolean initialize, ClassLoader loader, @SuppressWarnings("unused") Class<?> caller) throws Throwable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.svm.hosted;

import com.oracle.svm.core.BuildArtifacts;
import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue;
import com.oracle.svm.core.option.HostedOptionKey;
import com.oracle.svm.core.option.HostedOptionValues;
import com.oracle.svm.hosted.phases.AnalyzeMethodsRequiringMetadataUsagePhase;
import jdk.graal.compiler.debug.GraalError;
import jdk.graal.compiler.options.Option;
import jdk.graal.compiler.util.json.JsonBuilder;
import jdk.graal.compiler.util.json.JsonWriter;
import jdk.vm.ci.meta.ResolvedJavaMethod;
import org.graalvm.nativeimage.ImageSingletons;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* This is a support class that keeps track of calls requiring metadata usage detected during
* {@link AnalyzeMethodsRequiringMetadataUsagePhase} and outputs them to the image-build output.
*/
public final class AnalyzeMethodsRequiringMetadataUsageSupport {
public static final String METHODTYPE_REFLECTION = "reflection";
public static final String METHODTYPE_RESOURCE = "resource";
public static final String METHODTYPE_SERIALIZATION = "serialization";
public static final String METHODTYPE_PROXY = "proxy";
private final Map<String, List<String>> reflectiveCalls;
private final Map<String, List<String>> resourceCalls;
private final Map<String, List<String>> serializationCalls;
private final Map<String, List<String>> proxyCalls;
private final Set<String> jarPaths;
private final Set<FoldEntry> foldEntries = ConcurrentHashMap.newKeySet();

public AnalyzeMethodsRequiringMetadataUsageSupport() {
this.reflectiveCalls = new TreeMap<>();
this.resourceCalls = new TreeMap<>();
this.serializationCalls = new TreeMap<>();
this.proxyCalls = new TreeMap<>();
this.jarPaths = Collections.unmodifiableSet(new HashSet<>(AnalyzeMethodsRequiringMetadataUsageSupport.Options.TrackMethodsRequiringMetadata.getValue().values()));
}

public static AnalyzeMethodsRequiringMetadataUsageSupport instance() {
AnalyzeMethodsRequiringMetadataUsageSupport trus = ImageSingletons.lookup(AnalyzeMethodsRequiringMetadataUsageSupport.class);
GraalError.guarantee(trus != null, "Should never be null.");
return trus;
}

public void addCall(String methodType, String call, String callLocation) {
switch (methodType) {
case METHODTYPE_REFLECTION -> this.reflectiveCalls.computeIfAbsent(call, k -> new ArrayList<>()).add(callLocation);
case METHODTYPE_RESOURCE -> this.resourceCalls.computeIfAbsent(call, k -> new ArrayList<>()).add(callLocation);
case METHODTYPE_SERIALIZATION -> this.serializationCalls.computeIfAbsent(call, k -> new ArrayList<>()).add(callLocation);
case METHODTYPE_PROXY -> this.proxyCalls.computeIfAbsent(call, k -> new ArrayList<>()).add(callLocation);
default -> throw new IllegalArgumentException("Unknown method type: " + methodType);
}
}

public void printReport(String methodType) {
Map<String, List<String>> callMap = switch (methodType) {
case METHODTYPE_REFLECTION -> this.reflectiveCalls;
case METHODTYPE_RESOURCE -> this.resourceCalls;
case METHODTYPE_SERIALIZATION -> this.serializationCalls;
case METHODTYPE_PROXY -> this.proxyCalls;
default -> throw new IllegalArgumentException("Unknown method type: " + methodType);
};

System.out.println(methodType.substring(0, 1).toUpperCase() + methodType.substring(1) + " calls detected:");
for (String key : callMap.keySet()) {
System.out.println(" " + key + ":");
callMap.get(key).sort(Comparator.comparing(String::toString));
for (String callLocation : callMap.get(key)) {
System.out.println(" at " + callLocation);
}
}
}

public void dumpReport(String methodType) {
Map<String, List<String>> calls = switch (methodType) {
case METHODTYPE_REFLECTION -> this.reflectiveCalls;
case METHODTYPE_RESOURCE -> this.resourceCalls;
case METHODTYPE_SERIALIZATION -> this.serializationCalls;
case METHODTYPE_PROXY -> this.proxyCalls;
default -> throw new IllegalArgumentException("Unknown method type: " + methodType);
};
String fileName = methodType + "-usage.json";

Path targetPath = NativeImageGenerator.generatedFiles(HostedOptionValues.singleton()).resolve(fileName);
try (var writer = new JsonWriter(targetPath);
var builder = writer.objectBuilder()) {
for (Map.Entry<String, List<String>> entry : calls.entrySet()) {
try (JsonBuilder.ArrayBuilder array = builder.append(entry.getKey()).array()) {
for (String call : entry.getValue()) {
array.append(call);
}
}
}
BuildArtifacts.singleton().add(BuildArtifacts.ArtifactType.BUILD_INFO, targetPath);
} catch (IOException e) {
System.out.println("Failed to print JSON to " + targetPath + ":");
e.printStackTrace(System.out);
}
}

public void reportReflection() {
Map<String, Map<String, List<String>>> callMaps = Map.of(
METHODTYPE_REFLECTION, this.reflectiveCalls,
METHODTYPE_RESOURCE, this.resourceCalls,
METHODTYPE_SERIALIZATION, this.serializationCalls,
METHODTYPE_PROXY, this.proxyCalls);
for (Map.Entry<String, Map<String, List<String>>> entry : callMaps.entrySet()) {
String methodType = entry.getKey();
Map<String, List<String>> calls = entry.getValue();
if (!calls.isEmpty()) {
printReport(methodType);
dumpReport(methodType);
}
}
}

public Set<String> getJarPaths() {
return jarPaths;
}

/*
* Support data structure used to keep track of calls which don't require metadata, but can't be
* folded.
*/
public static class FoldEntry {
private final int bci;
private final ResolvedJavaMethod method;

public FoldEntry(int bci, ResolvedJavaMethod method) {
this.bci = bci;
this.method = method;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
FoldEntry other = (FoldEntry) obj;
return bci == other.bci && Objects.equals(method, other.method);
}

@Override
public int hashCode() {
return Objects.hash(bci, method);
}
}

public void addFoldEntry(int bci, ResolvedJavaMethod method) {
this.foldEntries.add(new FoldEntry(bci, method));
}

/*
* If a fold entry exists for the given method, the method should be ignored by the analysis
* phase.
*/
public boolean containsFoldEntry(int bci, ResolvedJavaMethod method) {
return this.foldEntries.contains(new FoldEntry(bci, method));
}

public static class Options {
@Option(help = "Output all metadata requiring call usages in the reached parts of the project, limited to the provided comma-separated list of JAR files.")//
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> TrackMethodsRequiringMetadata = new HostedOptionKey<>(
AccumulatingLocatableMultiOptionValue.Strings.buildWithCommaDelimiter());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import com.oracle.svm.hosted.code.SubstrateCompilationDirectives;
import com.oracle.svm.hosted.imagelayer.HostedImageLayerBuildingSupport;
import com.oracle.svm.hosted.meta.HostedType;

import com.oracle.svm.hosted.phases.AnalyzeMethodsRequiringMetadataUsagePhase;
import com.oracle.svm.hosted.phases.AnalyzeJavaHomeAccessPhase;

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

public class SubstrateStrengthenGraphs extends StrengthenGraphs {

private final Boolean trackMetadataRequiringMethodUsage;
private final Boolean trackJavaHomeAccess;
private final Boolean trackJavaHomeAccessDetailed;

public SubstrateStrengthenGraphs(Inflation bb, Universe converter) {
super(bb, converter);
trackJavaHomeAccess = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccess.getValue(bb.getOptions());
trackJavaHomeAccessDetailed = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccessDetailed.getValue(bb.getOptions());
trackMetadataRequiringMethodUsage = AnalyzeMethodsRequiringMetadataUsageSupport.Options.TrackMethodsRequiringMetadata.hasBeenSet();
trackJavaHomeAccess = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccess.getValue();
trackJavaHomeAccessDetailed = AnalyzeJavaHomeAccessFeature.Options.TrackJavaHomeAccessDetailed.getValue();
}

@Override
protected void preStrengthenGraphs(StructuredGraph graph, AnalysisMethod method) {
if (trackMetadataRequiringMethodUsage) {
new AnalyzeMethodsRequiringMetadataUsagePhase().apply(graph, bb.getProviders(method));
}
}

@Override
Expand Down
Loading

0 comments on commit 3eaf0db

Please sign in to comment.