1 /*
  2  * Copyright (c) 2023, 2024, 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 jtreg.SkippedException;
 32 
 33 /*
 34  * This is a base class used for testing CDS functionalities with complex applications.
 35  * You can define the application by overridding the vmArgs(), classpath() and appCommandLine()
 36  * methods. Application-specific validation checks can be implemented with checkExecution().
 37  *
 38  * Note: to debug the new workflow, run jtreg with -vmoption:-DCDSAppTester.split.new.workflow=true
 39  * This will run the new workflow in two separate processes that you can rerun easily inside a debugger.
 40  * Also, the log files are easier to read.
 41 */
 42 abstract public class CDSAppTester {
 43     private final String name;
 44     private final String classListFile;
 45     private final String classListFileLog;
 46     private final String staticArchiveFile;
 47     private final String staticArchiveFileLog;
 48     private final String dynamicArchiveFile;
 49     private final String dynamicArchiveFileLog;
 50     private final String codeCacheFile;  // old workflow
 51     private final String codeCacheFileLog;
 52     private final String cdsFile;        // new workflow: -XX:CacheDataStore=<foo>.cds
 53     private final String cdsFileLog;
 54     private final String cdsFilePreImage;        // new workflow: -XX:CacheDataStore=<foo>.cds
 55     private final String cdsFilePreImageLog;
 56     private final String aotFile;        // new workflow = cdsFile + ".code"
 57     private int numProductionRuns = 0;
 58 
 59     public CDSAppTester(String name) {
 60         if (CDSTestUtils.DYNAMIC_DUMP) {
 61             throw new jtreg.SkippedException("Tests based on CDSAppTester should be excluded when -Dtest.dynamic.cds.archive is specified");
 62         }
 63 
 64         // Old workflow
 65         this.name = name;
 66         classListFile = name() + ".classlist";
 67         classListFileLog = classListFile + ".log";
 68         staticArchiveFile = name() + ".static.jsa";
 69         staticArchiveFileLog = staticArchiveFile + ".log";
 70         dynamicArchiveFile = name() + ".dynamic.jsa";
 71         dynamicArchiveFileLog = dynamicArchiveFile + ".log";
 72         codeCacheFile = name() + ".code.jsa";
 73         codeCacheFileLog = codeCacheFile + ".log";
 74         cdsFile = name() + ".cds";
 75         cdsFileLog = cdsFile + ".log";
 76         cdsFilePreImage = cdsFile + ".preimage";
 77         cdsFilePreImageLog = cdsFilePreImage + ".log";
 78         aotFile = cdsFile + ".code";
 79     }
 80 
 81     private String productionRunLog() {
 82         if (numProductionRuns == 0) {
 83             return name() + ".production.log";
 84         } else {
 85             return name() + ".production." + numProductionRuns + ".log";
 86         }
 87     }
 88 
 89     private enum Workflow {
 90         STATIC,        // classic -Xshare:dump workflow
 91         DYNAMIC,       // classic -XX:ArchiveClassesAtExit
 92         LEYDEN_OLD,    // The old "5 step workflow", to be phased out
 93         LEYDEN,        // The new "one step training workflow" -- see JDK-8320264
 94     }
 95 
 96     public enum RunMode {
 97         CLASSLIST,
 98         DUMP_STATIC,
 99         DUMP_DYNAMIC,
100         DUMP_CODECACHE,    // LEYDEN_OLD only
101         TRAINING,          // LEYDEN only
102         TRAINING0,         // LEYDEN only
103         TRAINING1,         // LEYDEN only
104         PRODUCTION;
105 
106         public boolean isStaticDump() {
107             return this == DUMP_STATIC;
108         }
109         public boolean isProductionRun() {
110             return this == PRODUCTION;
111         }
112     }
113 
114     public final String name() {
115         return this.name;
116     }
117 
118     // optional
119     public String[] vmArgs(RunMode runMode) {
120         return new String[0];
121     }
122 
123     // optional
124     public String classpath(RunMode runMode) {
125         return null;
126     }
127 
128     // must override
129     // main class, followed by arguments to the main class
130     abstract public String[] appCommandLine(RunMode runMode);
131 
132     // optional
133     public void checkExecution(OutputAnalyzer out, RunMode runMode) throws Exception {}
134 
135     private Workflow workflow;
136     private boolean checkExitValue = true;
137 
138     public final void setCheckExitValue(boolean b) {
139         checkExitValue = b;
140     }
141 
142     public final boolean isStaticWorkflow() {
143         return workflow == Workflow.STATIC;
144     }
145 
146     public final boolean isDynamicWorkflow() {
147         return workflow == Workflow.DYNAMIC;
148     }
149 
150     public final boolean isLeydenOldWorkflow() {
151         return workflow == Workflow.LEYDEN_OLD;
152     }
153 
154     public final boolean isLeydenWorkflow() {
155         return workflow == Workflow.LEYDEN;
156     }
157 
158     private String logToFile(String logFile, String... logTags) {
159         StringBuilder sb = new StringBuilder("-Xlog:");
160         String prefix = "";
161         for (String tag : logTags) {
162             sb.append(prefix);
163             sb.append(tag);
164             prefix = ",";
165         }
166         sb.append(":file=" + logFile + "::filesize=0");
167         return sb.toString();
168     }
169 
170     private void listOutputFile(String file) {
171         File f = new File(file);
172         if (f.exists()) {
173             System.out.println("[output file: " + file + " " + f.length() + " bytes]");
174         } else {
175             System.out.println("[output file: " + file + " does not exist]");
176         }
177     }
178 
179     private OutputAnalyzer executeAndCheck(String[] cmdLine, RunMode runMode, String... logFiles) throws Exception {
180         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
181         Process process = pb.start();
182         OutputAnalyzer output = CDSTestUtils.executeAndLog(process, runMode.toString());
183         for (String logFile : logFiles) {
184             listOutputFile(logFile);
185         }
186         if (checkExitValue) {
187             output.shouldHaveExitValue(0);
188         }
189         CDSTestUtils.checkCommonExecExceptions(output);
190         checkExecution(output, runMode);
191         return output;
192     }
193 
194     private OutputAnalyzer createClassList() throws Exception {
195         RunMode runMode = RunMode.CLASSLIST;
196         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
197                                                    "-Xshare:off",
198                                                    "-XX:DumpLoadedClassList=" + classListFile,
199                                                    "-cp", classpath(runMode),
200                                                    logToFile(classListFileLog,
201                                                              "class+load=debug"));
202         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
203         return executeAndCheck(cmdLine, runMode, classListFile, classListFileLog);
204     }
205 
206     private OutputAnalyzer dumpStaticArchive() throws Exception {
207         RunMode runMode = RunMode.DUMP_STATIC;
208         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
209                                                    "-Xlog:cds",
210                                                    "-Xlog:cds+heap=error",
211                                                    "-Xshare:dump",
212                                                    "-XX:SharedArchiveFile=" + staticArchiveFile,
213                                                    "-XX:SharedClassListFile=" + classListFile,
214                                                    "-cp", classpath(runMode),
215                                                    logToFile(staticArchiveFileLog,
216                                                              "cds=debug",
217                                                              "cds+class=debug",
218                                                              "cds+heap=warning",
219                                                              "cds+resolve=debug"));
220         if (isLeydenOldWorkflow()) {
221             cmdLine = StringArrayUtils.concat(cmdLine,
222                                               "-XX:+AOTClassLinking",
223                                               "-XX:+ArchiveDynamicProxies",
224                                               "-XX:+ArchiveReflectionData");
225         }
226 
227         return executeAndCheck(cmdLine, runMode, staticArchiveFile, staticArchiveFileLog);
228     }
229 
230     private OutputAnalyzer dumpDynamicArchive() throws Exception {
231         RunMode runMode = RunMode.DUMP_DYNAMIC;
232         String[] cmdLine = new String[0];
233         if (isDynamicWorkflow()) {
234           // "classic" dynamic archive
235           cmdLine = StringArrayUtils.concat(vmArgs(runMode),
236                                             "-Xlog:cds",
237                                             "-XX:ArchiveClassesAtExit=" + dynamicArchiveFile,
238                                             "-cp", classpath(runMode),
239                                             logToFile(dynamicArchiveFileLog,
240                                                       "cds=debug",
241                                                       "cds+class=debug",
242                                                       "cds+resolve=debug",
243                                                       "class+load=debug"));
244         } else {
245           // Leyden "OLD" workflow step 3
246           cmdLine = StringArrayUtils.concat(vmArgs(runMode),
247                                             "-Xlog:cds",
248                                             "-XX:ArchiveClassesAtExit=" + dynamicArchiveFile,
249                                             "-XX:SharedArchiveFile=" + staticArchiveFile,
250                                             "-XX:+RecordTraining",
251                                             "-cp", classpath(runMode),
252                                             logToFile(dynamicArchiveFileLog,
253                                                       "cds=debug",
254                                                       "cds+class=debug",
255                                                       "cds+resolve=debug",
256                                                       "class+load=debug"));
257         }
258         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
259         return executeAndCheck(cmdLine, runMode, dynamicArchiveFile, dynamicArchiveFileLog);
260     }
261 
262 
263     private OutputAnalyzer dumpCodeCache() throws Exception {
264         RunMode runMode = RunMode.DUMP_CODECACHE;
265         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
266                                                    "-XX:SharedArchiveFile=" + dynamicArchiveFile,
267                                                    "-XX:+ReplayTraining",
268                                                    "-XX:+StoreCachedCode",
269                                                    "-XX:CachedCodeFile=" + codeCacheFile,
270                                                    "-XX:CachedCodeMaxSize=512M",
271                                                    "-cp", classpath(runMode),
272                                                    logToFile(codeCacheFileLog, "scc"));
273 
274         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
275         return executeAndCheck(cmdLine, runMode, codeCacheFile, codeCacheFileLog);
276     }
277 
278     private OutputAnalyzer oldProductionRun() throws Exception {
279         RunMode runMode = RunMode.PRODUCTION;
280         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
281                                                    "-XX:+UnlockDiagnosticVMOptions",
282                                                    "-XX:VerifyArchivedFields=2", // make sure archived heap objects are good.
283                                                    "-XX:SharedArchiveFile=" + dynamicArchiveFile,
284                                                    "-XX:+ReplayTraining",
285                                                    "-XX:+LoadCachedCode",
286                                                    "-XX:CachedCodeFile=" + codeCacheFile,
287                                                    "-cp", classpath(runMode),
288                                                    logToFile(productionRunLog(), "cds", "scc*=warning"));
289         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
290         return executeAndCheck(cmdLine, runMode, productionRunLog());
291     }
292 
293     private String trainingLog(String file) {
294         return logToFile(file,
295                          "cds=debug",
296                          "cds+class=debug",
297                          "cds+heap=warning",
298                          "cds+resolve=debug");
299     }
300 
301     // normal training workflow (main JVM process spawns child process)
302     private OutputAnalyzer trainingRun() throws Exception {
303         RunMode runMode = RunMode.TRAINING;
304         File f = new File(cdsFile);
305         f.delete();
306         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
307                                                    "-XX:+AOTClassLinking",
308                                                    "-XX:+ArchiveDynamicProxies",
309                                                  //"-XX:+ArchiveReflectionData",
310                                                    "-XX:CacheDataStore=" + cdsFile,
311                                                    "-cp", classpath(runMode),
312                                                    // Use PID to distinguish the logs of the training process
313                                                    // and the forked final image dump process.
314                                                    "-Xlog:cds::uptime,level,tags,pid",
315                                                    trainingLog(cdsFileLog));
316         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
317         OutputAnalyzer out =  executeAndCheck(cmdLine, runMode, cdsFile, cdsFileLog);
318         listOutputFile(cdsFile + ".log.0"); // the preimage dump
319         return out;
320     }
321 
322     // "split" training workflow (launch the two processes manually, for easier debugging);
323     private OutputAnalyzer trainingRun0() throws Exception {
324         RunMode runMode = RunMode.TRAINING0;
325         File f = new File(cdsFile);
326         f.delete();
327         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
328                                                    "-XX:+UnlockDiagnosticVMOptions",
329                                                    "-XX:+CDSManualFinalImage",
330                                                    "-XX:+AOTClassLinking",
331                                                    "-XX:+ArchiveDynamicProxies",
332                                                  //"-XX:+ArchiveReflectionData",
333                                                    "-XX:CacheDataStore=" + cdsFile,
334                                                    "-cp", classpath(runMode),
335                                                    trainingLog(cdsFilePreImageLog));
336         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
337         return executeAndCheck(cmdLine, runMode, cdsFilePreImage, cdsFilePreImageLog);
338     }
339     private OutputAnalyzer trainingRun1() throws Exception {
340         RunMode runMode = RunMode.TRAINING1;
341         File f = new File(cdsFile);
342         f.delete();
343         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
344                                                    "-XX:+UnlockDiagnosticVMOptions",
345                                                    "-XX:+AOTClassLinking",
346                                                    "-XX:+ArchiveDynamicProxies",
347                                                  //"-XX:+ArchiveReflectionData",
348                                                    "-XX:CacheDataStore=" + cdsFile,
349                                                    "-XX:CDSPreimage=" + cdsFilePreImage,
350                                                    "-cp", classpath(runMode),
351                                                    trainingLog(cdsFileLog));
352         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
353         return executeAndCheck(cmdLine, runMode, cdsFile, aotFile, cdsFileLog);
354     }
355 
356     private OutputAnalyzer productionRun() throws Exception {
357         return productionRun(null, null);
358     }
359 
360     public OutputAnalyzer productionRun(String[] extraVmArgs) throws Exception {
361         return productionRun(extraVmArgs, null);
362     }
363 
364     // After calling run(String[]), you can call this method to run the app again, with the AOTCache
365     // using different args to the VM and application.
366     public OutputAnalyzer productionRun(String[] extraVmArgs, String[] extraAppArgs) throws Exception {
367         RunMode runMode = RunMode.PRODUCTION;
368         String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
369                                                    "-XX:+UnlockDiagnosticVMOptions",
370                                                    "-XX:VerifyArchivedFields=2", // make sure archived heap objects are good.
371                                                    "-cp", classpath(runMode),
372                                                    logToFile(productionRunLog(), "cds"));
373 
374         if (isStaticWorkflow()) {
375             cmdLine = StringArrayUtils.concat(cmdLine, "-XX:SharedArchiveFile=" + staticArchiveFile);
376         } else if (isDynamicWorkflow()) {
377             cmdLine = StringArrayUtils.concat(cmdLine, "-XX:SharedArchiveFile=" + dynamicArchiveFile);
378         } else {
379             cmdLine = StringArrayUtils.concat(cmdLine, "-XX:CacheDataStore=" + cdsFile);
380         }
381 
382         if (extraVmArgs != null) {
383             cmdLine = StringArrayUtils.concat(cmdLine, extraVmArgs);
384         }
385 
386         cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
387 
388         if (extraAppArgs != null) {
389             cmdLine = StringArrayUtils.concat(cmdLine, extraAppArgs);
390         }
391 
392         OutputAnalyzer out = executeAndCheck(cmdLine, runMode, productionRunLog());
393         numProductionRuns ++;
394         return out;
395     }
396 
397     public void run(String args[]) throws Exception {
398         String err = "Must have exactly one command line argument of the following: ";
399         String prefix = "";
400         for (Workflow wf : Workflow.values()) {
401             err += prefix;
402             err += wf;
403             prefix = ", ";
404         }
405         if (args.length != 1) {
406             throw new RuntimeException(err);
407         } else {
408             if (args[0].equals("STATIC")) {
409                 runStaticWorkflow();
410             } else if (args[0].equals("DYNAMIC")) {
411                 runDynamicWorkflow();
412             } else if (args[0].equals("LEYDEN_OLD")) {
413                 runLeydenOldWorkflow();
414             } else if (args[0].equals("LEYDEN")) {
415                 runLeydenWorkflow(false);
416             } else if (args[0].equals("LEYDEN_TRAINONLY")) {
417                 runLeydenWorkflow(true);
418             } else {
419                 throw new RuntimeException(err);
420             }
421         }
422     }
423 
424     private void runStaticWorkflow() throws Exception {
425         this.workflow = Workflow.STATIC;
426         createClassList();
427         dumpStaticArchive();
428         productionRun();
429     }
430 
431     private void runDynamicWorkflow() throws Exception {
432         this.workflow = Workflow.DYNAMIC;
433         dumpDynamicArchive();
434         productionRun();
435     }
436 
437     private void runLeydenOldWorkflow() throws Exception {
438         this.workflow = Workflow.LEYDEN_OLD;
439         createClassList();
440         dumpStaticArchive();
441         dumpDynamicArchive();
442         dumpCodeCache();
443         oldProductionRun();
444     }
445 
446     private void runLeydenWorkflow(boolean trainOnly) throws Exception {
447         this.workflow = Workflow.LEYDEN;
448         if (System.getProperty("CDSAppTester.split.new.workflow") != null) {
449             trainingRun0();
450             trainingRun1();
451         } else {
452             trainingRun();
453         }
454         if (!trainOnly) {
455             productionRun();
456         }
457     }
458 }