1 /*
   2  * Copyright (c) 2015, 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.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package optionsvalidation;
  25 
  26 import java.io.BufferedReader;
  27 import java.io.IOException;
  28 import java.io.InputStreamReader;
  29 import java.io.Reader;
  30 import java.util.ArrayList;
  31 import java.util.Arrays;
  32 import java.util.List;
  33 import java.util.LinkedHashMap;
  34 import java.util.Map;
  35 import java.util.StringTokenizer;
  36 import java.util.function.Predicate;
  37 import jdk.test.lib.OutputAnalyzer;
  38 import jdk.test.lib.Platform;
  39 import jdk.test.lib.ProcessTools;
  40 
  41 public class JVMOptionsUtils {
  42 
  43     /* Java option which print options with ranges */
  44     private static final String PRINT_FLAGS_RANGES = "-XX:+PrintFlagsRanges";
  45 
  46     /* StringBuilder to accumulate failed message */
  47     private static final StringBuilder finalFailedMessage = new StringBuilder();
  48 
  49     /* Used to start the JVM with the same type as current */
  50     static String VMType;
  51 
  52     static {
  53         if (Platform.isServer()) {
  54             VMType = "-server";
  55         } else if (Platform.isClient()) {
  56             VMType = "-client";
  57         } else if (Platform.isMinimal()) {
  58             VMType = "-minimal";
  59         } else if (Platform.isGraal()) {
  60             VMType = "-graal";
  61         } else {
  62             VMType = null;
  63         }
  64     }
  65 
  66     /**
  67      * Add dependency for option depending on it's name. E.g. enable G1 GC for
  68      * G1 options or add prepend options to not hit constraints.
  69      *
  70      * @param option option
  71      */
  72     private static void addNameDependency(JVMOption option) {
  73         String name = option.getName();
  74 
  75         if (name.startsWith("G1")) {
  76             option.addPrepend("-XX:+UseG1GC");
  77         }
  78 
  79         if (name.startsWith("CMS")) {
  80             option.addPrepend("-XX:+UseConcMarkSweepGC");
  81         }
  82 
  83         switch (name) {
  84             case "MinHeapFreeRatio":
  85                 option.addPrepend("-XX:MaxHeapFreeRatio=100");
  86                 break;
  87             case "MaxHeapFreeRatio":
  88                 option.addPrepend("-XX:MinHeapFreeRatio=0");
  89                 break;
  90             case "MinMetaspaceFreeRatio":
  91                 option.addPrepend("-XX:MaxMetaspaceFreeRatio=100");
  92                 break;
  93             case "MaxMetaspaceFreeRatio":
  94                 option.addPrepend("-XX:MinMetaspaceFreeRatio=0");
  95                 break;
  96             case "CMSOldPLABMin":
  97                 option.addPrepend("-XX:CMSOldPLABMax=" + option.getMax());
  98                 break;
  99             case "CMSOldPLABMax":
 100                 option.addPrepend("-XX:CMSOldPLABMin=" + option.getMin());
 101                 break;
 102             case "CMSPrecleanNumerator":
 103                 option.addPrepend("-XX:CMSPrecleanDenominator=" + option.getMax());
 104                 break;
 105             case "CMSPrecleanDenominator":
 106                 option.addPrepend("-XX:CMSPrecleanNumerator=" + ((new Integer(option.getMin())) - 1));
 107                 break;
 108             case "InitialTenuringThreshold":
 109                 option.addPrepend("-XX:MaxTenuringThreshold=" + option.getMax());
 110                 break;
 111             default:
 112                 /* Do nothing */
 113                 break;
 114         }
 115 
 116     }
 117 
 118     /**
 119      * Add dependency for option depending on it's type. E.g. run the JVM in
 120      * compilation mode for compiler options.
 121      *
 122      * @param option option
 123      * @param type type of the option
 124      */
 125     private static void addTypeDependency(JVMOption option, String type) {
 126         if (type.contains("C1") || type.contains("C2")) {
 127             /* Run in compiler mode for compiler flags */
 128             option.addPrepend("-Xcomp");
 129         }
 130     }
 131 
 132     /**
 133      * Parse JVM Options. Get input from "inputReader". Parse using
 134      * "-XX:+PrintFlagsRanges" output format.
 135      *
 136      * @param inputReader input data for parsing
 137      * @param withRanges true if needed options with defined ranges inside JVM
 138      * @param acceptOrigin predicate for option origins. Origins can be
 139      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 140      * to true.
 141      * @return map from option name to the JVMOption object
 142      * @throws IOException if an error occurred while reading the data
 143      */
 144     private static Map<String, JVMOption> getJVMOptions(Reader inputReader,
 145             boolean withRanges, Predicate<String> acceptOrigin) throws IOException {
 146         BufferedReader reader = new BufferedReader(inputReader);
 147         String type;
 148         String line;
 149         String token;
 150         String name;
 151         StringTokenizer st;
 152         JVMOption option;
 153         Map<String, JVMOption> allOptions = new LinkedHashMap<>();
 154 
 155         // Skip first line
 156         line = reader.readLine();
 157 
 158         while ((line = reader.readLine()) != null) {
 159             /*
 160              * Parse option from following line:
 161              * <type> <name> [ <min, optional> ... <max, optional> ] {<origin>}
 162              */
 163             st = new StringTokenizer(line);
 164 
 165             type = st.nextToken();
 166 
 167             name = st.nextToken();
 168 
 169             option = JVMOption.createVMOption(type, name);
 170 
 171             /* Skip '[' */
 172             token = st.nextToken();
 173 
 174             /* Read min range or "..." if range is absent */
 175             token = st.nextToken();
 176 
 177             if (token.equals("...") == false) {
 178                 if (!withRanges) {
 179                     /*
 180                      * Option have range, but asked for options without
 181                      * ranges => skip it
 182                      */
 183                     continue;
 184                 }
 185 
 186                 /* Mark this option as option which range is defined in VM */
 187                 option.optionWithRange();
 188 
 189                 option.setMin(token);
 190 
 191                 /* Read "..." and skip it */
 192                 token = st.nextToken();
 193 
 194                 /* Get max value */
 195                 token = st.nextToken();
 196                 option.setMax(token);
 197             } else if (withRanges) {
 198                 /*
 199                  * Option not have range, but asked for options with
 200                  * ranges => skip it
 201                  */
 202                 continue;
 203             }
 204 
 205             /* Skip ']' */
 206             token = st.nextToken();
 207 
 208             /* Read origin of the option */
 209             token = st.nextToken();
 210 
 211             while (st.hasMoreTokens()) {
 212                 token += st.nextToken();
 213             };
 214             token = token.substring(1, token.indexOf("}"));
 215 
 216             if (acceptOrigin.test(token)) {
 217                 addTypeDependency(option, token);
 218                 addNameDependency(option);
 219 
 220                 allOptions.put(name, option);
 221             }
 222         }
 223 
 224         return allOptions;
 225     }
 226 
 227     static void failedMessage(String optionName, String value, boolean valid, String message) {
 228         String temp;
 229 
 230         if (valid) {
 231             temp = "valid";
 232         } else {
 233             temp = "invalid";
 234         }
 235 
 236         failedMessage(String.format("Error processing option %s with %s value '%s'! %s",
 237                 optionName, temp, value, message));
 238     }
 239 
 240     static void failedMessage(String message) {
 241         System.err.println("TEST FAILED: " + message);
 242         finalFailedMessage.append(String.format("(%s)%n", message));
 243     }
 244 
 245     static void printOutputContent(OutputAnalyzer output) {
 246         System.err.println(String.format("stdout content[%s]", output.getStdout()));
 247         System.err.println(String.format("stderr content[%s]%n", output.getStderr()));
 248     }
 249 
 250     /**
 251      * Return string with accumulated failure messages
 252      *
 253      * @return string with accumulated failure messages
 254      */
 255     public static String getMessageWithFailures() {
 256         return finalFailedMessage.toString();
 257     }
 258 
 259     /**
 260      * Run command line tests for options passed in the list
 261      *
 262      * @param options list of options to test
 263      * @return number of failed tests
 264      * @throws Exception if java process can not be started
 265      */
 266     public static int runCommandLineTests(List<? extends JVMOption> options) throws Exception {
 267         int failed = 0;
 268 
 269         for (JVMOption option : options) {
 270             failed += option.testCommandLine();
 271         }
 272 
 273         return failed;
 274     }
 275 
 276     /**
 277      * Test passed options using DynamicVMOption isValidValue and isInvalidValue
 278      * methods. Only tests writeable options.
 279      *
 280      * @param options list of options to test
 281      * @return number of failed tests
 282      */
 283     public static int runDynamicTests(List<? extends JVMOption> options) {
 284         int failed = 0;
 285 
 286         for (JVMOption option : options) {
 287             failed += option.testDynamic();
 288         }
 289 
 290         return failed;
 291     }
 292 
 293     /**
 294      * Test passed options using Jcmd. Only tests writeable options.
 295      *
 296      * @param options list of options to test
 297      * @return number of failed tests
 298      */
 299     public static int runJcmdTests(List<? extends JVMOption> options) {
 300         int failed = 0;
 301 
 302         for (JVMOption option : options) {
 303             failed += option.testJcmd();
 304         }
 305 
 306         return failed;
 307     }
 308 
 309     /**
 310      * Test passed option using attach method. Only tests writeable options.
 311      *
 312      * @param options list of options to test
 313      * @return number of failed tests
 314      * @throws Exception if an error occurred while attaching to the target JVM
 315      */
 316     public static int runAttachTests(List<? extends JVMOption> options) throws Exception {
 317         int failed = 0;
 318 
 319         for (JVMOption option : options) {
 320             failed += option.testAttach();
 321         }
 322 
 323         return failed;
 324     }
 325 
 326     /**
 327      * Get JVM options as map. Can return options with defined ranges or options
 328      * without range depending on "withRanges" argument. "acceptOrigin"
 329      * predicate can be used to filter option origin.
 330      *
 331      * @param withRanges true if needed options with defined ranges inside JVM
 332      * @param acceptOrigin predicate for option origins. Origins can be
 333      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 334      * to true.
 335      * @param additionalArgs additional arguments to the Java process which ran
 336      * with "-XX:+PrintFlagsRanges"
 337      * @return map from option name to the JVMOption object
 338      * @throws Exception if a new process can not be created or an error
 339      * occurred while reading the data
 340      */
 341     public static Map<String, JVMOption> getOptionsAsMap(boolean withRanges, Predicate<String> acceptOrigin,
 342             String... additionalArgs) throws Exception {
 343         Map<String, JVMOption> result;
 344         Process p;
 345         List<String> runJava = new ArrayList<>();
 346 
 347         if (additionalArgs.length > 0) {
 348             runJava.addAll(Arrays.asList(additionalArgs));
 349         }
 350 
 351         if (VMType != null) {
 352             runJava.add(VMType);
 353         }
 354         runJava.add(PRINT_FLAGS_RANGES);
 355         runJava.add("-version");
 356 
 357         p = ProcessTools.createJavaProcessBuilder(runJava.toArray(new String[0])).start();
 358 
 359         result = getJVMOptions(new InputStreamReader(p.getInputStream()), withRanges, acceptOrigin);
 360 
 361         p.waitFor();
 362 
 363         return result;
 364     }
 365 
 366     /**
 367      * Get JVM options as list. Can return options with defined ranges or
 368      * options without range depending on "withRanges" argument. "acceptOrigin"
 369      * predicate can be used to filter option origin.
 370      *
 371      * @param withRanges true if needed options with defined ranges inside JVM
 372      * @param acceptOrigin predicate for option origins. Origins can be
 373      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 374      * to true.
 375      * @param additionalArgs additional arguments to the Java process which ran
 376      * with "-XX:+PrintFlagsRanges"
 377      * @return List of options
 378      * @throws Exception if a new process can not be created or an error
 379      * occurred while reading the data
 380      */
 381     public static List<JVMOption> getOptions(boolean withRanges, Predicate<String> acceptOrigin,
 382             String... additionalArgs) throws Exception {
 383         return new ArrayList<>(getOptionsAsMap(withRanges, acceptOrigin, additionalArgs).values());
 384     }
 385 
 386     /**
 387      * Get JVM options with ranges as list. "acceptOrigin" predicate can be used
 388      * to filter option origin.
 389      *
 390      * @param acceptOrigin predicate for option origins. Origins can be
 391      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 392      * to true.
 393      * @param additionalArgs additional arguments to the Java process which ran
 394      * with "-XX:+PrintFlagsRanges"
 395      * @return List of options
 396      * @throws Exception if a new process can not be created or an error
 397      * occurred while reading the data
 398      */
 399     public static List<JVMOption> getOptionsWithRange(Predicate<String> acceptOrigin, String... additionalArgs) throws Exception {
 400         return getOptions(true, acceptOrigin, additionalArgs);
 401     }
 402 
 403     /**
 404      * Get JVM options with ranges as list.
 405      *
 406      * @param additionalArgs additional arguments to the Java process which ran
 407      * with "-XX:+PrintFlagsRanges"
 408      * @return list of options
 409      * @throws Exception if a new process can not be created or an error
 410      * occurred while reading the data
 411      */
 412     public static List<JVMOption> getOptionsWithRange(String... additionalArgs) throws Exception {
 413         return getOptionsWithRange(origin -> true, additionalArgs);
 414     }
 415 
 416     /**
 417      * Get JVM options with range as map. "acceptOrigin" predicate can be used
 418      * to filter option origin.
 419      *
 420      * @param acceptOrigin predicate for option origins. Origins can be
 421      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 422      * to true.
 423      * @param additionalArgs additional arguments to the Java process which ran
 424      * with "-XX:+PrintFlagsRanges"
 425      * @return Map from option name to the JVMOption object
 426      * @throws Exception if a new process can not be created or an error
 427      * occurred while reading the data
 428      */
 429     public static Map<String, JVMOption> getOptionsWithRangeAsMap(Predicate<String> acceptOrigin, String... additionalArgs) throws Exception {
 430         return getOptionsAsMap(true, acceptOrigin, additionalArgs);
 431     }
 432 
 433     /**
 434      * Get JVM options with range as map
 435      *
 436      * @param additionalArgs additional arguments to the Java process which ran
 437      * with "-XX:+PrintFlagsRanges"
 438      * @return map from option name to the JVMOption object
 439      * @throws Exception if a new process can not be created or an error
 440      * occurred while reading the data
 441      */
 442     public static Map<String, JVMOption> getOptionsWithRangeAsMap(String... additionalArgs) throws Exception {
 443         return getOptionsWithRangeAsMap(origin -> true, additionalArgs);
 444     }
 445 
 446     /* Simple method to test that java start-up. Used for testing options. */
 447     public static void main(String[] args) {
 448         System.out.print("Java start-up!");
 449     }
 450 }