/*
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package gc.stringdedup;
/*
* Common code for string deduplication tests
*/
import com.sun.management.GarbageCollectionNotificationInfo;
import java.lang.reflect.*;
import java.lang.management.*;
import java.util.*;
import java.util.stream.Collectors;
import javax.management.*;
import javax.management.openmbean.*;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.process.OutputAnalyzer;
import sun.misc.*;
class TestStringDeduplicationTools {
private static final String YoungGC = "YoungGC";
private static final String FullGC = "FullGC";
private static final int Xmn = 50; // MB
private static final int Xms = 100; // MB
private static final int Xmx = 100; // MB
private static final int MB = 1024 * 1024;
private static final int StringLength = 50;
private static final int LargeNumberOfStrings = 10000;
private static final int SmallNumberOfStrings = 10;
private static Field valueField;
private static Unsafe unsafe;
private static byte[] dummy;
private static String selectedGC = null;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void selectGC(String[] args) {
selectedGC = args[0];
}
private static Object getValue(String string) {
try {
return valueField.get(string);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Get system load.
*
*
*
load() ~= 1
fully loaded system, all cores are used 100%
*
load() < 1
some cpu resources are available
*
load() > 1
system is overloaded
*
*
* @return the load of the system or Optional.empty() if the load can not be determined.
*/
private static Optional systemLoad() {
OperatingSystemMXBean bean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
double average = bean.getSystemLoadAverage() / bean.getAvailableProcessors();
return (average < 0)
? Optional.empty()
: Optional.of(average);
}
private static String minMax(List> l) {
DoubleSummaryStatistics minmax = l.stream().flatMap(Optional::stream).collect(Collectors.summarizingDouble(d -> d));
return minmax.getCount() != 0
? "min: " + minmax.getMin() + ", max: " + minmax.getMax()
: "could not gather load statistics from system";
}
private static void doFullGc(int numberOfTimes) {
List> newStrings = new ArrayList>();
for (int i = 0; i < numberOfTimes; i++) {
// Create some more strings for every collection, to ensure
// there will be deduplication work that will be reported.
newStrings.add(createStrings(SmallNumberOfStrings, SmallNumberOfStrings));
System.out.println("Begin: Full GC " + (i + 1) + "/" + numberOfTimes);
System.gc();
System.out.println("End: Full GC " + (i + 1) + "/" + numberOfTimes);
}
}
private static volatile int gcCount;
private static NotificationListener listener = new NotificationListener() {
@Override
public void handleNotification(Notification n, Object o) {
if (n.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
GarbageCollectionNotificationInfo info = GarbageCollectionNotificationInfo.from((CompositeData) n.getUserData());
// Shenandoah and Z GC also report GC pauses, skip them
if (info.getGcName().startsWith("Shenandoah")) {
if ("end of GC cycle".equals(info.getGcAction())) {
gcCount++;
}
} else if (info.getGcName().startsWith("ZGC")) {
// ZGC only triggers string deduplications from major collections
if (info.getGcName().startsWith("ZGC Major") && "end of GC cycle".equals(info.getGcAction())) {
gcCount++;
}
} else if (info.getGcName().startsWith("G1")) {
if ("end of minor GC".equals(info.getGcAction())) {
gcCount++;
}
} else {
gcCount++;
}
}
}
};
private static void registerGCListener() {
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
((NotificationEmitter)bean).addNotificationListener(listener, null, null);
}
}
private static void unregisterGCListener() {
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
try {
((NotificationEmitter) bean).removeNotificationListener(listener, null, null);
} catch (Exception e) {
}
}
}
private static void doYoungGc(int numberOfTimes) {
final int objectSize = 128;
List> newStrings = new ArrayList>();
// Provoke at least numberOfTimes young GCs
gcCount = 0;
registerGCListener();
while (gcCount < numberOfTimes) {
int currentCount = gcCount;
// Create some more strings for every collection, to ensure
// there will be deduplication work that will be reported.
newStrings.add(createStrings(SmallNumberOfStrings, SmallNumberOfStrings));
System.out.println("Begin: Young GC " + (currentCount + 1) + "/" + numberOfTimes);
while (currentCount == gcCount) {
dummy = new byte[objectSize];
}
System.out.println("End: Young GC " + (currentCount + 1) + "/" + numberOfTimes);
}
unregisterGCListener();
}
private static void forceDeduplication(int ageThreshold, String gcType) {
// Force deduplication to happen by either causing a FullGC or a YoungGC.
// We do several collections to also provoke a situation where the
// deduplication thread needs to yield while processing the queue. This
// also tests that the references in the deduplication queue are adjusted
// accordingly.
if (gcType.equals(FullGC)) {
doFullGc(3);
} else {
doYoungGc(ageThreshold + 3);
}
}
private static void waitForDeduplication(String s1, String s2) {
boolean first = true;
int timeout = 10000; // 10sec in ms
int iterationWait = 100; // 100ms
List> loadHistory = new ArrayList<>();
for (int attempts = 0; attempts < (timeout / iterationWait); attempts++) {
loadHistory.add(systemLoad());
if (getValue(s1) == getValue(s2)) {
return;
}
if (first) {
System.out.println("Waiting for deduplication...");
first = false;
}
try {
Thread.sleep(iterationWait);
} catch (Exception e) {
throw new RuntimeException("Deduplication has not occurred: Thread.sleep() threw", e);
}
}
throw new RuntimeException("Deduplication has not occurred, load history: " + minMax(loadHistory));
}
private static String generateString(int id) {
StringBuilder builder = new StringBuilder(StringLength);
builder.append("DeduplicationTestString:" + id + ":");
while (builder.length() < StringLength) {
builder.append('X');
}
return builder.toString();
}
private static ArrayList createStrings(int total, int unique) {
System.out.println("Creating strings: total=" + total + ", unique=" + unique);
if (total % unique != 0) {
throw new RuntimeException("Total must be divisible by unique");
}
ArrayList list = new ArrayList(total);
for (int j = 0; j < total / unique; j++) {
for (int i = 0; i < unique; i++) {
list.add(generateString(i));
}
}
return list;
}
/**
* Verifies that the given list contains expected number of unique strings.
* It's possible that deduplication hasn't completed yet, so the method
* will perform several attempts to check with a little pause between.
* The method throws RuntimeException to signal that verification failed.
*
* @param list strings to check
* @param uniqueExpected expected number of unique strings
* @throws RuntimeException if check fails
*/
private static void verifyStrings(ArrayList list, int uniqueExpected) {
boolean passed = false;
List> loadHistory = new ArrayList<>();
for (int attempts = 0; attempts < 10; attempts++) {
loadHistory.add(systemLoad());
// Check number of deduplicated strings
ArrayList