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