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