Skip to content

Commit 1c3e155

Browse files
committed
Add merge support & update RecordMetadata API
1 parent 3bc9532 commit 1c3e155

File tree

1 file changed

+136
-24
lines changed

1 file changed

+136
-24
lines changed

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java

Lines changed: 136 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
import java.nio.file.Files;
2929
import java.nio.file.Path;
3030
import java.nio.file.Paths;
31+
import java.util.HashMap;
32+
import java.util.LinkedHashSet;
3133
import java.util.List;
34+
import java.util.Map;
35+
import java.util.Set;
3236

3337
import org.graalvm.nativeimage.ImageSingletons;
3438
import org.graalvm.nativeimage.hosted.Feature;
@@ -37,8 +41,10 @@
3741
import com.oracle.svm.configure.NamedConfigurationTypeDescriptor;
3842
import com.oracle.svm.configure.ProxyConfigurationTypeDescriptor;
3943
import com.oracle.svm.configure.UnresolvedConfigurationCondition;
44+
import com.oracle.svm.configure.config.ConfigurationFileCollection;
4045
import com.oracle.svm.configure.config.ConfigurationSet;
4146
import com.oracle.svm.configure.config.ConfigurationType;
47+
import com.oracle.svm.core.SubstrateUtil;
4248
import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature;
4349
import com.oracle.svm.core.feature.InternalFeature;
4450
import com.oracle.svm.core.jdk.RuntimeSupport;
@@ -51,38 +57,51 @@
5157

5258
import jdk.graal.compiler.api.replacements.Fold;
5359
import jdk.graal.compiler.options.Option;
60+
import jdk.graal.compiler.options.OptionStability;
5461

