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