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 "InitialTenuringThreshold": 220 option.addPrepend("-XX:MaxTenuringThreshold=" + option.getMax()); 221 break; 222 case "NUMAInterleaveGranularity": 223 option.addPrepend("-XX:+UseNUMAInterleaving"); 224 break; 225 case "CPUForCMSThread": 226 option.addPrepend("-XX:+BindCMSThreadToCPU"); 227 break; 228 case "VerifyGCStartAt": 229 option.addPrepend("-XX:+VerifyBeforeGC"); 230 option.addPrepend("-XX:+VerifyAfterGC"); 231 break; 232 case "NewSizeThreadIncrease": 233 option.addPrepend("-XX:+UseSerialGC"); 234 break; 235 case "SharedBaseAddress": 236 case "SharedSymbolTableBucketSize": 237 option.addPrepend("-XX:+UnlockDiagnosticVMOptions"); 238 option.addPrepend("-XX:SharedArchiveFile=TestOptionsWithRanges.jsa"); 239 option.addPrepend("-Xshare:dump"); 240 break; 241 case "TLABWasteIncrement": 242 option.addPrepend("-XX:+UseParallelGC"); 243 break; 244 default: 245 /* Do nothing */ 246 break; 247 } 248 } 249 250 /** 251 * Parse JVM Options. Get input from "inputReader". Parse using 252 * "-XX:+PrintFlagsRanges" output format. 253 * 254 * @param inputReader input data for parsing 255 * @param withRanges true if needed options with defined ranges inside JVM 256 * @param acceptOrigin predicate for option origins. Origins can be 257 * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates 258 * to true. 259 * @return map from option name to the JVMOption object 260 * @throws IOException if an error occurred while reading the data 261 */ 262 private static Map<String, JVMOption> getJVMOptions(Reader inputReader, 263 boolean withRanges, Predicate<String> acceptOrigin) throws IOException { 264 BufferedReader reader = new BufferedReader(inputReader); 265 String type; 266 String line; 267 String token; 268 String name; 269 StringTokenizer st; 270 JVMOption option; 271 Map<String, JVMOption> allOptions = new LinkedHashMap<>(); 272 273 // Skip first line 274 line = reader.readLine(); 275 276 while ((line = reader.readLine()) != null) { 277 /* 278 * Parse option from following line: 279 * <type> <name> [ <min, optional> ... <max, optional> ] {<origin>} 280 */ 281 st = new StringTokenizer(line); 282 283 type = st.nextToken(); 284 285 name = st.nextToken(); 286 287 option = JVMOption.createVMOption(type, name); 288 289 /* Skip '[' */ 290 token = st.nextToken(); 291 292 /* Read min range or "..." if range is absent */ 293 token = st.nextToken(); 294 295 if (token.equals("...") == false) { 296 if (!withRanges) { 297 /* 298 * Option have range, but asked for options without 299 * ranges => skip it 300 */ 301 continue; 302 } 303 304 /* Mark this option as option which range is defined in VM */ 305 option.optionWithRange(); 306 307 option.setMin(token); 308 309 /* Read "..." and skip it */ 310 token = st.nextToken(); 311 312 /* Get max value */ 313 token = st.nextToken(); 314 option.setMax(token); 315 } else if (withRanges) { 316 /* 317 * Option not have range, but asked for options with 318 * ranges => skip it 319 */ 320 continue; 321 } 322 323 /* Skip ']' */ 324 token = st.nextToken(); 325 326 /* Read origin of the option */ 327 token = st.nextToken(); 328 329 while (st.hasMoreTokens()) { 330 token += st.nextToken(); 331 }; 332 token = token.substring(1, token.indexOf("}")); 333 334 if (acceptOrigin.test(token)) { 335 addNameDependency(option); 336 337 allOptions.put(name, option); 338 } 339 } 340 341 return allOptions; 342 } 343 344 static void failedMessage(String optionName, String value, boolean valid, String message) { 345 String temp; 346 347 if (valid) { 348 temp = "valid"; 349 } else { 350 temp = "invalid"; 351 } 352 353 failedMessage(String.format("Error processing option %s with %s value '%s'! %s", 354 optionName, temp, value, message)); 355 } 356 357 static void failedMessage(String message) { 358 System.err.println("TEST FAILED: " + message); 359 finalFailedMessage.append(String.format("(%s)%n", message)); 360 } 361 362 static void printOutputContent(OutputAnalyzer output) { 363 System.err.println(String.format("stdout content[%s]", output.getStdout())); 364 System.err.println(String.format("stderr content[%s]%n", output.getStderr())); 365 } 366 367 /** 368 * Return string with accumulated failure messages 369 * 370 * @return string with accumulated failure messages 371 */ 372 public static String getMessageWithFailures() { 373 return finalFailedMessage.toString(); 374 } 375 376 /** 377 * Run command line tests for options passed in the list 378 * 379 * @param options list of options to test 380 * @return number of failed tests 381 * @throws Exception if java process can not be started 382 */ 383 public static int runCommandLineTests(List<? extends JVMOption> options) throws Exception { 384 int failed = 0; 385 386 for (JVMOption option : options) { 387 failed += option.testCommandLine(); 388 } 389 390 return failed; 391 } 392 393 /** 394 * Test passed options using DynamicVMOption isValidValue and isInvalidValue 395 * methods. Only tests writeable options. 396 * 397 * @param options list of options to test 398 * @return number of failed tests 399 */ 400 public static int runDynamicTests(List<? extends JVMOption> options) { 401 int failed = 0; 402 403 for (JVMOption option : options) { 404 failed += option.testDynamic(); 405 } 406 407 return failed; 408 } 409 410 /** 411 * Test passed options using Jcmd. Only tests writeable options. 412 * 413 * @param options list of options to test 414 * @return number of failed tests 415 */ 416 public static int runJcmdTests(List<? extends JVMOption> options) { 417 int failed = 0; 418 419 for (JVMOption option : options) { 420 failed += option.testJcmd(); 421 } 422 423 return failed; 424 } 425 426 /** 427 * Test passed option using attach method. Only tests writeable options. 428 * 429 * @param options list of options to test 430 * @return number of failed tests 431 * @throws Exception if an error occurred while attaching to the target JVM 432 */ 433 public static int runAttachTests(List<? extends JVMOption> options) throws Exception { 434 int failed = 0; 435 436 for (JVMOption option : options) { 437 failed += option.testAttach(); 438 } 439 440 return failed; 441 } 442 443 /** 444 * Get JVM options as map. Can return options with defined ranges or options 445 * without range depending on "withRanges" argument. "acceptOrigin" 446 * predicate can be used to filter option origin. 447 * 448 * @param withRanges true if needed options with defined ranges inside JVM 449 * @param acceptOrigin predicate for option origins. Origins can be 450 * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates 451 * to true. 452 * @param additionalArgs additional arguments to the Java process which ran 453 * with "-XX:+PrintFlagsRanges" 454 * @return map from option name to the JVMOption object 455 * @throws Exception if a new process can not be created or an error 456 * occurred while reading the data 457 */ 458 public static Map<String, JVMOption> getOptionsAsMap(boolean withRanges, Predicate<String> acceptOrigin, 459 String... additionalArgs) throws Exception { 460 Map<String, JVMOption> result; 461 Process p; 462 List<String> runJava = new ArrayList<>(); 463 464 if (additionalArgs.length > 0) { 465 runJava.addAll(Arrays.asList(additionalArgs)); 466 } 467 468 if (VMType != null) { 469 runJava.add(VMType); 470 } 471 472 if (GCType != null) { 473 runJava.add(GCType); 474 } 475 runJava.add(PRINT_FLAGS_RANGES); 476 runJava.add("-version"); 477 478 p = ProcessTools.createJavaProcessBuilder(runJava.toArray(new String[0])).start(); 479 480 result = getJVMOptions(new InputStreamReader(p.getInputStream()), withRanges, acceptOrigin); 481 482 p.waitFor(); 483 484 return result; 485 } 486 487 /** 488 * Get JVM options as list. Can return options with defined ranges or 489 * options without range depending on "withRanges" argument. "acceptOrigin" 490 * predicate can be used to filter option origin. 491 * 492 * @param withRanges true if needed options with defined ranges inside JVM 493 * @param acceptOrigin predicate for option origins. Origins can be 494 * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates 495 * to true. 496 * @param additionalArgs additional arguments to the Java process which ran 497 * with "-XX:+PrintFlagsRanges" 498 * @return List of options 499 * @throws Exception if a new process can not be created or an error 500 * occurred while reading the data 501 */ 502 public static List<JVMOption> getOptions(boolean withRanges, Predicate<String> acceptOrigin, 503 String... additionalArgs) throws Exception { 504 return new ArrayList<>(getOptionsAsMap(withRanges, acceptOrigin, additionalArgs).values()); 505 } 506 507 /** 508 * Get JVM options with ranges as list. "acceptOrigin" predicate can be used 509 * to filter option origin. 510 * 511 * @param acceptOrigin predicate for option origins. Origins can be 512 * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates 513 * to true. 514 * @param additionalArgs additional arguments to the Java process which ran 515 * with "-XX:+PrintFlagsRanges" 516 * @return List of options 517 * @throws Exception if a new process can not be created or an error 518 * occurred while reading the data 519 */ 520 public static List<JVMOption> getOptionsWithRange(Predicate<String> acceptOrigin, String... additionalArgs) throws Exception { 521 return getOptions(true, acceptOrigin, additionalArgs); 522 } 523 524 /** 525 * Get JVM options with ranges as list. 526 * 527 * @param additionalArgs additional arguments to the Java process which ran 528 * with "-XX:+PrintFlagsRanges" 529 * @return list of options 530 * @throws Exception if a new process can not be created or an error 531 * occurred while reading the data 532 */ 533 public static List<JVMOption> getOptionsWithRange(String... additionalArgs) throws Exception { 534 return getOptionsWithRange(origin -> true, additionalArgs); 535 } 536 537 /** 538 * Get JVM options with range as map. "acceptOrigin" predicate can be used 539 * to filter option origin. 540 * 541 * @param acceptOrigin predicate for option origins. Origins can be 542 * "product", "diagnostic" etc. Accept option only if acceptOrigin evaluates 543 * to true. 544 * @param additionalArgs additional arguments to the Java process which ran 545 * with "-XX:+PrintFlagsRanges" 546 * @return Map from option name to the JVMOption object 547 * @throws Exception if a new process can not be created or an error 548 * occurred while reading the data 549 */ 550 public static Map<String, JVMOption> getOptionsWithRangeAsMap(Predicate<String> acceptOrigin, String... additionalArgs) throws Exception { 551 return getOptionsAsMap(true, acceptOrigin, additionalArgs); 552 } 553 554 /** 555 * Get JVM options with range as map 556 * 557 * @param additionalArgs additional arguments to the Java process which ran 558 * with "-XX:+PrintFlagsRanges" 559 * @return map from option name to the JVMOption object 560 * @throws Exception if a new process can not be created or an error 561 * occurred while reading the data 562 */ 563 public static Map<String, JVMOption> getOptionsWithRangeAsMap(String... additionalArgs) throws Exception { 564 return getOptionsWithRangeAsMap(origin -> true, additionalArgs); 565 } 566 }