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 }