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