1/*
2 * Copyright (c) 2014, 2016, 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
28import java.lang.management.*;
29import java.lang.reflect.*;
30import java.security.*;
31import java.util.*;
32import jdk.test.lib.process.ProcessTools;
33import jdk.test.lib.process.OutputAnalyzer;
34import sun.misc.*;
35
36class 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
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
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
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
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
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
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     */
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
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 {
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
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 {
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
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
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 (G1 Evacuation Pause)");
339        output.shouldContain("Concurrent String Deduplication");
340        output.shouldContain("Deduplicated:");
341        output.shouldHaveExitValue(0);
342    }
343
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 (G1 Evacuation Pause)");
351        output.shouldContain("Full GC");
352        output.shouldContain("Concurrent String Deduplication");
353        output.shouldContain("Deduplicated:");
354        output.shouldHaveExitValue(0);
355    }
356
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
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
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
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
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