1 /*
  2  * Copyright (c) 2020, 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 /**
 25  * @test
 26  * @bug 8241071
 27  * @summary The same JDK build should always generate the same archive file (no randomness).
 28  * @requires vm.cds & vm.flagless
 29  * @library /test/lib
 30  * @build jdk.test.whitebox.WhiteBox
 31  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 32  * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions
 33  *                   -XX:+WhiteBoxAPI DeterministicDump
 34  */
 35 
 36 import jdk.test.lib.cds.CDSArchiveUtils;
 37 import jdk.test.lib.cds.CDSOptions;
 38 import jdk.test.lib.cds.CDSTestUtils;
 39 import jdk.test.lib.Platform;
 40 import java.io.BufferedReader;
 41 import java.io.File;
 42 import java.io.FileInputStream;
 43 import java.io.FileReader;
 44 import java.io.IOException;
 45 import java.util.ArrayDeque;
 46 import java.util.ArrayList;
 47 
 48 public class DeterministicDump {
 49 
 50     static long HEADER_SIZE;      // Size of header in bytes
 51     static int HEADER_LEN = 106;  // Number of lines in CDS map file header
 52     static int LINE_OFFSET = 22;  // Offset from address to first word of data
 53     static int NUM_LINES = 5;     // Number of lines to be printed
 54     static int WORD_LEN = 16 + 1; // Length of word in map file
 55 
 56     public static void main(String[] args) throws Exception {
 57         doTest(false);
 58 
 59         if (Platform.is64bit()) {
 60             // There's no oop/klass compression on 32-bit.
 61             doTest(true);
 62         }
 63     }
 64 
 65     public static void doTest(boolean compressed) throws Exception {
 66         ArrayList<String> baseArgs = new ArrayList<>();
 67 
 68         // Try to reduce indeterminism of GC heap sizing and evacuation.
 69         baseArgs.add("-Xmx128M");
 70         baseArgs.add("-Xms128M");
 71         baseArgs.add("-Xmn120M");
 72 
 73         if (Platform.is64bit()) {
 74             // This option is available only on 64-bit.
 75             String sign = (compressed) ?  "+" : "-";
 76             baseArgs.add("-XX:" + sign + "UseCompressedOops");
 77             baseArgs.add("-XX:+UnlockExperimentalVMOptions");
 78             baseArgs.add("-XX:-UseCompactObjectHeaders");
 79         }
 80 
 81         String baseArchive = dump(baseArgs);
 82         File baseArchiveFile = new File(baseArchive + ".jsa");
 83         HEADER_SIZE = CDSArchiveUtils.fileHeaderSize(baseArchiveFile);
 84 
 85         // (1) Dump with the same args. Should produce the same archive.
 86         String baseArchive2 = dump(baseArgs);
 87         compare(baseArchive, baseArchive2, baseArchiveFile);
 88 
 89         // (2) This will cause the archive to be relocated during dump time. We should
 90         //     still get the same bits. This simulates relocation that happens when
 91         //     Address Space Layout Randomization prevents the archive space to
 92         //     be mapped at the default location.
 93         String relocatedArchive = dump(baseArgs, "-XX:+UnlockDiagnosticVMOptions", "-XX:ArchiveRelocationMode=1");
 94         compare(baseArchive, relocatedArchive, baseArchiveFile);
 95     }
 96 
 97     static int id = 0;
 98     static String dump(ArrayList<String> args, String... more) throws Exception {
 99         String logName = "SharedArchiveFile" + (id++);
100         String archiveName = logName + ".jsa";
101         String mapName = logName + ".map";
102         CDSOptions opts = (new CDSOptions())
103             .addPrefix("-Xint") // Override any -Xmixed/-Xcomp flags from jtreg -vmoptions
104             .addPrefix("-Xlog:cds=debug,gc=debug")
105             .addPrefix("-Xlog:cds+map*=trace:file=" + mapName + ":none:filesize=0")
106             .setArchiveName(archiveName)
107             .addSuffix(args)
108             .addSuffix(more);
109         CDSTestUtils.createArchiveAndCheck(opts);
110 
111         return logName;
112     }
113 
114     static void compare(String file0, String file1, File archiveFile) throws Exception {
115         byte[] buff0 = new byte[4096];
116         byte[] buff1 = new byte[4096];
117         try (FileInputStream in0 = new FileInputStream(file0 + ".jsa");
118              FileInputStream in1 = new FileInputStream(file1 + ".jsa")) {
119             int total = 0;
120             while (true) {
121                 int n0 = read(in0, buff0);
122                 int n1 = read(in1, buff1);
123                 if (n0 != n1) {
124                     throw new RuntimeException("File contents (file sizes?) are different after " + total + " bytes; n0 = "
125                                                + n0 + ", n1 = " + n1);
126                 }
127                 if (n0 == 0) {
128                     System.out.println("File contents are the same: " + total + " bytes");
129                     break;
130                 }
131                 for (int i = 0; i < n0; i++) {
132                     byte b0 = buff0[i];
133                     byte b1 = buff1[i];
134                     if (b0 != b1) {
135                         // The checksums are stored in the header so it should be skipped
136                         // since we want to see the first meaningful diff between the archives
137                         if (total + i > HEADER_SIZE) {
138                             print_diff(file0 + ".map", file1 + ".map", archiveFile, total + i);
139                             throw new RuntimeException("File content different at byte #" + (total + i) + ", b0 = " + b0 + ", b1 = " + b1);
140                         }
141                     }
142                 }
143                 total += n0;
144             }
145         }
146     }
147 
148     static int read(FileInputStream in, byte[] buff) throws IOException {
149         int total = 0;
150         while (total < buff.length) {
151             int n = in.read(buff, total, buff.length - total);
152             if (n <= 0) {
153                 return total;
154             }
155             total += n;
156         }
157 
158         return total;
159     }
160 
161     // CDS map file doesn't print the alignment bytes so they need to be considered
162     // when mapping the byte number in the archive to the word in the map file
163     static int archiveByteToMapWord(File archiveFile, int location) throws Exception {
164         int totalSize = 0;
165         int word = location;
166 
167         long len = HEADER_SIZE;
168         long aligned = CDSArchiveUtils.fileHeaderSizeAligned(archiveFile);
169         for (int i = 0; i < CDSArchiveUtils.num_regions(); i++) {
170             if (i != 0) {
171                 len = CDSArchiveUtils.usedRegionSize(archiveFile, i);
172                 aligned = CDSArchiveUtils.usedRegionSizeAligned(archiveFile, i);
173             }
174             totalSize += len;
175             if (location > totalSize) {
176                 word -= (aligned - len - 16);
177             }
178         }
179         return word/8;
180     }
181 
182     // Read the mapfile and print out the lines associated with the location
183     static void print_diff(String mapName0, String mapName1, File archiveFile, int location) throws Exception {
184         FileReader f0 = new FileReader(mapName0);
185         BufferedReader b0 = new BufferedReader(f0);
186 
187         FileReader f1 = new FileReader(mapName1);
188         BufferedReader b1 = new BufferedReader(f1);
189 
190         int word = archiveByteToMapWord(archiveFile, location);
191         int wordOffset = word % 4; // Each line in the map file prints four words
192         String region = "";
193 
194         // Skip header text and go to first line
195         for (int i = 0; i < HEADER_LEN; i++) {
196             b0.readLine();
197             b1.readLine();
198         }
199 
200         int line_num = HEADER_LEN;
201         String s0 = "";
202         String s1 = "";
203         int count = 0;
204 
205         // Store lines before and including the diff
206         ArrayDeque<String> prefix0 = new ArrayDeque<String>();
207         ArrayDeque<String> prefix1 = new ArrayDeque<String>();
208 
209         // A line may contain 1-4 words so we iterate by word
210         do {
211             s0 = b0.readLine();
212             s1 = b1.readLine();
213             line_num++;
214 
215             if (prefix0.size() >= NUM_LINES / 2 + 1) {
216                 prefix0.removeFirst();
217                 prefix1.removeFirst();
218             }
219             prefix0.addLast(s0);
220             prefix1.addLast(s1);
221 
222             // Skip lines with headers when counting words e.g.
223             // [rw region          0x0000000800000000 - 0x00000008005a1f88   5906312 bytes]
224             // or
225             // 0x0000000800000b28: @@ TypeArrayU1       16
226             if (!s0.contains(": @@") && !s0.contains("bytes]")) {
227                 int words = (s0.length() - LINE_OFFSET - 70) / 8;
228                 count += words;
229             } else if (s0.contains("bytes]")) {
230                 region = s0;
231             }
232         } while (count < word);
233 
234         // Print the diff with the region name above it
235         System.out.println("[First diff: map file #1 (" + mapName0 + ")]");
236         System.out.println(region);
237         String diff0 = print_diff_helper(b0, wordOffset, prefix0);
238 
239         System.out.println("\n[First diff: map file #2 (" + mapName1 + ")]");
240         System.out.println(region);
241         String diff1 = print_diff_helper(b1, wordOffset, prefix1);
242 
243         System.out.printf("\nByte #%d at line #%d word #%d:\n", location, line_num, wordOffset);
244         System.out.printf("%s: %s\n%s: %s\n", mapName0, diff0, mapName1, diff1);
245 
246         f0.close();
247         f1.close();
248     }
249 
250     static String print_diff_helper(BufferedReader b, int wordOffset, ArrayDeque<String> prefix) throws Exception {
251         int start = LINE_OFFSET + WORD_LEN * wordOffset;
252         int end = start + WORD_LEN;
253         String line = prefix.getLast();
254         String diff = line.substring(start, end);
255 
256         // Print previous lines
257         for (String s : prefix) {
258             if (s.equals(line)) {
259                 System.out.println(">" + s);
260             } else {
261                 System.out.println(" " + s);
262             }
263         }
264 
265         // Print extra lines
266         for (int i = 0; i < NUM_LINES / 2; i++) {
267             System.out.println(" " + b.readLine());
268         }
269         return diff;
270     }
271 }