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