1 /* 2 * Copyright (c) 2023, 2025, 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 jdk.test.lib.cds; 25 26 import java.io.File; 27 import jdk.test.lib.cds.CDSTestUtils; 28 import jdk.test.lib.process.ProcessTools; 29 import jdk.test.lib.process.OutputAnalyzer; 30 import jdk.test.lib.StringArrayUtils; 31 import jdk.test.whitebox.WhiteBox; 32 import jtreg.SkippedException; 33 34 /* 35 * This is a base class used for testing CDS functionalities with complex applications. 36 * You can define the application by overridding the vmArgs(), classpath() and appCommandLine() 37 * methods. Application-specific validation checks can be implemented with checkExecution(). 38 * 39 * Note: to debug the new workflow, run jtreg with -vmoption:-DCDSAppTester.split.new.workflow=true 40 * This will run the new workflow in two separate processes that you can rerun easily inside a debugger. 41 * Also, the log files are easier to read. 42 */ 43 abstract public class CDSAppTester { 44 private final String name; 45 private final String classListFile; 46 private final String classListFileLog; 47 private final String aotConfigurationFile; 48 private final String aotConfigurationFileLog; 49 private final String staticArchiveFile; 50 private final String staticArchiveFileLog; 51 private final String aotCacheFile; 52 private final String aotCacheFileLog; 53 private final String dynamicArchiveFile; 54 private final String dynamicArchiveFileLog; 55 private final String cdsFile; // new workflow: -XX:CacheDataStore=<foo>.cds 56 private final String cdsFileLog; 57 private final String cdsFilePreImage; // new workflow: -XX:CacheDataStore=<foo>.cds 58 private final String cdsFilePreImageLog; 59 private final String tempBaseArchiveFile; 60 private int numProductionRuns = 0; 61 private String whiteBoxJar = null; 62 private boolean inOneStepTraining = false; 63 64 /** 65 * All files created in the CDS/AOT workflow will be name + extension. E.g. 66 * - name.aot 67 * - name.aotconfig 68 * - name.classlist 69 * - name.jsa 70 */ 71 public CDSAppTester(String name) { 72 if (CDSTestUtils.DYNAMIC_DUMP) { 73 throw new SkippedException("Tests based on CDSAppTester should be excluded when -Dtest.dynamic.cds.archive is specified"); 74 } 75 76 this.name = name; 77 classListFile = name() + ".classlist"; 78 classListFileLog = logFileName(classListFile); 79 aotConfigurationFile = name() + ".aotconfig"; 80 aotConfigurationFileLog = logFileName(aotConfigurationFile); 81 staticArchiveFile = name() + ".static.jsa"; 82 staticArchiveFileLog = logFileName(staticArchiveFile); 83 aotCacheFile = name() + ".aot"; 84 aotCacheFileLog = logFileName(aotCacheFile);; 85 dynamicArchiveFile = name() + ".dynamic.jsa"; 86 dynamicArchiveFileLog = logFileName(dynamicArchiveFile); 87 cdsFile = name() + ".cds"; 88 cdsFileLog = logFileName(cdsFile); 89 cdsFilePreImage = cdsFile + ".preimage"; 90 cdsFilePreImageLog = logFileName(cdsFilePreImage); 91 tempBaseArchiveFile = name() + ".temp-base.jsa"; 92 } 93 94 private String productionRunLog() { 95 if (numProductionRuns == 0) { 96 return logFileName(name() + ".production"); 97 } else { 98 return logFileName(name() + ".production." + numProductionRuns); 99 } 100 } 101 102 private static String logFileName(String file) { 103 file = file.replace("\"", "%22"); 104 file = file.replace("'", "%27"); 105 return file + ".log"; 106 } 107 108 private enum Workflow { 109 STATIC, // classic -Xshare:dump workflow 110 DYNAMIC, // classic -XX:ArchiveClassesAtExit 111 AOT, // JEP 483 Ahead-of-Time Class Loading & Linking 112 LEYDEN, // The new "one step training workflow" -- see JDK-8320264 113 } 114 115 public enum RunMode { 116 TRAINING, // -XX:DumpLoadedClassList OR {-XX:AOTMode=record -XX:AOTConfiguration} 117 TRAINING0, // LEYDEN only 118 TRAINING1, // LEYDEN only (assembly phase, app logic not executed) 119 DUMP_STATIC, // -Xshare:dump 120 DUMP_DYNAMIC, // -XX:ArchiveClassesArExit 121 ASSEMBLY, // JEP 483 (assembly phase, app logic not executed) 122 PRODUCTION; // Running with the CDS archive produced from the above steps 123 124 public boolean isStaticDump() { 125 return this == DUMP_STATIC; 126 } 127 public boolean isProductionRun() { 128 return this == PRODUCTION; 129 } 130 131 // When <code>CDSAppTester::checkExecution(out, runMode)</code> is called, has the application been 132 // executed? If so, <code>out</code> should contain logs printed by the application's own logic. 133 public boolean isApplicationExecuted() { 134 return (this != TRAINING1) && (this != ASSEMBLY) && (this != DUMP_STATIC); 135 } 136 } 137 138 public boolean isDumping(RunMode runMode) { 139 if (isStaticWorkflow()) { 140 return runMode == RunMode.DUMP_STATIC; 141 } else if (isDynamicWorkflow()) { 142 return runMode == RunMode.DUMP_DYNAMIC; 143 } else if (isAOTWorkflow()) { 144 return runMode == RunMode.TRAINING || runMode == RunMode.ASSEMBLY; 145 } else { 146 return runMode == RunMode.TRAINING || runMode == RunMode.TRAINING0 || runMode == RunMode.TRAINING1; 147 } 148 } 149 150 public final String name() { 151 return this.name; 152 } 153 154 // optional 155 public String[] vmArgs(RunMode runMode) { 156 return new String[0]; 157 } 158 159 // optional 160 public String classpath(RunMode runMode) { 161 return null; 162 } 163 164 // optional 165 public String modulepath(RunMode runMode) { 166 return null; 167 } 168 169 // must override 170 // main class, followed by arguments to the main class 171 abstract public String[] appCommandLine(RunMode runMode); 172 173 // optional 174 public void checkExecution(OutputAnalyzer out, RunMode runMode) throws Exception {} 175 176 private Workflow workflow; 177 private boolean checkExitValue = true; 178 179 public final void setCheckExitValue(boolean b) { 180 checkExitValue = b; 181 } 182 183 public final void useWhiteBox(String whiteBoxJar) { 184 this.whiteBoxJar = whiteBoxJar; 185 } 186 187 public final boolean isStaticWorkflow() { 188 return workflow == Workflow.STATIC; 189 } 190 191 public final boolean isDynamicWorkflow() { 192 return workflow == Workflow.DYNAMIC; 193 } 194 195 public final boolean isAOTWorkflow() { 196 return workflow == Workflow.AOT; 197 } 198 199 public final boolean isLeydenWorkflow() { 200 return workflow == Workflow.LEYDEN; 201 } 202 203 private String logToFile(String logFile, String... logTags) { 204 StringBuilder sb = new StringBuilder("-Xlog:"); 205 String prefix = ""; 206 for (String tag : logTags) { 207 sb.append(prefix); 208 sb.append(tag); 209 prefix = ","; 210 } 211 sb.append(":file=" + logFile + "::filesize=0"); 212 return sb.toString(); 213 } 214 215 private void listOutputFile(String file) { 216 File f = new File(file); 217 if (f.exists()) { 218 System.out.println("[output file: " + file + " " + f.length() + " bytes]"); 219 } else { 220 System.out.println("[output file: " + file + " does not exist]"); 221 } 222 } 223 224 private OutputAnalyzer executeAndCheck(String[] cmdLine, RunMode runMode, String... logFiles) throws Exception { 225 ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine); 226 Process process = pb.start(); 227 OutputAnalyzer output = CDSTestUtils.executeAndLog(process, runMode.toString()); 228 for (String logFile : logFiles) { 229 listOutputFile(logFile); 230 } 231 if (checkExitValue) { 232 output.shouldHaveExitValue(0); 233 } 234 //output.shouldNotContain(CDSTestUtils.MSG_STATIC_FIELD_MAY_HOLD_DIFFERENT_VALUE); // FIXME -- leyden+JEP483 merge 235 CDSTestUtils.checkCommonExecExceptions(output); 236 checkExecution(output, runMode); 237 return output; 238 } 239 240 private String[] addCommonVMArgs(RunMode runMode, String[] cmdLine) { 241 cmdLine = addClassOrModulePath(runMode, cmdLine); 242 cmdLine = addWhiteBox(cmdLine); 243 return cmdLine; 244 } 245 246 private String[] addClassOrModulePath(RunMode runMode, String[] cmdLine) { 247 String cp = classpath(runMode); 248 if (cp == null) { 249 // Override the "-cp ...." added by Jtreg 250 cmdLine = StringArrayUtils.concat(cmdLine, "-Djava.class.path="); 251 } else { 252 cmdLine = StringArrayUtils.concat(cmdLine, "-cp", cp); 253 } 254 String mp = modulepath(runMode); 255 if (mp != null) { 256 cmdLine = StringArrayUtils.concat(cmdLine, "--module-path", mp); 257 } 258 return cmdLine; 259 } 260 261 private String[] addWhiteBox(String[] cmdLine) { 262 if (whiteBoxJar != null) { 263 cmdLine = StringArrayUtils.concat(cmdLine, 264 "-XX:+UnlockDiagnosticVMOptions", 265 "-XX:+WhiteBoxAPI", 266 "-Xbootclasspath/a:" + whiteBoxJar); 267 } 268 return cmdLine; 269 } 270 271 private OutputAnalyzer recordAOTConfiguration() throws Exception { 272 RunMode runMode = RunMode.TRAINING; 273 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 274 "-XX:AOTMode=record", 275 "-XX:AOTConfiguration=" + aotConfigurationFile, 276 logToFile(aotConfigurationFileLog, 277 "class+load=debug", 278 "cds=debug", 279 "cds+class=debug")); 280 cmdLine = addCommonVMArgs(runMode, cmdLine); 281 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 282 return executeAndCheck(cmdLine, runMode, aotConfigurationFile, aotConfigurationFileLog); 283 } 284 285 private OutputAnalyzer createAOTCacheOneStep() throws Exception { 286 RunMode runMode = RunMode.TRAINING; 287 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 288 "-XX:AOTMode=record", 289 "-XX:AOTCacheOutput=" + aotCacheFile, 290 logToFile(aotCacheFileLog, 291 "class+load=debug", 292 "cds=debug", 293 "cds+class=debug")); 294 cmdLine = addCommonVMArgs(runMode, cmdLine); 295 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 296 OutputAnalyzer out = executeAndCheck(cmdLine, runMode, aotCacheFile, aotCacheFileLog); 297 listOutputFile(aotCacheFileLog + ".0"); // the log file for the training run 298 return out; 299 } 300 301 private OutputAnalyzer createClassList() throws Exception { 302 RunMode runMode = RunMode.TRAINING; 303 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 304 "-Xshare:off", 305 "-XX:DumpLoadedClassList=" + classListFile, 306 logToFile(classListFileLog, 307 "class+load=debug")); 308 cmdLine = addCommonVMArgs(runMode, cmdLine); 309 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 310 return executeAndCheck(cmdLine, runMode, classListFile, classListFileLog); 311 } 312 313 private OutputAnalyzer dumpStaticArchive() throws Exception { 314 RunMode runMode = RunMode.DUMP_STATIC; 315 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 316 "-Xlog:cds", 317 "-Xlog:cds+heap=error", 318 "-Xshare:dump", 319 "-XX:SharedArchiveFile=" + staticArchiveFile, 320 "-XX:SharedClassListFile=" + classListFile, 321 logToFile(staticArchiveFileLog, 322 "cds=debug", 323 "cds+class=debug", 324 "cds+heap=warning", 325 "cds+resolve=debug")); 326 cmdLine = addCommonVMArgs(runMode, cmdLine); 327 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 328 return executeAndCheck(cmdLine, runMode, staticArchiveFile, staticArchiveFileLog); 329 } 330 331 private OutputAnalyzer createAOTCache() throws Exception { 332 RunMode runMode = RunMode.ASSEMBLY; 333 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 334 "-Xlog:cds", 335 "-Xlog:cds+heap=error", 336 "-XX:AOTMode=create", 337 "-XX:AOTConfiguration=" + aotConfigurationFile, 338 "-XX:AOTCache=" + aotCacheFile, 339 logToFile(aotCacheFileLog, 340 "cds=debug", 341 "cds+class=debug", 342 "cds+heap=warning", 343 "cds+resolve=debug")); 344 cmdLine = addCommonVMArgs(runMode, cmdLine); 345 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 346 return executeAndCheck(cmdLine, runMode, aotCacheFile, aotCacheFileLog); 347 } 348 349 // Creating a dynamic CDS archive (with -XX:ArchiveClassesAtExit=<foo>.jsa) requires that the current 350 // JVM process is using a static archive (which is usually the default CDS archive included in the JDK). 351 // However, if the JDK doesn't include a default CDS archive that's compatible with the set of 352 // VM options used by this test, we need to create a temporary static archive to be used with -XX:ArchiveClassesAtExit. 353 private String getBaseArchiveForDynamicArchive() throws Exception { 354 WhiteBox wb = WhiteBox.getWhiteBox(); 355 if (wb.isSharingEnabled()) { 356 // This current JVM is able to use a default CDS archive included by the JDK, so 357 // if we launch a JVM child process (with the same set of options as the current JVM), 358 // that process is also able to use the same default CDS archive for creating 359 // a dynamic archive. 360 return null; 361 } else { 362 // This current JVM is unable to use a default CDS archive, so let's create a temporary 363 // static archive to be used with -XX:ArchiveClassesAtExit. 364 File f = new File(tempBaseArchiveFile); 365 if (!f.exists()) { 366 CDSOptions opts = new CDSOptions(); 367 opts.setArchiveName(tempBaseArchiveFile); 368 opts.addSuffix("-Djava.class.path="); 369 OutputAnalyzer out = CDSTestUtils.createArchive(opts); 370 CDSTestUtils.checkBaseDump(out); 371 } 372 return tempBaseArchiveFile; 373 } 374 } 375 376 private OutputAnalyzer dumpDynamicArchive() throws Exception { 377 RunMode runMode = RunMode.DUMP_DYNAMIC; 378 String[] cmdLine = new String[0]; 379 String baseArchive = getBaseArchiveForDynamicArchive(); 380 if (isDynamicWorkflow()) { 381 // "classic" dynamic archive 382 cmdLine = StringArrayUtils.concat(vmArgs(runMode), 383 "-Xlog:cds", 384 "-XX:ArchiveClassesAtExit=" + dynamicArchiveFile, 385 logToFile(dynamicArchiveFileLog, 386 "cds=debug", 387 "cds+class=debug", 388 "cds+resolve=debug", 389 "class+load=debug")); 390 cmdLine = addCommonVMArgs(runMode, cmdLine); 391 } 392 if (baseArchive != null) { 393 cmdLine = StringArrayUtils.concat(cmdLine, "-XX:SharedArchiveFile=" + baseArchive); 394 } 395 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 396 return executeAndCheck(cmdLine, runMode, dynamicArchiveFile, dynamicArchiveFileLog); 397 } 398 399 private String trainingLog(String file) { 400 return logToFile(file, 401 "cds=debug", 402 "cds+class=debug", 403 "cds+heap=warning", 404 "cds+resolve=debug"); 405 } 406 407 // normal training workflow (main JVM process spawns child process) 408 private OutputAnalyzer trainingRun() throws Exception { 409 RunMode runMode = RunMode.TRAINING; 410 File f = new File(cdsFile); 411 f.delete(); 412 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 413 "-XX:+AOTClassLinking", 414 "-XX:+ArchiveDynamicProxies", 415 //"-XX:+ArchiveReflectionData", 416 "-XX:CacheDataStore=" + cdsFile, 417 "-cp", classpath(runMode), 418 // Use PID to distinguish the logs of the training process 419 // and the forked final image dump process. 420 "-Xlog:cds::uptime,level,tags,pid", 421 trainingLog(cdsFileLog)); 422 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 423 OutputAnalyzer out = executeAndCheck(cmdLine, runMode, cdsFile, cdsFileLog); 424 listOutputFile(cdsFileLog + ".0"); // the preimage dump 425 return out; 426 } 427 428 // "split" training workflow (launch the two processes manually, for easier debugging); 429 private OutputAnalyzer trainingRun0() throws Exception { 430 RunMode runMode = RunMode.TRAINING0; 431 File f = new File(cdsFile); 432 f.delete(); 433 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 434 "-XX:+UnlockDiagnosticVMOptions", 435 "-XX:+CDSManualFinalImage", 436 "-XX:+AOTClassLinking", 437 "-XX:+ArchiveDynamicProxies", 438 //"-XX:+ArchiveReflectionData", 439 "-XX:CacheDataStore=" + cdsFile, 440 "-cp", classpath(runMode), 441 trainingLog(cdsFilePreImageLog)); 442 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 443 return executeAndCheck(cmdLine, runMode, cdsFilePreImage, cdsFilePreImageLog); 444 } 445 private OutputAnalyzer trainingRun1() throws Exception { 446 RunMode runMode = RunMode.TRAINING1; 447 File f = new File(cdsFile); 448 f.delete(); 449 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 450 "-XX:+UnlockDiagnosticVMOptions", 451 "-XX:+AOTClassLinking", 452 "-XX:+ArchiveDynamicProxies", 453 //"-XX:+ArchiveReflectionData", 454 "-XX:CacheDataStore=" + cdsFile, 455 "-XX:CDSPreimage=" + cdsFilePreImage, 456 "-cp", classpath(runMode), 457 trainingLog(cdsFileLog)); 458 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 459 return executeAndCheck(cmdLine, runMode, cdsFile, cdsFileLog); 460 } 461 462 private OutputAnalyzer productionRun() throws Exception { 463 return productionRun(null, null); 464 } 465 466 public OutputAnalyzer productionRun(String[] extraVmArgs) throws Exception { 467 return productionRun(extraVmArgs, null); 468 } 469 470 // After calling run(String[]), you can call this method to run the app again, with the AOTCache 471 // using different args to the VM and application. 472 public OutputAnalyzer productionRun(String[] extraVmArgs, String[] extraAppArgs) throws Exception { 473 RunMode runMode = RunMode.PRODUCTION; 474 String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode), 475 "-XX:+UnlockDiagnosticVMOptions", 476 "-XX:VerifyArchivedFields=2", // make sure archived heap objects are good. 477 logToFile(productionRunLog(), "cds")); 478 cmdLine = addCommonVMArgs(runMode, cmdLine); 479 480 if (isStaticWorkflow()) { 481 cmdLine = StringArrayUtils.concat(cmdLine, "-Xshare:on", "-XX:SharedArchiveFile=" + staticArchiveFile); 482 } else if (isDynamicWorkflow()) { 483 cmdLine = StringArrayUtils.concat(cmdLine, "-Xshare:on", "-XX:SharedArchiveFile=" + dynamicArchiveFile); 484 } else if (isAOTWorkflow()) { 485 cmdLine = StringArrayUtils.concat(cmdLine, "-XX:AOTMode=on", "-XX:AOTCache=" + aotCacheFile); 486 } else { 487 cmdLine = StringArrayUtils.concat(cmdLine, "-XX:CacheDataStore=" + cdsFile); 488 } 489 490 if (extraVmArgs != null) { 491 cmdLine = StringArrayUtils.concat(cmdLine, extraVmArgs); 492 } 493 494 cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode)); 495 496 if (extraAppArgs != null) { 497 cmdLine = StringArrayUtils.concat(cmdLine, extraAppArgs); 498 } 499 500 OutputAnalyzer out = executeAndCheck(cmdLine, runMode, productionRunLog()); 501 numProductionRuns ++; 502 return out; 503 } 504 505 public void run(String... args) throws Exception { 506 String err = "Must have at least one command line argument of the following: "; 507 String prefix = ""; 508 for (Workflow wf : Workflow.values()) { 509 err += prefix; 510 err += wf; 511 prefix = ", "; 512 } 513 if (args.length < 1) { 514 throw new RuntimeException(err); 515 } else { 516 if (args[0].equals("STATIC")) { 517 runStaticWorkflow(); 518 } else if (args[0].equals("DYNAMIC")) { 519 runDynamicWorkflow(); 520 } else if (args[0].equals("AOT")) { 521 runAOTWorkflow(args); 522 } else if (args[0].equals("LEYDEN")) { 523 runLeydenWorkflow(false); 524 } else if (args[0].equals("LEYDEN_TRAINONLY")) { 525 runLeydenWorkflow(true); 526 } else { 527 throw new RuntimeException(err); 528 } 529 } 530 } 531 532 public void runStaticWorkflow() throws Exception { 533 this.workflow = Workflow.STATIC; 534 createClassList(); 535 dumpStaticArchive(); 536 productionRun(); 537 } 538 539 public void runDynamicWorkflow() throws Exception { 540 this.workflow = Workflow.DYNAMIC; 541 dumpDynamicArchive(); 542 productionRun(); 543 } 544 545 // See JEP 483 546 public void runAOTWorkflow(String... args) throws Exception { 547 this.workflow = Workflow.AOT; 548 if (args.length > 1 && args[1].equals("--onestep-training")) { 549 try { 550 inOneStepTraining = true; 551 createAOTCacheOneStep(); 552 } finally { 553 inOneStepTraining = false; 554 } 555 } else { 556 recordAOTConfiguration(); 557 createAOTCache(); 558 } 559 productionRun(); 560 } 561 562 private void runLeydenWorkflow(boolean trainOnly) throws Exception { 563 this.workflow = Workflow.LEYDEN; 564 if (System.getProperty("CDSAppTester.split.new.workflow") != null) { 565 trainingRun0(); 566 trainingRun1(); 567 } else { 568 trainingRun(); 569 } 570 if (!trainOnly) { 571 productionRun(); 572 } 573 } 574 }