5562
/**
5663
* Implements reachability metadata tracing during native image execution. Enabling
5764
* {@link Options#MetadataTracingSupport} at build time will generate code to trace all accesses of
58-
* reachability metadata. When {@link Options#RecordMetadata} is specified at run time, the image
59-
* will trace and emit metadata to the specified path.
65+
* reachability metadata, and then the run-time option {@link Options#RecordMetadata} enables
66+
* tracing.
6067
*/
6168
public final class MetadataTracer {
6269

6370
public static class Options {
64-
@Option(help = "Enables the run-time code to trace reachability metadata accesses in the produced native image by using -XX:RecordMetadata=<path>.")//
71+
@Option(help = "Generate an image that supports reachability metadata access tracing. " +
72+
"When tracing is supported, use the -XX:RecordMetadata option to enable tracing at run time.")//
6573
public static final HostedOptionKey<Boolean> MetadataTracingSupport = new HostedOptionKey<>(false);
6674

67-
@Option(help = "The path of the directory to write traced metadata to. Metadata tracing is enabled only when this option is provided.")//
68-
public static final RuntimeOptionKey<String> RecordMetadata = new RuntimeOptionKey<>("");
75+
static final String RECORD_METADATA_HELP = """
76+
Enables metadata tracing at run time. This option is only supported if -H:+MetadataTracingSupport is set when building the image.
77+
The value of this option is a comma-separated list of arguments specified as key-value pairs. The following arguments are supported:
78+
79+
- path=<trace-output-directory> (required): Specifies the directory to write traced metadata to.
80+
- merge=<boolean> (optional): Specifies whether to merge or overwrite metadata with existing files at the output path (default: true).
81+
82+
Example usage:
83+
-H:RecordMetadata=path=trace_output_directory
84+
-H:RecordMetadata=path=trace_output_directory,merge=false
85+
""";
86+
87+
@Option(help = RECORD_METADATA_HELP, stability = OptionStability.EXPERIMENTAL)//
88+
public static final RuntimeOptionKey<String> RecordMetadata = new RuntimeOptionKey<>(null);
6989
}
7090

91+
private RecordOptions options;
7192
private volatile ConfigurationSet config;
7293

73-
private Path recordMetadataPath;
74-
7594
@Fold
7695
public static MetadataTracer singleton() {
7796
return ImageSingletons.lookup(MetadataTracer.class);
7897
}
7998

8099
/**
81-
* Returns whether tracing is enabled at run time (using {@code -XX:RecordMetadata=path}).
100+
* Returns whether tracing is enabled at run time (using {@code -XX:RecordMetadata}).
82101
*/
83102
public boolean enabled() {
84103
VMError.guarantee(Options.MetadataTracingSupport.getValue());
85-
return recordMetadataPath != null;
104+
return options != null;
86105
}
87106

88107
/**
@@ -159,21 +178,35 @@ public void traceSerializationType(String className) {
159178
}
160179
}
161180

162-
private static void initialize() {
181+
private static void initialize(String recordMetadataValue) {
163182
assert Options.MetadataTracingSupport.getValue();
164-
MetadataTracer singleton = MetadataTracer.singleton();
165-
String recordMetadataValue = Options.RecordMetadata.getValue();
166-
if (recordMetadataValue.isEmpty()) {
167-
throw new IllegalArgumentException("Empty path provided for " + Options.RecordMetadata.getName() + ".");
168-
}
169-
Path recordMetadataPath = Paths.get(recordMetadataValue);
183+
184+
RecordOptions parsedOptions = RecordOptions.parse(recordMetadataValue);
170185
try {
171-
Files.createDirectories(recordMetadataPath);
186+
Files.createDirectories(parsedOptions.path());
172187
} catch (IOException ex) {
173-
throw new IllegalArgumentException("Exception occurred creating the output directory for tracing (" + recordMetadataPath + ")", ex);
188+
throw new IllegalArgumentException("Exception occurred creating the output directory for tracing (" + parsedOptions.path() + ")", ex);
174189
}
175-
singleton.recordMetadataPath = recordMetadataPath;
176-
singleton.config = new ConfigurationSet();
190+
191+
MetadataTracer singleton = MetadataTracer.singleton();
192+
singleton.options = parsedOptions;
193+
singleton.config = initializeConfigurationSet(parsedOptions);
194+
}
195+
196+
private static ConfigurationSet initializeConfigurationSet(RecordOptions options) {
197+
if (options.merge() && Files.exists(options.path())) {
198+
ConfigurationFileCollection mergeConfigs = new ConfigurationFileCollection();
199+
mergeConfigs.addDirectory(options.path());
200+
try {
201+
return mergeConfigs.loadConfigurationSet(ioexception -> ioexception, null, null);
202+
} catch (Exception ex) {
203+
// suppress and fall back on empty configuration set.
204+
Log.log().string("An exception occurred when loading merge metadata from path " + options.path() + ". ")
205+
.string("Any existing metadata may be overwritten.").newline()
206+
.string("Exception: ").exception(ex).newline();
207+
}
208+
}
209+
return new ConfigurationSet();
177210
}
178211

179212
private static void shutdown() {
@@ -183,10 +216,10 @@ private static void shutdown() {
183216
singleton.config = null; // clear config so that shutdown events are not traced.
184217
if (config != null) {
185218
try {
186-
config.writeConfiguration(configFile -> singleton.recordMetadataPath.resolve(configFile.getFileName()));
219+
config.writeConfiguration(configFile -> singleton.options.path().resolve(configFile.getFileName()));
187220
} catch (IOException ex) {
188221
Log log = Log.log();
189-
log.string("Failed to write out reachability metadata to directory ").string(singleton.recordMetadataPath.toString());
222+
log.string("Failed to write out reachability metadata to directory ").string(singleton.options.path().toString());
190223
log.string(":").string(ex.getMessage());
191224
log.newline();
192225
}
@@ -200,7 +233,7 @@ static RuntimeSupport.Hook initializeMetadataTracingHook() {
200233
}
201234
VMError.guarantee(Options.MetadataTracingSupport.getValue());
202235
if (Options.RecordMetadata.hasBeenSet()) {
203-
initialize();
236+
initialize(Options.RecordMetadata.getValue());
204237
}
205238
};
206239
}
@@ -230,12 +263,91 @@ static RuntimeSupport.Hook checkImproperOptionUsageHook() {
230263
throw new IllegalArgumentException(
231264
"The option " + Options.RecordMetadata.getName() + " can only be used if metadata tracing is enabled at build time (using " +
232265
hostedOptionCommandArgument + ").");
233-
234266
}
235267
};
236268
}
237269
}
238270

271+
record RecordOptions(Path path, boolean merge) {
272+
273+
private static final int ARGUMENT_PARTS = 2;
274+
275+
static RecordOptions parse(String recordMetadataValue) {
276+
if (recordMetadataValue.isEmpty()) {
277+
throw printHelp("Option " + MetadataTracer.Options.RecordMetadata.getName() + " cannot be empty.");
278+
} else if (recordMetadataValue.equals("help")) {
279+
throw printHelp("Option " + MetadataTracer.Options.RecordMetadata.getName() + " value is 'help'. Printing a description and aborting.");
280+
}
281+
282+
Map<String, String> parsedArguments = new HashMap<>();
283+
Set<String> allArguments = new LinkedHashSet<>(List.of("path", "merge"));
284+
for (String argument : recordMetadataValue.split(",")) {
285+
String[] parts = SubstrateUtil.split(argument, "=", ARGUMENT_PARTS);
286+
if (parts.length != ARGUMENT_PARTS) {
287+
throw badArgumentError(argument, "Argument should be a key-value pair separated by '='");
288+
} else if (!allArguments.contains(parts[0])) {
289+
throw badArgumentError(argument, "Argument key should be one of " + allArguments);
290+
} else if (parsedArguments.containsKey(parts[0])) {
291+
throw badArgumentError(argument, "Argument '" + parts[0] + "' was already specified with value '" + parsedArguments.get(parts[0]) + "'");
292+
} else if (parts[1].isEmpty()) {
293+
throw badArgumentError(argument, "Value cannot be empty");
294+
}
295+
parsedArguments.put(parts[0], parts[1]);
296+
}
297+
298+
String path = requiredArgument(parsedArguments, "path", IDENTITY_PARSER);
299+
boolean merge = optionalArgument(parsedArguments, "merge", true, BOOLEAN_PARSER);
300+
return new RecordOptions(Paths.get(path), merge);
301+
}
302+
303+
private static IllegalArgumentException printHelp(String errorMessage) {
304+
throw new IllegalArgumentException("""
305+
%s
306+
307+
%s description:
308+
309+
%s
310+
""".formatted(errorMessage, MetadataTracer.Options.RecordMetadata.getName(), MetadataTracer.Options.RECORD_METADATA_HELP));
311+
}
312+
313+
private static IllegalArgumentException parseError(String message) {
314+
return new IllegalArgumentException(message + ". For more information (including usage examples), pass 'help' as an argument to " + MetadataTracer.Options.RecordMetadata.getName() + ".");
315+
}
316+
317+
private static IllegalArgumentException badArgumentError(String argument, String message) {
318+
throw parseError("Bad argument provided for " + MetadataTracer.Options.RecordMetadata.getName() + ": '" + argument + "'. " + message);
319+
}
320+
321+
private static IllegalArgumentException badArgumentValueError(String argumentKey, String argumentValue, String message) {
322+
throw badArgumentError(argumentKey + "=" + argumentValue, message);
323+
}
324+
325+
private interface ArgumentParser<T> {
326+
T parse(String argumentKey, String argumentValue);
327+
}
328+
329+
private static final ArgumentParser<String> IDENTITY_PARSER = ((argumentKey, argumentValue) -> argumentValue);
330+
private static final ArgumentParser<Boolean> BOOLEAN_PARSER = ((argumentKey, argumentValue) -> switch (argumentValue) {
331+
case "true" -> true;
332+
case "false" -> false;
333+
default -> throw badArgumentValueError(argumentKey, argumentValue, "Value must be a literal 'true' or 'false'");
334+
});
335+
336+
private static <T> T requiredArgument(Map<String, String> arguments, String key, ArgumentParser<T> parser) {
337+
if (arguments.containsKey(key)) {
338+
return parser.parse(key, arguments.get(key));
339+
}
340+
throw parseError(MetadataTracer.Options.RecordMetadata.getName() + " missing required argument '" + key + "'");
341+
}
342+
343+
private static <T> T optionalArgument(Map<String, String> options, String key, T defaultValue, ArgumentParser<T> parser) {
344+
if (options.containsKey(key)) {
345+
return parser.parse(key, options.get(key));
346+
}
347+
return defaultValue;
348+
}
349+
}
350+
239351
@AutomaticallyRegisteredFeature
240352
class MetadataTracerFeature implements InternalFeature {
241353
@Override

0 commit comments

Comments
 (0)