2828import java .nio .file .Files ;
2929import java .nio .file .Path ;
3030import java .nio .file .Paths ;
31+ import java .util .HashMap ;
32+ import java .util .LinkedHashSet ;
3133import java .util .List ;
34+ import java .util .Map ;
35+ import java .util .Set ;
3236
3337import org .graalvm .nativeimage .ImageSingletons ;
3438import org .graalvm .nativeimage .hosted .Feature ;
3741import com .oracle .svm .configure .NamedConfigurationTypeDescriptor ;
3842import com .oracle .svm .configure .ProxyConfigurationTypeDescriptor ;
3943import com .oracle .svm .configure .UnresolvedConfigurationCondition ;
44+ import com .oracle .svm .configure .config .ConfigurationFileCollection ;
4045import com .oracle .svm .configure .config .ConfigurationSet ;
4146import com .oracle .svm .configure .config .ConfigurationType ;
47+ import com .oracle .svm .core .SubstrateUtil ;
4248import com .oracle .svm .core .feature .AutomaticallyRegisteredFeature ;
4349import com .oracle .svm .core .feature .InternalFeature ;
4450import com .oracle .svm .core .jdk .RuntimeSupport ;
5157
5258import jdk .graal .compiler .api .replacements .Fold ;
5359import 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 */
6168public 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
240352class MetadataTracerFeature implements InternalFeature {
241353 @ Override
0 commit comments