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