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