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 
  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 (Normal) (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 (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 
 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 }