/**
* Copyright 2012 Alexey Ragozin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gridkit.benchmark.gc;
import java.io.IOException;
import java.io.Serializable;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
/**
* @author Alexey Ragozin (alexey.ragozin@gmail.com)
*/
@SuppressWarnings("restriction")
@Parameters(commandDescriptionKey="ygc", commandDescription = "Young GC benchmark")
public class YoungGCPauseBenchmark {
public enum DataMode {
STRING,
LONG,
INT,
CONST,
}
private Random random = new Random(0);
private Map<Integer, Object> maps[];
@Parameter(names = {"-es", "--entry-size"}, description = "Memory size of entry")
int entrySize = -1;
@Parameter(names = {"-h", "--head-room"}, description = "Reserved portion of old space in MiB")
int headRoom = 256;
@Parameter(names = {"-m", "--data-mode"}, description = "Type of garbage data")
DataMode mode = DataMode.STRING;
@Parameter(names = {"-l", "--string-len"}, description = "Average length of string")
int stringLen = 64;
@Parameter(names = {"-d", "--dry-mode"}, description = "After filling old space, test will stop dirting old space. This mode should exclude dirty page scanning from measurement.")
boolean dryMode = false;
@Parameter(names = {"--max-time"}, description = "Benchmark time limit (sec)")
int maxTime = 60;
@Parameter(names = {"--max-old"}, description = "Number of old collections to benchmark")
int maxOld = -1;
@Parameter(names = {"--max-young"}, description = "Number of young collections to benchmark")
int maxYoung = -1;
@Parameter(names = {"--min-time"}, description = "Minimum benchmark time limit (sec)")
int minTime = -1;
@Parameter(names = {"--min-old"}, description = "Minimum number of old collections to measure")
int minOld = -1;
@Parameter(names = {"--min-young"}, description = "Minimum number of young collections to measure")
int minYoung = -1;
@Parameter(names = {"-r", "--overide-rate"}, description = "Chance that new object would be put to the map (otherwise existing would be reiserted)")
double overrideRate = 0.1;
@Parameter(names = {"-e", "--print-events"}, description = "Print GC events to console")
boolean printEvents = false;
private int count;
private com.sun.management.GarbageCollectorMXBean oldGcMBean;
private GarbageCollectorMXBean youngGcMBean;
private MemoryPoolMXBean oldMemPool;
private double activeOverrideRate = 1.1;
private static char[] STRING_TEMPLATE;
private static List<String> OLD_POOLS = Arrays.asList("Tenured Gen", "PS Old Gen", "CMS Old Gen", "G1 Old Gen", "Old Space");
private static List<String> CONC_MODE = Arrays.asList("CMS Old Gen", "G1 Old Gen");
private boolean concurentMode;
private int align(int size, int al) {
return (size + al - 1) & (~(al - 1));
}
public TestResult benchmark() throws IOException {
STRING_TEMPLATE = new char[stringLen];
if (mode == DataMode.CONST) {
overrideRate = 1.1d;
}
System.out.println("Java: " + System.getProperty("java.version") + " VM: " + System.getProperty("java.vm.version"));
System.out.println("Data model: " + Integer.getInteger("sun.arch.data.model"));
long tenuredSize = Runtime.getRuntime().maxMemory();
concurentMode = false;
// getting more accurate data
for(MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
if (CONC_MODE.contains(bean.getName())) {
concurentMode = true;
}
if (OLD_POOLS.contains(bean.getName())) {
tenuredSize = bean.getUsage().getMax();
if (tenuredSize < 0) {
tenuredSize = bean.getUsage().getCommitted();
}
System.out.println("Exact old space size is " + (tenuredSize) + " bytes");
oldMemPool = bean;
break;
}
}
beans:
for(GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
for(String pool: bean.getMemoryPoolNames()) {
if (OLD_POOLS.contains(pool)) {
oldGcMBean = (com.sun.management.GarbageCollectorMXBean) bean;
continue beans;
}
else {
}
}
youngGcMBean = bean;
}
boolean entrySizeAdjust = false;
if (entrySize <= 0) {
entrySizeAdjust = true;
System.out.println("Estimating entry memory footprint ...");
System.gc();
long used = getOldSpaceUsed();
int testSize = 100000;
initMaps(testSize);
while(size() < testSize) {
populateMap(concurentMode, testSize);
}
System.gc();
long footPrint = getOldSpaceUsed() - used;
entrySize = align((int) (footPrint / testSize), 32);
System.out.println("Entry footprint: " + entrySize);
maps = null;
}
System.gc();
long oldSpaceUsed = getOldSpaceUsed();
long freeTenured = tenuredSize - getOldSpaceUsed();
calculateCount(freeTenured);
if (concurentMode) {
System.out.println("Concurent mode is enabled");
System.out.println("Available old space: " + (freeTenured >> 20) + "MiB");
}
else {
System.out.println("Available old space: " + (freeTenured >> 20) + "MiB (-" + headRoom + " MiB)");
}
if (count < 0) {
System.out.println("Heap size is too small, increase heap size or reduce headroom");
return null;
}
System.out.println("Young space collector: " + youngGcMBean.getName());
System.out.println("Old space collector: " + oldGcMBean.getName());
System.out.println("Populating - " + count);
initMaps(count);
int n = 0;
if (entrySizeAdjust) {
int targetSize = 4 * count / 5;
while(size() < targetSize) {
populateMap(concurentMode, count);
n++;
}
System.gc();
System.gc();
long footPrint = getOldSpaceUsed() - oldSpaceUsed;
entrySize = align((int) (footPrint / size()), 32);
System.out.println("Adjusted entry footprint: " + entrySize);
calculateCount(freeTenured);
}
while(size() < count) {
populateMap(concurentMode, count);
n++;
}
if (concurentMode) {
while(--n > 0) {
processMap(false);
}
}
// Let's wait for at least one major collection to complete
if (!oldGcMBean.getName().startsWith("G1")) {
// in G1 incremental collections are not treated as full
// so we have to ignore this
if (oldGcMBean != null) {
long c = oldGcMBean.getCollectionCount();
while(c == oldGcMBean.getCollectionCount()) {
processMap(false);
}
}
}
System.out.println("Size: " + size());
if (!dryMode) {
System.out.println("Processing ... ");
}
else {
System.out.println("Processing ... (DRY MODE ENABLED)");
}
StringBuffer sb = new StringBuffer();
sb.append("Limits:");
if (maxTime > 0) {
sb.append(" ").append(maxTime + " sec");
}
if (maxOld > 0) {
sb.append(" ").append(maxOld + " old collections");
}
if (maxYoung > 0) {
sb.append(" ").append(maxYoung + " young collections");
}
System.out.println(sb.toString());
activeOverrideRate = overrideRate;
YoungGcTimeTracker tracker = new YoungGcTimeTracker();
tracker.init();
// start count down here
long startTime = System.currentTimeMillis();
long oldC = oldGcMBean.getCollectionCount();
long youngC = youngGcMBean.getCollectionCount();
while(true) {
processMap(dryMode);
tracker.probe();
if (maxOld > 0) {
if (oldGcMBean.getCollectionCount() - oldC > maxOld) {
break;
}
}
if (maxYoung > 0) {
if (youngGcMBean.getCollectionCount() - youngC > maxYoung) {
break;
}
}
if (maxTime > 0 && (System.currentTimeMillis() > (startTime + TimeUnit.SECONDS.toMillis(maxTime)))) {
break;
}
}
System.out.println("Benchmark complete");
return tracker.result();
}
private long getOldSpaceUsed() {
long usage = oldMemPool.getUsage().getUsed();
if (usage < 0) {
return oldGcMBean.getLastGcInfo().getMemoryUsageAfterGc().get(oldMemPool.getName()).getUsed();
}
else {
return usage;
}
//return jstatLong("sun.gc.generation.1.space.0.used");
}
private void calculateCount(long tenuredSize) {
count = (int) ((tenuredSize - (headRoom << 20)) / entrySize);
if (concurentMode) {
count /= 2;
}
}
@SuppressWarnings("unchecked")
private void initMaps(int entryCount) {
maps = new Map[(entryCount + 200000 - 1) / 200000];
for(int i = 0; i != maps.length; ++i) {
maps[i] = new HashMap<Integer, Object>(250000 >> 8);
}
}
private void processMap(boolean dry) {
boolean remove = size() > 1.01 * count;
for(int i = 0; i != 1000; ++i) {
if ((remove) && random.nextBoolean()) {
if (dry) {
dryRemoveRandom(count);
}
else {
removeRandom(count);
}
}
else {
if (dry) {
dryPutRandom(count);
}
else {
putRandom(count);
}
}
}
}
private void populateMap(boolean concurentMode, int count) {
for(int i = 0; i != 1000; ++i) {
putRandom(count);
if (concurentMode & random.nextInt(10) > 7) {
removeRandom(count);
}
}
}
@SuppressWarnings("rawtypes")
private int size() {
int size = 0;
for(Map map: maps) {
size += map.size();
}
return size;
}
private Object newObject() {
switch (mode) {
case STRING: return new String(STRING_TEMPLATE);
case INT: return new Integer(random.nextInt());
case LONG: return new Long(random.nextInt());
case CONST: return this;
}
return null;
}
private void putRandom(int count) {
int key = random.nextInt(2 * count);
if (Math.abs(random.nextDouble()) < activeOverrideRate) {
Object val = newObject();
maps[key % maps.length].put(new Integer(key), val);
}
else {
Integer ik = new Integer(key);
Object v = maps[key % maps.length].get(ik);
if (v != null) {
maps[key % maps.length].put(ik, v);
}
}
}
private void dryPutRandom(int count) {
int key = random.nextInt(2 * count);
Object val = newObject();
val.equals(maps[key % maps.length].get(key));
}
private void removeRandom(int count) {
int key = random.nextInt(2 * count);
maps[key % maps.length].remove(key);
}
private void dryRemoveRandom(int count) {
int key = random.nextInt(2 * count);
maps[key % maps.length].get(key);
}
private class YoungGcTimeTracker {
private long totalTime = 0;
private long evenCount = 0;
private double squareTotal = 0;
private long lastTime;
private long lastYoungCount;
private long lastOldCount;
public void init() {
while(true) {
long ygc = youngGcMBean.getCollectionCount();
long ogc = oldGcMBean.getCollectionCount();
long yt = youngGcMBean.getCollectionTime();
if (youngGcMBean.getCollectionCount() == ygc
|| oldGcMBean.getCollectionCount() == ogc) {
lastTime = yt;
lastYoungCount = ygc;
lastOldCount = ogc;
break;
}
}
}
public void probe() {
while(true) {
long ygc = youngGcMBean.getCollectionCount();
if (ygc == lastYoungCount) {
return;
}
long ogc = oldGcMBean.getCollectionCount();
long yt = youngGcMBean.getCollectionTime();
if (youngGcMBean.getCollectionCount() == ygc
|| oldGcMBean.getCollectionCount() == ogc) {
long ycd = ygc - lastYoungCount;
long ocd = ogc - lastOldCount;
long td = yt - lastTime;
lastYoungCount = ygc;
lastOldCount = ogc;
lastTime = yt;
if (!concurentMode && ocd > 0) {
// ignoring young part of full gc
ycd -= ocd;
}
if (ycd > 0) {
totalTime += td;
evenCount += ycd;
double avg = ((double)td)/ycd;
squareTotal += ycd * avg * avg;
if (printEvents) {
StringBuilder sb = new StringBuilder();
sb.append("Young GC (" + ycd + " events), total time: " + td + "ms, (Old events: " + ogc + ")");
System.out.println(sb);
}
}
break;
}
}
}
public TestResult result() {
TestResult result = new TestResult();
result.totalSquareTime = squareTotal;
result.totalTime = totalTime;
result.youngGcCount = evenCount;
return result;
}
}
public static class TestResult implements Serializable {
private static final long serialVersionUID = 20130518L;
public long totalTime;
public double totalSquareTime;
public long youngGcCount;
public double getAverage() {
double avg = ((double)totalTime) / youngGcCount;
return avg;
}
public double getStdDev() {
double avg = ((double)totalTime) / youngGcCount;
double stdDev = Math.sqrt((totalSquareTime / youngGcCount) - (avg * avg));
return stdDev;
}
public String toString() {
double avg = ((double)totalTime) / youngGcCount;
double stdDev = Math.sqrt((totalSquareTime / youngGcCount) - (avg * avg));
return String.format("%f [%f] ms", avg, stdDev);
}
}
}