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