1 /*
2  * Copyright (c) 2014, 2018, 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  * Common code for string deduplication tests
26  */
27 
28 import java.lang.management.*;
29 import java.lang.reflect.*;
30 import java.security.*;
31 import java.util.*;
32 import jdk.test.lib.process.ProcessTools;
33 import jdk.test.lib.process.OutputAnalyzer;
34 import sun.misc.*;
35 
36 class TestStringDeduplicationTools {
37     private static final String YoungGC = "YoungGC";
38     private static final String FullGC  = "FullGC";
39 
40     private static final int Xmn = 50;  // MB
41     private static final int Xms = 100; // MB
42     private static final int Xmx = 100; // MB
43     private static final int MB = 1024 * 1024;
44     private static final int StringLength = 50;
45 
46     private static Field valueField;
47     private static Unsafe unsafe;
48     private static byte[] dummy;
49 
50     static {
51         try {
52             Field field = Unsafe.class.getDeclaredField("theUnsafe");
53             field.setAccessible(true);
54             unsafe = (Unsafe)field.get(null);
55 
56             valueField = String.class.getDeclaredField("value");
57             valueField.setAccessible(true);
58         } catch (Exception e) {
59             throw new RuntimeException(e);
60         }
61     }
62 
getValue(String string)63     private static Object getValue(String string) {
64         try {
65             return valueField.get(string);
66         } catch (Exception e) {
67             throw new RuntimeException(e);
68         }
69     }
70 
doFullGc(int numberOfTimes)71     private static void doFullGc(int numberOfTimes) {
72         for (int i = 0; i < numberOfTimes; i++) {
73             System.out.println("Begin: Full GC " + (i + 1) + "/" + numberOfTimes);
74             System.gc();
75             System.out.println("End: Full GC " + (i + 1) + "/" + numberOfTimes);
76         }
77     }
78 
doYoungGc(int numberOfTimes)79     private static void doYoungGc(int numberOfTimes) {
80         // Provoke at least numberOfTimes young GCs
81         final int objectSize = 128;
82         final int maxObjectInYoung = (Xmn * MB) / objectSize;
83         for (int i = 0; i < numberOfTimes; i++) {
84             System.out.println("Begin: Young GC " + (i + 1) + "/" + numberOfTimes);
85             for (int j = 0; j < maxObjectInYoung + 1; j++) {
86                 dummy = new byte[objectSize];
87             }
88             System.out.println("End: Young GC " + (i + 1) + "/" + numberOfTimes);
89         }
90     }
91 
forceDeduplication(int ageThreshold, String gcType)92     private static void forceDeduplication(int ageThreshold, String gcType) {
93         // Force deduplication to happen by either causing a FullGC or a YoungGC.
94         // We do several collections to also provoke a situation where the the
95         // deduplication thread needs to yield while processing the queue. This
96         // also tests that the references in the deduplication queue are adjusted
97         // accordingly.
98         if (gcType.equals(FullGC)) {
99             doFullGc(3);
100         } else {
101             doYoungGc(ageThreshold + 3);
102         }
103     }
104 
generateString(int id)105     private static String generateString(int id) {
106         StringBuilder builder = new StringBuilder(StringLength);
107 
108         builder.append("DeduplicationTestString:" + id + ":");
109 
110         while (builder.length() < StringLength) {
111             builder.append('X');
112         }
113 
114         return builder.toString();
115     }
116 
createStrings(int total, int unique)117     private static ArrayList<String> createStrings(int total, int unique) {
118         System.out.println("Creating strings: total=" + total + ", unique=" + unique);
119         if (total % unique != 0) {
120             throw new RuntimeException("Total must be divisible by unique");
121         }
122 
123         ArrayList<String> list = new ArrayList<String>(total);
124         for (int j = 0; j < total / unique; j++) {
125             for (int i = 0; i < unique; i++) {
126                 list.add(generateString(i));
127             }
128         }
129 
130         return list;
131     }
132 
133     /**
134      * Verifies that the given list contains expected number of unique strings.
135      * It's possible that deduplication hasn't completed yet, so the method
136      * will perform several attempts to check with a little pause between.
137      * The method throws RuntimeException to signal that verification failed.
138      *
139      * @param list strings to check
140      * @param uniqueExpected expected number of unique strings
141      * @throws RuntimeException if check fails
142      */
verifyStrings(ArrayList<String> list, int uniqueExpected)143     private static void verifyStrings(ArrayList<String> list, int uniqueExpected) {
144         boolean passed = false;
145         for (int attempts = 0; attempts < 10; attempts++) {
146             // Check number of deduplicated strings
147             ArrayList<Object> unique = new ArrayList<Object>(uniqueExpected);
148             for (String string: list) {
149                 Object value = getValue(string);
150                 boolean uniqueValue = true;
151                 for (Object obj: unique) {
152                     if (obj == value) {
153                         uniqueValue = false;
154                         break;
155                     }
156                 }
157 
158                 if (uniqueValue) {
159                     unique.add(value);
160                 }
161             }
162 
163             System.out.println("Verifying strings: total=" + list.size() +
164                                ", uniqueFound=" + unique.size() +
165                                ", uniqueExpected=" + uniqueExpected);
166 
167             if (unique.size() == uniqueExpected) {
168                 System.out.println("Deduplication completed (as fast as " + attempts + " iterations)");
169                 passed = true;
170                 break;
171             } else {
172                 System.out.println("Deduplication not completed, waiting...");
173                 // Give the deduplication thread time to complete
174                 try {
175                     Thread.sleep(1000);
176                 } catch (Exception e) {
177                     throw new RuntimeException(e);
178                 }
179             }
180         }
181         if (!passed) {
182             throw new RuntimeException("String verification failed");
183         }
184     }
185 
runTest(String... extraArgs)186     private static OutputAnalyzer runTest(String... extraArgs) throws Exception {
187         String[] defaultArgs = new String[] {
188             "-Xmn" + Xmn + "m",
189             "-Xms" + Xms + "m",
190             "-Xmx" + Xmx + "m",
191             "-XX:+UseG1GC",
192             "-XX:+UnlockDiagnosticVMOptions",
193             "--add-opens=java.base/java.lang=ALL-UNNAMED",
194             "-XX:+VerifyAfterGC" // Always verify after GC
195         };
196 
197         ArrayList<String> args = new ArrayList<String>();
198         args.addAll(Arrays.asList(defaultArgs));
199         args.addAll(Arrays.asList(extraArgs));
200 
201         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(args.toArray(new String[args.size()]));
202         OutputAnalyzer output = new OutputAnalyzer(pb.start());
203         System.err.println(output.getStderr());
204         System.out.println(output.getStdout());
205         return output;
206     }
207 
208     private static class DeduplicationTest {
main(String[] args)209         public static void main(String[] args) {
210             System.out.println("Begin: DeduplicationTest");
211 
212             final int numberOfStrings = Integer.parseUnsignedInt(args[0]);
213             final int numberOfUniqueStrings = Integer.parseUnsignedInt(args[1]);
214             final int ageThreshold = Integer.parseUnsignedInt(args[2]);
215             final String gcType = args[3];
216 
217             ArrayList<String> list = createStrings(numberOfStrings, numberOfUniqueStrings);
218             forceDeduplication(ageThreshold, gcType);
219             verifyStrings(list, numberOfUniqueStrings);
220 
221             System.out.println("End: DeduplicationTest");
222         }
223 
run(int numberOfStrings, int ageThreshold, String gcType, String... extraArgs)224         public static OutputAnalyzer run(int numberOfStrings, int ageThreshold, String gcType, String... extraArgs) throws Exception {
225             String[] defaultArgs = new String[] {
226                 "-XX:+UseStringDeduplication",
227                 "-XX:StringDeduplicationAgeThreshold=" + ageThreshold,
228                 DeduplicationTest.class.getName(),
229                 "" + numberOfStrings,
230                 "" + numberOfStrings / 2,
231                 "" + ageThreshold,
232                 gcType
233             };
234 
235             ArrayList<String> args = new ArrayList<String>();
236             args.addAll(Arrays.asList(extraArgs));
237             args.addAll(Arrays.asList(defaultArgs));
238 
239             return runTest(args.toArray(new String[args.size()]));
240         }
241     }
242 
243     private static class InternedTest {
main(String[] args)244         public static void main(String[] args) {
245             // This test verifies that interned strings are always
246             // deduplicated when being interned, and never after
247             // being interned.
248 
249             System.out.println("Begin: InternedTest");
250 
251             final int ageThreshold = Integer.parseUnsignedInt(args[0]);
252             final String baseString = "DeduplicationTestString:" + InternedTest.class.getName();
253 
254             // Create duplicate of baseString
255             StringBuilder sb1 = new StringBuilder(baseString);
256             String dupString1 = sb1.toString();
257             if (getValue(dupString1) == getValue(baseString)) {
258                 throw new RuntimeException("Values should not match");
259             }
260 
261             // Force baseString to be inspected for deduplication
262             // and be inserted into the deduplication hashtable.
263             forceDeduplication(ageThreshold, FullGC);
264 
265             // Wait for deduplication to occur
266             for (int attempts = 0; attempts < 10; attempts++) {
267                 if (getValue(dupString1) == getValue(baseString)) {
268                     break;
269                 }
270                 System.out.println("Waiting...");
271                 try {
272                     Thread.sleep(1000);
273                 } catch (Exception e) {
274                     throw new RuntimeException(e);
275                 }
276             }
277             if (getValue(dupString1) != getValue(baseString)) {
278                 throw new RuntimeException("Deduplication has not occurred");
279             }
280 
281             // Create a new duplicate of baseString
282             StringBuilder sb2 = new StringBuilder(baseString);
283             String dupString2 = sb2.toString();
284             if (getValue(dupString2) == getValue(baseString)) {
285                 throw new RuntimeException("Values should not match");
286             }
287 
288             // Intern the new duplicate
289             Object beforeInternedValue = getValue(dupString2);
290             String internedString = dupString2.intern();
291             if (internedString != dupString2) {
292                 throw new RuntimeException("String should match");
293             }
294             if (getValue(internedString) != getValue(baseString)) {
295                 throw new RuntimeException("Values should match");
296             }
297 
298             // Check original value of interned string, to make sure
299             // deduplication happened on the interned string and not
300             // on the base string
301             if (beforeInternedValue == getValue(baseString)) {
302                 throw new RuntimeException("Values should not match");
303             }
304 
305             System.out.println("End: InternedTest");
306         }
307 
run()308         public static OutputAnalyzer run() throws Exception {
309             return runTest("-Xlog:gc=debug,gc+stringdedup=trace",
310                            "-XX:+UseStringDeduplication",
311                            "-XX:StringDeduplicationAgeThreshold=" + DefaultAgeThreshold,
312                            InternedTest.class.getName(),
313                            "" + DefaultAgeThreshold);
314         }
315     }
316 
317     /*
318      * Tests
319      */
320 
321     private static final int LargeNumberOfStrings = 10000;
322     private static final int SmallNumberOfStrings = 10;
323 
324     private static final int MaxAgeThreshold      = 15;
325     private static final int DefaultAgeThreshold  = 3;
326     private static final int MinAgeThreshold      = 1;
327 
328     private static final int TooLowAgeThreshold   = MinAgeThreshold - 1;
329     private static final int TooHighAgeThreshold  = MaxAgeThreshold + 1;
330 
testYoungGC()331     public static void testYoungGC() throws Exception {
332         // Do young GC to age strings to provoke deduplication
333         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
334                                                       DefaultAgeThreshold,
335                                                       YoungGC,
336                                                       "-Xlog:gc,gc+stringdedup=trace");
337         output.shouldNotContain("Full GC");
338         output.shouldContain("Pause Young (Normal) (G1 Evacuation Pause)");
339         output.shouldContain("Concurrent String Deduplication");
340         output.shouldContain("Deduplicated:");
341         output.shouldHaveExitValue(0);
342     }
343 
testFullGC()344     public static void testFullGC() throws Exception {
345         // Do full GC to age strings to provoke deduplication
346         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
347                                                       DefaultAgeThreshold,
348                                                       FullGC,
349                                                       "-Xlog:gc,gc+stringdedup=trace");
350         output.shouldNotContain("Pause Young (Normal) (G1 Evacuation Pause)");
351         output.shouldContain("Full GC");
352         output.shouldContain("Concurrent String Deduplication");
353         output.shouldContain("Deduplicated:");
354         output.shouldHaveExitValue(0);
355     }
356 
testTableResize()357     public static void testTableResize() throws Exception {
358         // Test with StringDeduplicationResizeALot
359         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
360                                                       DefaultAgeThreshold,
361                                                       YoungGC,
362                                                       "-Xlog:gc,gc+stringdedup=trace",
363                                                       "-XX:+StringDeduplicationResizeALot");
364         output.shouldContain("Concurrent String Deduplication");
365         output.shouldContain("Deduplicated:");
366         output.shouldNotContain("Resize Count: 0");
367         output.shouldHaveExitValue(0);
368     }
369 
testTableRehash()370     public static void testTableRehash() throws Exception {
371         // Test with StringDeduplicationRehashALot
372         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
373                                                       DefaultAgeThreshold,
374                                                       YoungGC,
375                                                       "-Xlog:gc,gc+stringdedup=trace",
376                                                       "-XX:+StringDeduplicationRehashALot");
377         output.shouldContain("Concurrent String Deduplication");
378         output.shouldContain("Deduplicated:");
379         output.shouldNotContain("Rehash Count: 0");
380         output.shouldNotContain("Hash Seed: 0x0");
381         output.shouldHaveExitValue(0);
382     }
383 
testAgeThreshold()384     public static void testAgeThreshold() throws Exception {
385         OutputAnalyzer output;
386 
387         // Test with max age theshold
388         output = DeduplicationTest.run(SmallNumberOfStrings,
389                                        MaxAgeThreshold,
390                                        YoungGC,
391                                        "-Xlog:gc,gc+stringdedup=trace");
392         output.shouldContain("Concurrent String Deduplication");
393         output.shouldContain("Deduplicated:");
394         output.shouldHaveExitValue(0);
395 
396         // Test with min age theshold
397         output = DeduplicationTest.run(SmallNumberOfStrings,
398                                        MinAgeThreshold,
399                                        YoungGC,
400                                        "-Xlog:gc,gc+stringdedup=trace");
401         output.shouldContain("Concurrent String Deduplication");
402         output.shouldContain("Deduplicated:");
403         output.shouldHaveExitValue(0);
404 
405         // Test with too low age threshold
406         output = DeduplicationTest.run(SmallNumberOfStrings,
407                                        TooLowAgeThreshold,
408                                        YoungGC);
409         output.shouldContain("outside the allowed range");
410         output.shouldHaveExitValue(1);
411 
412         // Test with too high age threshold
413         output = DeduplicationTest.run(SmallNumberOfStrings,
414                                        TooHighAgeThreshold,
415                                        YoungGC);
416         output.shouldContain("outside the allowed range");
417         output.shouldHaveExitValue(1);
418     }
419 
testPrintOptions()420     public static void testPrintOptions() throws Exception {
421         OutputAnalyzer output;
422 
423         // Test without -Xlog:gc
424         output = DeduplicationTest.run(SmallNumberOfStrings,
425                                        DefaultAgeThreshold,
426                                        YoungGC);
427         output.shouldNotContain("Concurrent String Deduplication");
428         output.shouldNotContain("Deduplicated:");
429         output.shouldHaveExitValue(0);
430 
431         // Test with -Xlog:gc+stringdedup
432         output = DeduplicationTest.run(SmallNumberOfStrings,
433                                        DefaultAgeThreshold,
434                                        YoungGC,
435                                        "-Xlog:gc+stringdedup");
436         output.shouldContain("Concurrent String Deduplication");
437         output.shouldNotContain("Deduplicated:");
438         output.shouldHaveExitValue(0);
439     }
440 
testInterned()441     public static void testInterned() throws Exception {
442         // Test that interned strings are deduplicated before being interned
443         OutputAnalyzer output = InternedTest.run();
444         output.shouldHaveExitValue(0);
445     }
446 }
447