1 /*
   2  * Copyright (c) 2015, 2017, 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.lang.management.GarbageCollectorMXBean;
  31 import java.lang.management.ManagementFactory;
  32 import java.math.BigDecimal;
  33 import java.util.ArrayList;
  34 import java.util.Arrays;
  35 import java.util.List;
  36 import java.util.LinkedHashMap;
  37 import java.util.Map;
  38 import java.util.StringTokenizer;
  39 import java.util.function.Predicate;
  40 import jdk.test.lib.process.OutputAnalyzer;
  41 import jdk.test.lib.Platform;
  42 import jdk.test.lib.process.ProcessTools;
  43 
  44 public class JVMOptionsUtils {
  45 
  46     /* Java option which print options with ranges */
  47     private static final String PRINT_FLAGS_RANGES = "-XX:+PrintFlagsRanges";
  48 
  49     /* StringBuilder to accumulate failed message */
  50     private static final StringBuilder finalFailedMessage = new StringBuilder();
  51 
  52     /* Used to start the JVM with the same type as current */
  53     static String VMType;
  54 
  55     /* Used to start the JVM with the same GC type as current */
  56     static String GCType;
  57 
  58     private static Map<String, JVMOption> optionsAsMap;
  59 
  60     static {
  61         if (Platform.isServer()) {
  62             VMType = "-server";
  63         } else if (Platform.isClient()) {
  64             VMType = "-client";
  65         } else if (Platform.isMinimal()) {
  66             VMType = "-minimal";
  67         } else if (Platform.isGraal()) {
  68             VMType = "-graal";
  69         } else {
  70             VMType = null;
  71         }
  72 
  73         List<GarbageCollectorMXBean> gcMxBeans = ManagementFactory.getGarbageCollectorMXBeans();
  74 
  75         GCType = null;
  76 
  77         for (GarbageCollectorMXBean gcMxBean : gcMxBeans) {
  78             switch (gcMxBean.getName()) {
  79                 case "ConcurrentMarkSweep":
  80                     GCType = "-XX:+UseConcMarkSweepGC";
  81                     break;
  82                 case "MarkSweepCompact":
  83                     GCType = "-XX:+UseSerialGC";
  84                     break;
  85                 case "PS Scavenge":
  86                     GCType = "-XX:+UseParallelGC";
  87                     break;
  88                 case "G1 Old Generation":
  89                     GCType = "-XX:+UseG1GC";
  90                     break;
  91             }
  92         }
  93     }
  94 
  95     public static boolean fitsRange(String optionName, BigDecimal number) throws Exception {
  96         JVMOption option;
  97         String minRangeString = null;
  98         String maxRangeString = null;
  99         boolean fits = true;
 100 
 101         if (optionsAsMap == null) {
 102             optionsAsMap = getOptionsWithRangeAsMap();
 103         }
 104 
 105         option = optionsAsMap.get(optionName);
 106         if (option != null) {
 107             minRangeString = option.getMin();
 108             if (minRangeString != null) {
 109                 fits = (number.compareTo(new BigDecimal(minRangeString)) >= 0);
 110             }
 111             maxRangeString = option.getMax();
 112             if (maxRangeString != null) {
 113                 fits &= (number.compareTo(new BigDecimal(maxRangeString)) <= 0);
 114             }
 115         }
 116 
 117         return fits;
 118     }
 119 
 120     public static boolean fitsRange(String optionName, String number) throws Exception {
 121         String lowerCase = number.toLowerCase();
 122         String multiplier = "1";
 123         if (lowerCase.endsWith("k")) {
 124             multiplier = "1024";
 125             lowerCase = lowerCase.substring(0, lowerCase.length()-1);
 126         } else if (lowerCase.endsWith("m")) {
 127             multiplier = "1048576"; //1024*1024
 128             lowerCase = lowerCase.substring(0, lowerCase.length()-1);
 129         } else if (lowerCase.endsWith("g")) {
 130             multiplier = "1073741824"; //1024*1024*1024
 131             lowerCase = lowerCase.substring(0, lowerCase.length()-1);
 132         } else if (lowerCase.endsWith("t")) {
 133             multiplier = "1099511627776"; //1024*1024*1024*1024
 134             lowerCase = lowerCase.substring(0, lowerCase.length()-1);
 135         }
 136         BigDecimal valueBig = new BigDecimal(lowerCase);
 137         BigDecimal multiplierBig = new BigDecimal(multiplier);
 138         return fitsRange(optionName, valueBig.multiply(multiplierBig));
 139     }
 140 
 141     public static String getMinOptionRange(String optionName) throws Exception {
 142         JVMOption option;
 143         String minRange = null;
 144 
 145         if (optionsAsMap == null) {
 146             optionsAsMap = getOptionsWithRangeAsMap();
 147         }
 148 
 149         option = optionsAsMap.get(optionName);
 150         if (option != null) {
 151             minRange = option.getMin();
 152         }
 153 
 154         return minRange;
 155     }
 156 
 157     public static String getMaxOptionRange(String optionName) throws Exception {
 158         JVMOption option;
 159         String maxRange = null;
 160 
 161         if (optionsAsMap == null) {
 162             optionsAsMap = getOptionsWithRangeAsMap();
 163         }
 164 
 165         option = optionsAsMap.get(optionName);
 166         if (option != null) {
 167             maxRange = option.getMax();
 168         }
 169 
 170         return maxRange;
 171     }
 172 
 173     /**
 174      * Add dependency for option depending on it's name. E.g. enable G1 GC for
 175      * G1 options or add prepend options to not hit constraints.
 176      *
 177      * @param option option
 178      */
 179     private static void addNameDependency(JVMOption option) {
 180         String name = option.getName();
 181 
 182         if (name.startsWith("G1")) {
 183             option.addPrepend("-XX:+UseG1GC");
 184         }
 185 
 186         if (name.startsWith("CMS")) {
 187             option.addPrepend("-XX:+UseConcMarkSweepGC");
 188         }
 189 
 190         if (name.startsWith("NUMA")) {
 191             option.addPrepend("-XX:+UseNUMA");
 192         }
 193 
 194         switch (name) {
 195             case "MinHeapFreeRatio":
 196                 option.addPrepend("-XX:MaxHeapFreeRatio=100");
 197                 break;
 198             case "MaxHeapFreeRatio":
 199                 option.addPrepend("-XX:MinHeapFreeRatio=0");
 200                 break;
 201             case "MinMetaspaceFreeRatio":
 202                 option.addPrepend("-XX:MaxMetaspaceFreeRatio=100");
 203                 break;
 204             case "MaxMetaspaceFreeRatio":
 205                 option.addPrepend("-XX:MinMetaspaceFreeRatio=0");
 206                 break;
 207             case "CMSOldPLABMin":
 208                 option.addPrepend("-XX:CMSOldPLABMax=" + option.getMax());
 209                 break;
 210             case "CMSOldPLABMax":
 211                 option.addPrepend("-XX:CMSOldPLABMin=" + option.getMin());
 212                 break;
 213             case "CMSPrecleanNumerator":
 214                 option.addPrepend("-XX:CMSPrecleanDenominator=" + option.getMax());
 215                 break;
 216             case "CMSPrecleanDenominator":
 217                 option.addPrepend("-XX:CMSPrecleanNumerator=" + ((new Integer(option.getMin())) - 1));
 218                 break;
 219             case "G1RefProcDrainInterval":
 220                 option.addPrepend("-XX:+ExplicitGCInvokesConcurrent");
 221                 break;
 222             case "InitialTenuringThreshold":
 223                 option.addPrepend("-XX:MaxTenuringThreshold=" + option.getMax());
 224                 break;
 225             case "NUMAInterleaveGranularity":
 226                 option.addPrepend("-XX:+UseNUMAInterleaving");
 227                 break;
 228             case "CPUForCMSThread":
 229                 option.addPrepend("-XX:+BindCMSThreadToCPU");
 230                 break;
 231             case "VerifyGCStartAt":
 232                 option.addPrepend("-XX:+VerifyBeforeGC");
 233                 option.addPrepend("-XX:+VerifyAfterGC");
 234                 break;
 235             case "NewSizeThreadIncrease":
 236                 option.addPrepend("-XX:+UseSerialGC");
 237                 break;
 238             case "SharedBaseAddress":
 239             case "SharedSymbolTableBucketSize":
 240                 option.addPrepend("-XX:+UnlockDiagnosticVMOptions");
 241                 option.addPrepend("-XX:SharedArchiveFile=TestOptionsWithRanges.jsa");
 242                 option.addPrepend("-Xshare:dump");
 243                 break;
 244             case "TLABWasteIncrement":
 245                 option.addPrepend("-XX:+UseParallelGC");
 246                 break;
 247             default:
 248                 /* Do nothing */
 249                 break;
 250         }
 251     }
 252 
 253     /**
 254      * Parse JVM Options. Get input from "inputReader". Parse using
 255      * "-XX:+PrintFlagsRanges" output format.
 256      *
 257      * @param inputReader input data for parsing
 258      * @param withRanges true if needed options with defined ranges inside JVM
 259      * @param acceptOrigin predicate for option origins. Origins can be
 260      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 261      * to true.
 262      * @return map from option name to the JVMOption object
 263      * @throws IOException if an error occurred while reading the data
 264      */
 265     private static Map<String, JVMOption> getJVMOptions(Reader inputReader,
 266             boolean withRanges, Predicate<String> acceptOrigin) throws IOException {
 267         BufferedReader reader = new BufferedReader(inputReader);
 268         String type;
 269         String line;
 270         String token;
 271         String name;
 272         StringTokenizer st;
 273         JVMOption option;
 274         Map<String, JVMOption> allOptions = new LinkedHashMap<>();
 275 
 276         // Skip first line
 277         line = reader.readLine();
 278 
 279         while ((line = reader.readLine()) != null) {
 280             /*
 281              * Parse option from following line:
 282              * <type> <name> [ <min, optional> ... <max, optional> ] {<origin>}
 283              */
 284             st = new StringTokenizer(line);
 285 
 286             type = st.nextToken();
 287 
 288             name = st.nextToken();
 289 
 290             option = JVMOption.createVMOption(type, name);
 291 
 292             /* Skip '[' */
 293             token = st.nextToken();
 294 
 295             /* Read min range or "..." if range is absent */
 296             token = st.nextToken();
 297 
 298             if (token.equals("...") == false) {
 299                 if (!withRanges) {
 300                     /*
 301                      * Option have range, but asked for options without
 302                      * ranges => skip it
 303                      */
 304                     continue;
 305                 }
 306 
 307                 /* Mark this option as option which range is defined in VM */
 308                 option.optionWithRange();
 309 
 310                 option.setMin(token);
 311 
 312                 /* Read "..." and skip it */
 313                 token = st.nextToken();
 314 
 315                 /* Get max value */
 316                 token = st.nextToken();
 317                 option.setMax(token);
 318             } else if (withRanges) {
 319                 /*
 320                  * Option not have range, but asked for options with
 321                  * ranges => skip it
 322                  */
 323                 continue;
 324             }
 325 
 326             /* Skip ']' */
 327             token = st.nextToken();
 328 
 329             /* Read origin of the option */
 330             token = st.nextToken();
 331 
 332             while (st.hasMoreTokens()) {
 333                 token += st.nextToken();
 334             };
 335             token = token.substring(1, token.indexOf("}"));
 336 
 337             if (acceptOrigin.test(token)) {
 338                 addNameDependency(option);
 339 
 340                 allOptions.put(name, option);
 341             }
 342         }
 343 
 344         return allOptions;
 345     }
 346 
 347     static void failedMessage(String optionName, String value, boolean valid, String message) {
 348         String temp;
 349 
 350         if (valid) {
 351             temp = "valid";
 352         } else {
 353             temp = "invalid";
 354         }
 355 
 356         failedMessage(String.format("Error processing option %s with %s value '%s'! %s",
 357                 optionName, temp, value, message));
 358     }
 359 
 360     static void failedMessage(String message) {
 361         System.err.println("TEST FAILED: " + message);
 362         finalFailedMessage.append(String.format("(%s)%n", message));
 363     }
 364 
 365     static void printOutputContent(OutputAnalyzer output) {
 366         System.err.println(String.format("stdout content[%s]", output.getStdout()));
 367         System.err.println(String.format("stderr content[%s]%n", output.getStderr()));
 368     }
 369 
 370     /**
 371      * Return string with accumulated failure messages
 372      *
 373      * @return string with accumulated failure messages
 374      */
 375     public static String getMessageWithFailures() {
 376         return finalFailedMessage.toString();
 377     }
 378 
 379     /**
 380      * Run command line tests for options passed in the list
 381      *
 382      * @param options list of options to test
 383      * @return number of failed tests
 384      * @throws Exception if java process can not be started
 385      */
 386     public static int runCommandLineTests(List<? extends JVMOption> options) throws Exception {
 387         int failed = 0;
 388 
 389         for (JVMOption option : options) {
 390             failed += option.testCommandLine();
 391         }
 392 
 393         return failed;
 394     }
 395 
 396     /**
 397      * Test passed options using DynamicVMOption isValidValue and isInvalidValue
 398      * methods. Only tests writeable options.
 399      *
 400      * @param options list of options to test
 401      * @return number of failed tests
 402      */
 403     public static int runDynamicTests(List<? extends JVMOption> options) {
 404         int failed = 0;
 405 
 406         for (JVMOption option : options) {
 407             failed += option.testDynamic();
 408         }
 409 
 410         return failed;
 411     }
 412 
 413     /**
 414      * Test passed options using Jcmd. Only tests writeable options.
 415      *
 416      * @param options list of options to test
 417      * @return number of failed tests
 418      */
 419     public static int runJcmdTests(List<? extends JVMOption> options) {
 420         int failed = 0;
 421 
 422         for (JVMOption option : options) {
 423             failed += option.testJcmd();
 424         }
 425 
 426         return failed;
 427     }
 428 
 429     /**
 430      * Test passed option using attach method. Only tests writeable options.
 431      *
 432      * @param options list of options to test
 433      * @return number of failed tests
 434      * @throws Exception if an error occurred while attaching to the target JVM
 435      */
 436     public static int runAttachTests(List<? extends JVMOption> options) throws Exception {
 437         int failed = 0;
 438 
 439         for (JVMOption option : options) {
 440             failed += option.testAttach();
 441         }
 442 
 443         return failed;
 444     }
 445 
 446     /**
 447      * Get JVM options as map. Can return options with defined ranges or options
 448      * without range depending on "withRanges" argument. "acceptOrigin"
 449      * predicate can be used to filter option origin.
 450      *
 451      * @param withRanges true if needed options with defined ranges inside JVM
 452      * @param acceptOrigin predicate for option origins. Origins can be
 453      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 454      * to true.
 455      * @param additionalArgs additional arguments to the Java process which ran
 456      * with "-XX:+PrintFlagsRanges"
 457      * @return map from option name to the JVMOption object
 458      * @throws Exception if a new process can not be created or an error
 459      * occurred while reading the data
 460      */
 461     public static Map<String, JVMOption> getOptionsAsMap(boolean withRanges, Predicate<String> acceptOrigin,
 462             String... additionalArgs) throws Exception {
 463         Map<String, JVMOption> result;
 464         Process p;
 465         List<String> runJava = new ArrayList<>();
 466 
 467         if (additionalArgs.length > 0) {
 468             runJava.addAll(Arrays.asList(additionalArgs));
 469         }
 470 
 471         if (VMType != null) {
 472             runJava.add(VMType);
 473         }
 474 
 475         if (GCType != null) {
 476             runJava.add(GCType);
 477         }
 478         runJava.add(PRINT_FLAGS_RANGES);
 479         runJava.add("-version");
 480 
 481         p = ProcessTools.createJavaProcessBuilder(runJava.toArray(new String[0])).start();
 482 
 483         result = getJVMOptions(new InputStreamReader(p.getInputStream()), withRanges, acceptOrigin);
 484 
 485         p.waitFor();
 486 
 487         return result;
 488     }
 489 
 490     /**
 491      * Get JVM options as list. Can return options with defined ranges or
 492      * options without range depending on "withRanges" argument. "acceptOrigin"
 493      * predicate can be used to filter option origin.
 494      *
 495      * @param withRanges true if needed options with defined ranges inside JVM
 496      * @param acceptOrigin predicate for option origins. Origins can be
 497      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 498      * to true.
 499      * @param additionalArgs additional arguments to the Java process which ran
 500      * with "-XX:+PrintFlagsRanges"
 501      * @return List of options
 502      * @throws Exception if a new process can not be created or an error
 503      * occurred while reading the data
 504      */
 505     public static List<JVMOption> getOptions(boolean withRanges, Predicate<String> acceptOrigin,
 506             String... additionalArgs) throws Exception {
 507         return new ArrayList<>(getOptionsAsMap(withRanges, acceptOrigin, additionalArgs).values());
 508     }
 509 
 510     /**
 511      * Get JVM options with ranges as list. "acceptOrigin" predicate can be used
 512      * to filter option origin.
 513      *
 514      * @param acceptOrigin predicate for option origins. Origins can be
 515      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 516      * to true.
 517      * @param additionalArgs additional arguments to the Java process which ran
 518      * with "-XX:+PrintFlagsRanges"
 519      * @return List of options
 520      * @throws Exception if a new process can not be created or an error
 521      * occurred while reading the data
 522      */
 523     public static List<JVMOption> getOptionsWithRange(Predicate<String> acceptOrigin, String... additionalArgs) throws Exception {
 524         return getOptions(true, acceptOrigin, additionalArgs);
 525     }
 526 
 527     /**
 528      * Get JVM options with ranges as list.
 529      *
 530      * @param additionalArgs additional arguments to the Java process which ran
 531      * with "-XX:+PrintFlagsRanges"
 532      * @return list of options
 533      * @throws Exception if a new process can not be created or an error
 534      * occurred while reading the data
 535      */
 536     public static List<JVMOption> getOptionsWithRange(String... additionalArgs) throws Exception {
 537         return getOptionsWithRange(origin -> true, additionalArgs);
 538     }
 539 
 540     /**
 541      * Get JVM options with range as map. "acceptOrigin" predicate can be used
 542      * to filter option origin.
 543      *
 544      * @param acceptOrigin predicate for option origins. Origins can be
 545      * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates
 546      * to true.
 547      * @param additionalArgs additional arguments to the Java process which ran
 548      * with "-XX:+PrintFlagsRanges"
 549      * @return Map from option name to the JVMOption object
 550      * @throws Exception if a new process can not be created or an error
 551      * occurred while reading the data
 552      */
 553     public static Map<String, JVMOption> getOptionsWithRangeAsMap(Predicate<String> acceptOrigin, String... additionalArgs) throws Exception {
 554         return getOptionsAsMap(true, acceptOrigin, additionalArgs);
 555     }
 556 
 557     /**
 558      * Get JVM options with range as map
 559      *
 560      * @param additionalArgs additional arguments to the Java process which ran
 561      * with "-XX:+PrintFlagsRanges"
 562      * @return map from option name to the JVMOption object
 563      * @throws Exception if a new process can not be created or an error
 564      * occurred while reading the data
 565      */
 566     public static Map<String, JVMOption> getOptionsWithRangeAsMap(String... additionalArgs) throws Exception {
 567         return getOptionsWithRangeAsMap(origin -> true, additionalArgs);
 568     }
 569 }