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