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 .Locale ;
35+ import java .util .Map ;
36+ import java .util .Set ;
3237
3338import org .graalvm .nativeimage .ImageSingletons ;
3439import org .graalvm .nativeimage .hosted .Feature ;
3944import com .oracle .svm .configure .UnresolvedConfigurationCondition ;
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 ;
5561/**
5662 * Implements reachability metadata tracing during native image execution. Enabling
5763 * {@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 .
64+ * reachability metadata, and then the run-time option {@link Options#RecordMetadata} enables
65+ * tracing .
6066 */
6167public final class MetadataTracer {
6268
6369 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>." )//
70+ @ Option (help = "Generate an image that supports reachability metadata access tracing. " +
71+ "When tracing is supported, use the -XX:RecordMetadata option to enable tracing at run time." )//
6572 public static final HostedOptionKey <Boolean > MetadataTracingSupport = new HostedOptionKey <>(false );
6673
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 <>("" );
74+ @ Option (help = "file:doc-files/RecordMetadataHelp.txt " )//
75+ public static final RuntimeOptionKey <String > RecordMetadata = new RuntimeOptionKey <>(null );
6976 }
7077
78+ private RecordOptions options ;
7179 private volatile ConfigurationSet config ;
7280
73- private Path recordMetadataPath ;
74-
7581 @ Fold
7682 public static MetadataTracer singleton () {
7783 return ImageSingletons .lookup (MetadataTracer .class );
7884 }
7985
8086 /**
81- * Returns whether tracing is enabled at run time (using {@code -XX:RecordMetadata=path }).
87+ * Returns whether tracing is enabled at run time (using {@code -XX:RecordMetadata}).
8288 */
8389 public boolean enabled () {
8490 VMError .guarantee (Options .MetadataTracingSupport .getValue ());
85- return recordMetadataPath != null ;
91+ return options != null ;
8692 }
8793
8894 /**
@@ -159,20 +165,21 @@ public void traceSerializationType(String className) {
159165 }
160166 }
161167
162- private static void initialize () {
168+ private static void initialize (String recordMetadataValue ) {
163169 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 );
170+
171+ RecordOptions parsedOptions = RecordOptions .parse (recordMetadataValue );
170172 try {
171- Files .createDirectories (recordMetadataPath );
173+ Files .createDirectories (parsedOptions . path () );
172174 } catch (IOException ex ) {
173- throw new IllegalArgumentException ("Exception occurred creating the output directory for tracing (" + recordMetadataPath + ")" , ex );
175+ throw new IllegalArgumentException ("Exception occurred creating the output directory for tracing (" + parsedOptions . path () + ")" , ex );
174176 }
175- singleton .recordMetadataPath = recordMetadataPath ;
177+ if (parsedOptions .mode () != RecordMode .DEFAULT ) {
178+ throw new IllegalArgumentException ("Mode " + parsedOptions .mode () + " is not yet supported." );
179+ }
180+
181+ MetadataTracer singleton = MetadataTracer .singleton ();
182+ singleton .options = parsedOptions ;
176183 singleton .config = new ConfigurationSet ();
177184 }
178185
@@ -183,10 +190,10 @@ private static void shutdown() {
183190 singleton .config = null ; // clear config so that shutdown events are not traced.
184191 if (config != null ) {
185192 try {
186- config .writeConfiguration (configFile -> singleton .recordMetadataPath .resolve (configFile .getFileName ()));
193+ config .writeConfiguration (configFile -> singleton .options . path () .resolve (configFile .getFileName ()));
187194 } catch (IOException ex ) {
188195 Log log = Log .log ();
189- log .string ("Failed to write out reachability metadata to directory " ).string (singleton .recordMetadataPath .toString ());
196+ log .string ("Failed to write out reachability metadata to directory " ).string (singleton .options . path () .toString ());
190197 log .string (":" ).string (ex .getMessage ());
191198 log .newline ();
192199 }
@@ -200,7 +207,7 @@ static RuntimeSupport.Hook initializeMetadataTracingHook() {
200207 }
201208 VMError .guarantee (Options .MetadataTracingSupport .getValue ());
202209 if (Options .RecordMetadata .hasBeenSet ()) {
203- initialize ();
210+ initialize (Options . RecordMetadata . getValue () );
204211 }
205212 };
206213 }
@@ -230,12 +237,86 @@ static RuntimeSupport.Hook checkImproperOptionUsageHook() {
230237 throw new IllegalArgumentException (
231238 "The option " + Options .RecordMetadata .getName () + " can only be used if metadata tracing is enabled at build time (using " +
232239 hostedOptionCommandArgument + ")." );
233-
234240 }
235241 };
236242 }
237243}
238244
245+ enum RecordMode {
246+ DEFAULT ,
247+ CONDITIONAL
248+ }
249+
250+ record RecordOptions (Path path , RecordMode mode ) {
251+
252+ static RecordOptions parse (String recordMetadataValue ) {
253+ if (recordMetadataValue .isEmpty ()) {
254+ throw parseError ("Option " + MetadataTracer .Options .RecordMetadata .getName () + " cannot be empty" );
255+ }
256+
257+ Map <String , String > parsedArguments = new HashMap <>();
258+ Set <String > allArguments = new LinkedHashSet <>(List .of ("path" , "mode" ));
259+ for (String argument : recordMetadataValue .split ("," )) {
260+ String [] parts = SubstrateUtil .split (argument , "=" , 2 );
261+ if (parts .length != 2 ) {
262+ throw badArgumentError (argument , "Argument should be a key-value pair separated by '='" );
263+ } else if (!allArguments .contains (parts [0 ])) {
264+ throw badArgumentError (argument , "Argument key should be one of " + allArguments );
265+ } else if (parsedArguments .containsKey (parts [0 ])) {
266+ throw badArgumentError (argument , "Argument '" + parts [0 ] + "' was already specified with value '" + parsedArguments .get (parts [0 ]) + "'" );
267+ } else if (parts [1 ].isEmpty ()) {
268+ throw badArgumentError (argument , "Argument '" + parts [0 ] + "' cannot be empty" );
269+ }
270+ parsedArguments .put (parts [0 ], parts [1 ]);
271+ }
272+
273+ String path = requiredArgument (parsedArguments , "path" );
274+ String mode = optionalArgument (parsedArguments , "mode" , "default" );
275+ return new RecordOptions (Paths .get (path ), parseMode (mode ));
276+ }
277+
278+ private static IllegalArgumentException parseError (String message ) {
279+ return new IllegalArgumentException (message + ". Sample usage: -XX:" + MetadataTracer .Options .RecordMetadata .getName () + "=path=<trace-output-directory>[,mode=<mode>]" );
280+ }
281+
282+ private static IllegalArgumentException badArgumentError (String option , String message ) {
283+ throw parseError ("Bad argument provided for " + MetadataTracer .Options .RecordMetadata .getName () + ": '" + option + "'. " + message );
284+ }
285+
286+ private static String requiredArgument (Map <String , String > arguments , String key ) {
287+ if (arguments .containsKey (key )) {
288+ return arguments .get (key );
289+ }
290+ throw parseError (MetadataTracer .Options .RecordMetadata .getName () + " missing required argument '" + key + "'" );
291+ }
292+
293+ private static String optionalArgument (Map <String , String > options , String key , String defaultValue ) {
294+ if (options .containsKey (key )) {
295+ return options .get (key );
296+ }
297+ return defaultValue ;
298+ }
299+
300+ private static RecordMode parseMode (String modeValue ) {
301+ try {
302+ return RecordMode .valueOf (modeValue .toUpperCase (Locale .ROOT ));
303+ } catch (IllegalArgumentException ex ) {
304+ StringBuilder message = new StringBuilder ("Mode should be one of [" );
305+ boolean first = true ;
306+ for (RecordMode mode : RecordMode .values ()) {
307+ if (first ) {
308+ first = false ;
309+ } else {
310+ message .append (", " );
311+ }
312+ message .append (mode .toString ().toLowerCase (Locale .ROOT ));
313+ }
314+ message .append ("]" );
315+ throw badArgumentError ("mode=" + modeValue , message .toString ());
316+ }
317+ }
318+ }
319+
239320@ AutomaticallyRegisteredFeature
240321class MetadataTracerFeature implements InternalFeature {
241322 @ Override
0 commit comments