1 /*
   2  * Copyright (c) 2014, 2015, 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 
  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             "-XX:+VerifyAfterGC" // Always verify after GC
 194         };
 195 
 196         ArrayList<String> args = new ArrayList<String>();
 197         args.addAll(Arrays.asList(defaultArgs));
 198         args.addAll(Arrays.asList(extraArgs));
 199 
 200         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(args.toArray(new String[args.size()]));
 201         OutputAnalyzer output = new OutputAnalyzer(pb.start());
 202         System.err.println(output.getStderr());
 203         System.out.println(output.getStdout());
 204         return output;
 205     }
 206 
 207     private static class DeduplicationTest {
 208         public static void main(String[] args) {
 209             System.out.println("Begin: DeduplicationTest");
 210 
 211             final int numberOfStrings = Integer.parseUnsignedInt(args[0]);
 212             final int numberOfUniqueStrings = Integer.parseUnsignedInt(args[1]);
 213             final int ageThreshold = Integer.parseUnsignedInt(args[2]);
 214             final String gcType = args[3];
 215 
 216             ArrayList<String> list = createStrings(numberOfStrings, numberOfUniqueStrings);
 217             forceDeduplication(ageThreshold, gcType);
 218             verifyStrings(list, numberOfUniqueStrings);
 219 
 220             System.out.println("End: DeduplicationTest");
 221         }
 222 
 223         public static OutputAnalyzer run(int numberOfStrings, int ageThreshold, String gcType, String... extraArgs) throws Exception {
 224             String[] defaultArgs = new String[] {
 225                 "-XX:+UseStringDeduplication",
 226                 "-XX:StringDeduplicationAgeThreshold=" + ageThreshold,
 227                 DeduplicationTest.class.getName(),
 228                 "" + numberOfStrings,
 229                 "" + numberOfStrings / 2,
 230                 "" + ageThreshold,
 231                 gcType
 232             };
 233 
 234             ArrayList<String> args = new ArrayList<String>();
 235             args.addAll(Arrays.asList(extraArgs));
 236             args.addAll(Arrays.asList(defaultArgs));
 237 
 238             return runTest(args.toArray(new String[args.size()]));
 239         }
 240     }
 241 
 242     private static class InternedTest {
 243         public static void main(String[] args) {
 244             // This test verifies that interned strings are always
 245             // deduplicated when being interned, and never after
 246             // being interned.
 247 
 248             System.out.println("Begin: InternedTest");
 249 
 250             final int ageThreshold = Integer.parseUnsignedInt(args[0]);
 251             final String baseString = "DeduplicationTestString:" + InternedTest.class.getName();
 252 
 253             // Create duplicate of baseString
 254             StringBuilder sb1 = new StringBuilder(baseString);
 255             String dupString1 = sb1.toString();
 256             if (getValue(dupString1) == getValue(baseString)) {
 257                 throw new RuntimeException("Values should not match");
 258             }
 259 
 260             // Force baseString to be inspected for deduplication
 261             // and be inserted into the deduplication hashtable.
 262             forceDeduplication(ageThreshold, FullGC);
 263 
 264             // Wait for deduplication to occur
 265             for (int attempts = 0; attempts < 10; attempts++) {
 266                 if (getValue(dupString1) == getValue(baseString)) {
 267                     break;
 268                 }
 269                 System.out.println("Waiting...");
 270                 try {
 271                     Thread.sleep(1000);
 272                 } catch (Exception e) {
 273                     throw new RuntimeException(e);
 274                 }
 275             }
 276             if (getValue(dupString1) != getValue(baseString)) {
 277                 throw new RuntimeException("Deduplication has not occurred");
 278             }
 279 
 280             // Create a new duplicate of baseString
 281             StringBuilder sb2 = new StringBuilder(baseString);
 282             String dupString2 = sb2.toString();
 283             if (getValue(dupString2) == getValue(baseString)) {
 284                 throw new RuntimeException("Values should not match");
 285             }
 286 
 287             // Intern the new duplicate
 288             Object beforeInternedValue = getValue(dupString2);
 289             String internedString = dupString2.intern();
 290             if (internedString != dupString2) {
 291                 throw new RuntimeException("String should match");
 292             }
 293             if (getValue(internedString) != getValue(baseString)) {
 294                 throw new RuntimeException("Values should match");
 295             }
 296 
 297             // Check original value of interned string, to make sure
 298             // deduplication happened on the interned string and not
 299             // on the base string
 300             if (beforeInternedValue == getValue(baseString)) {
 301                 throw new RuntimeException("Values should not match");
 302             }
 303 
 304             System.out.println("End: InternedTest");
 305         }
 306 
 307         public static OutputAnalyzer run() throws Exception {
 308             return runTest("-Xlog:gc=debug,gc+stringdedup=trace",
 309                            "-XX:+UseStringDeduplication",
 310                            "-XX:StringDeduplicationAgeThreshold=" + DefaultAgeThreshold,
 311                            InternedTest.class.getName(),
 312                            "" + DefaultAgeThreshold);
 313         }
 314     }
 315 
 316     /*
 317      * Tests
 318      */
 319 
 320     private static final int LargeNumberOfStrings = 10000;
 321     private static final int SmallNumberOfStrings = 10;
 322 
 323     private static final int MaxAgeThreshold      = 15;
 324     private static final int DefaultAgeThreshold  = 3;
 325     private static final int MinAgeThreshold      = 1;
 326 
 327     private static final int TooLowAgeThreshold   = MinAgeThreshold - 1;
 328     private static final int TooHighAgeThreshold  = MaxAgeThreshold + 1;
 329 
 330     public static void testYoungGC() throws Exception {
 331         // Do young GC to age strings to provoke deduplication
 332         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
 333                                                       DefaultAgeThreshold,
 334                                                       YoungGC,
 335                                                       "-Xlog:gc,gc+stringdedup=trace");
 336         output.shouldNotContain("Full GC");
 337         output.shouldContain("Pause Young (G1 Evacuation Pause)");
 338         output.shouldContain("Concurrent String Deduplication");
 339         output.shouldContain("Deduplicated:");
 340         output.shouldHaveExitValue(0);
 341     }
 342 
 343     public static void testFullGC() throws Exception {
 344         // Do full GC to age strings to provoke deduplication
 345         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
 346                                                       DefaultAgeThreshold,
 347                                                       FullGC,
 348                                                       "-Xlog:gc,gc+stringdedup=trace");
 349         output.shouldNotContain("Pause Young (G1 Evacuation Pause)");
 350         output.shouldContain("Full GC");
 351         output.shouldContain("Concurrent String Deduplication");
 352         output.shouldContain("Deduplicated:");
 353         output.shouldHaveExitValue(0);
 354     }
 355 
 356     public static void testTableResize() throws Exception {
 357         // Test with StringDeduplicationResizeALot
 358         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
 359                                                       DefaultAgeThreshold,
 360                                                       YoungGC,
 361                                                       "-Xlog:gc,gc+stringdedup=trace",
 362                                                       "-XX:+StringDeduplicationResizeALot");
 363         output.shouldContain("Concurrent String Deduplication");
 364         output.shouldContain("Deduplicated:");
 365         output.shouldNotContain("Resize Count: 0");
 366         output.shouldHaveExitValue(0);
 367     }
 368 
 369     public static void testTableRehash() throws Exception {
 370         // Test with StringDeduplicationRehashALot
 371         OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings,
 372                                                       DefaultAgeThreshold,
 373                                                       YoungGC,
 374                                                       "-Xlog:gc,gc+stringdedup=trace",
 375                                                       "-XX:+StringDeduplicationRehashALot");
 376         output.shouldContain("Concurrent String Deduplication");
 377         output.shouldContain("Deduplicated:");
 378         output.shouldNotContain("Rehash Count: 0");
 379         output.shouldNotContain("Hash Seed: 0x0");
 380         output.shouldHaveExitValue(0);
 381     }
 382 
 383     public static void testAgeThreshold() throws Exception {
 384         OutputAnalyzer output;
 385 
 386         // Test with max age theshold
 387         output = DeduplicationTest.run(SmallNumberOfStrings,
 388                                        MaxAgeThreshold,
 389                                        YoungGC,
 390                                        "-Xlog:gc,gc+stringdedup=trace");
 391         output.shouldContain("Concurrent String Deduplication");
 392         output.shouldContain("Deduplicated:");
 393         output.shouldHaveExitValue(0);
 394 
 395         // Test with min age theshold
 396         output = DeduplicationTest.run(SmallNumberOfStrings,
 397                                        MinAgeThreshold,
 398                                        YoungGC,
 399                                        "-Xlog:gc,gc+stringdedup=trace");
 400         output.shouldContain("Concurrent String Deduplication");
 401         output.shouldContain("Deduplicated:");
 402         output.shouldHaveExitValue(0);
 403 
 404         // Test with too low age threshold
 405         output = DeduplicationTest.run(SmallNumberOfStrings,
 406                                        TooLowAgeThreshold,
 407                                        YoungGC);
 408         output.shouldContain("outside the allowed range");
 409         output.shouldHaveExitValue(1);
 410 
 411         // Test with too high age threshold
 412         output = DeduplicationTest.run(SmallNumberOfStrings,
 413                                        TooHighAgeThreshold,
 414                                        YoungGC);
 415         output.shouldContain("outside the allowed range");
 416         output.shouldHaveExitValue(1);
 417     }
 418 
 419     public static void testPrintOptions() throws Exception {
 420         OutputAnalyzer output;
 421 
 422         // Test without -Xlog:gc
 423         output = DeduplicationTest.run(SmallNumberOfStrings,
 424                                        DefaultAgeThreshold,
 425                                        YoungGC);
 426         output.shouldNotContain("Concurrent String Deduplication");
 427         output.shouldNotContain("Deduplicated:");
 428         output.shouldHaveExitValue(0);
 429 
 430         // Test with -Xlog:gc+stringdedup
 431         output = DeduplicationTest.run(SmallNumberOfStrings,
 432                                        DefaultAgeThreshold,
 433                                        YoungGC,
 434                                        "-Xlog:gc+stringdedup");
 435         output.shouldContain("Concurrent String Deduplication");
 436         output.shouldNotContain("Deduplicated:");
 437         output.shouldHaveExitValue(0);
 438     }
 439 
 440     public static void testInterned() throws Exception {
 441         // Test that interned strings are deduplicated before being interned
 442         OutputAnalyzer output = InternedTest.run();
 443         output.shouldHaveExitValue(0);
 444     }
 445 }