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