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         }
 78 
 79         String baseArchive = dump(baseArgs);
 80         File baseArchiveFile = new File(baseArchive + ".jsa");
 81         HEADER_SIZE = CDSArchiveUtils.fileHeaderSize(baseArchiveFile);
 82 
 83         // (1) Dump with the same args. Should produce the same archive.
 84         String baseArchive2 = dump(baseArgs);
 85         compare(baseArchive, baseArchive2, baseArchiveFile);
 86 
 87         // (2) This will cause the archive to be relocated during dump time. We should
 88         //     still get the same bits. This simulates relocation that happens when
 89         //     Address Space Layout Randomization prevents the archive space to
 90         //     be mapped at the default location.
 91         String relocatedArchive = dump(baseArgs, "-XX:+UnlockDiagnosticVMOptions", "-XX:ArchiveRelocationMode=1");
 92         compare(baseArchive, relocatedArchive, baseArchiveFile);
 93     }
 94 
 95     static int id = 0;
 96     static String dump(ArrayList<String> args, String... more) throws Exception {
 97         String logName = "SharedArchiveFile" + (id++);
 98         String archiveName = logName + ".jsa";
 99         String mapName = logName + ".map";
100         CDSOptions opts = (new CDSOptions())
101             .addPrefix("-Xint") // Override any -Xmixed/-Xcomp flags from jtreg -vmoptions
102             .addPrefix("-Xlog:cds=debug,gc=debug")
103             .addPrefix("-Xlog:cds+map*=trace:file=" + mapName + ":none:filesize=0")
104             .setArchiveName(archiveName)
105             .addSuffix(args)
106             .addSuffix(more);
107         CDSTestUtils.createArchiveAndCheck(opts);
108 
109         return logName;
110     }
111 
112     static void compare(String file0, String file1, File archiveFile) throws Exception {
113         byte[] buff0 = new byte[4096];
114         byte[] buff1 = new byte[4096];
115         try (FileInputStream in0 = new FileInputStream(file0 + ".jsa");
116              FileInputStream in1 = new FileInputStream(file1 + ".jsa")) {
117             int total = 0;
118             while (true) {
119                 int n0 = read(in0, buff0);
120                 int n1 = read(in1, buff1);
121                 if (n0 != n1) {
122                     throw new RuntimeException("File contents (file sizes?) are different after " + total + " bytes; n0 = "
123                                                + n0 + ", n1 = " + n1);
124                 }
125                 if (n0 == 0) {
126                     System.out.println("File contents are the same: " + total + " bytes");
127                     break;
128                 }
129                 for (int i = 0; i < n0; i++) {
130                     byte b0 = buff0[i];
131                     byte b1 = buff1[i];
132                     if (b0 != b1) {
133                         // The checksums are stored in the header so it should be skipped
134                         // since we want to see the first meaningful diff between the archives
135                         if (total + i > HEADER_SIZE) {
136                             print_diff(file0 + ".map", file1 + ".map", archiveFile, total + i);
137                             throw new RuntimeException("File content different at byte #" + (total + i) + ", b0 = " + b0 + ", b1 = " + b1);
138                         }
139                     }
140                 }
141                 total += n0;
142             }
143         }
144     }
145 
146     static int read(FileInputStream in, byte[] buff) throws IOException {
147         int total = 0;
148         while (total < buff.length) {
149             int n = in.read(buff, total, buff.length - total);
150             if (n <= 0) {
151                 return total;
152             }
153             total += n;
154         }
155 
156         return total;
157     }
158 
159     // CDS map file doesn't print the alignment bytes so they need to be considered
160     // when mapping the byte number in the archive to the word in the map file
161     static int archiveByteToMapWord(File archiveFile, int location) throws Exception {
162         int totalSize = 0;
163         int word = location;
164 
165         long len = HEADER_SIZE;
166         long aligned = CDSArchiveUtils.fileHeaderSizeAligned(archiveFile);
167         for (int i = 0; i < CDSArchiveUtils.num_regions(); i++) {
168             if (i != 0) {
169                 len = CDSArchiveUtils.usedRegionSize(archiveFile, i);
170                 aligned = CDSArchiveUtils.usedRegionSizeAligned(archiveFile, i);
171             }
172             totalSize += len;
173             if (location > totalSize) {
174                 word -= (aligned - len - 16);
175             }
176         }
177         return word/8;
178     }
179 
180     // Read the mapfile and print out the lines associated with the location
181     static void print_diff(String mapName0, String mapName1, File archiveFile, int location) throws Exception {
182         FileReader f0 = new FileReader(mapName0);
183         BufferedReader b0 = new BufferedReader(f0);
184 
185         FileReader f1 = new FileReader(mapName1);
186         BufferedReader b1 = new BufferedReader(f1);
187 
188         int word = archiveByteToMapWord(archiveFile, location);
189         int wordOffset = word % 4; // Each line in the map file prints four words
190         String region = "";
191 
192         // Skip header text and go to first line
193         for (int i = 0; i < HEADER_LEN; i++) {
194             b0.readLine();
195             b1.readLine();
196         }
197 
198         int line_num = HEADER_LEN;
199         String s0 = "";
200         String s1 = "";
201         int count = 0;
202 
203         // Store lines before and including the diff
204         ArrayDeque<String> prefix0 = new ArrayDeque<String>();
205         ArrayDeque<String> prefix1 = new ArrayDeque<String>();
206 
207         // A line may contain 1-4 words so we iterate by word
208         do {
209             s0 = b0.readLine();
210             s1 = b1.readLine();
211             line_num++;
212 
213             if (prefix0.size() >= NUM_LINES / 2 + 1) {
214                 prefix0.removeFirst();
215                 prefix1.removeFirst();
216             }
217             prefix0.addLast(s0);
218             prefix1.addLast(s1);
219 
220             // Skip lines with headers when counting words e.g.
221             // [rw region          0x0000000800000000 - 0x00000008005a1f88   5906312 bytes]
222             // or
223             // 0x0000000800000b28: @@ TypeArrayU1       16
224             if (!s0.contains(": @@") && !s0.contains("bytes]")) {
225                 int words = (s0.length() - LINE_OFFSET - 70) / 8;
226                 count += words;
227             } else if (s0.contains("bytes]")) {
228                 region = s0;
229             }
230         } while (count < word);
231 
232         // Print the diff with the region name above it
233         System.out.println("[First diff: map file #1 (" + mapName0 + ")]");
234         System.out.println(region);
235         String diff0 = print_diff_helper(b0, wordOffset, prefix0);
236 
237         System.out.println("\n[First diff: map file #2 (" + mapName1 + ")]");
238         System.out.println(region);
239         String diff1 = print_diff_helper(b1, wordOffset, prefix1);
240 
241         System.out.printf("\nByte #%d at line #%d word #%d:\n", location, line_num, wordOffset);
242         System.out.printf("%s: %s\n%s: %s\n", mapName0, diff0, mapName1, diff1);
243 
244         f0.close();
245         f1.close();
246     }
247 
248     static String print_diff_helper(BufferedReader b, int wordOffset, ArrayDeque<String> prefix) throws Exception {
249         int start = LINE_OFFSET + WORD_LEN * wordOffset;
250         int end = start + WORD_LEN;
251         String line = prefix.getLast();
252         String diff = line.substring(start, end);
253 
254         // Print previous lines
255         for (String s : prefix) {
256             if (s.equals(line)) {
257                 System.out.println(">" + s);
258             } else {
259                 System.out.println(" " + s);
260             }
261         }
262 
263         // Print extra lines
264         for (int i = 0; i < NUM_LINES / 2; i++) {
265             System.out.println(" " + b.readLine());
266         }
267         return diff;
268     }
269 }