package org.cache2k.benchmark.jmh;
/*
* #%L
* Benchmarks: JMH suite.
* %%
* Copyright (C) 2013 - 2017 headissue GmbH, Munich
* %%
* 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.
* #L%
*/
/*
* Copied from JMH GCProfiler. Original copyright:
*
* Copyright (c) 2005, 2013, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
import org.openjdk.jmh.infra.BenchmarkParams;
import org.openjdk.jmh.infra.IterationParams;
import org.openjdk.jmh.profile.InternalProfiler;
import org.openjdk.jmh.profile.ProfilerException;
import org.openjdk.jmh.results.AggregationPolicy;
import org.openjdk.jmh.results.IterationResult;
import org.openjdk.jmh.results.Result;
import org.openjdk.jmh.results.ScalarResult;
import org.openjdk.jmh.util.HashMultiset;
import org.openjdk.jmh.util.Multiset;
import javax.management.ListenerNotFoundException;
import javax.management.Notification;
import javax.management.NotificationEmitter;
import javax.management.NotificationListener;
import javax.management.openmbean.CompositeData;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.lang.management.ThreadMXBean;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class GcProfiler implements InternalProfiler {
private long beforeTime;
private long beforeGCCount;
private long beforeGCTime;
private HotspotAllocationSnapshot beforeAllocated;
private final NotificationListener listener;
private volatile Multiset<String> churn;
private List<Long> usedAfterGc = Collections.synchronizedList(new ArrayList<>());
private List<Long> committedAfterGc = Collections.synchronizedList(new ArrayList<>());
public GcProfiler() throws ProfilerException {
churn = new HashMultiset<String>();
NotificationListener listener;
try {
final Class<?> infoKlass = Class.forName("com.sun.management.GarbageCollectionNotificationInfo");
final Field notifNameField = infoKlass.getField("GARBAGE_COLLECTION_NOTIFICATION");
final Method infoMethod = infoKlass.getMethod("from", CompositeData.class);
final Method getGcInfo = infoKlass.getMethod("getGcInfo");
final Method getMemoryUsageBeforeGc = getGcInfo.getReturnType().getMethod("getMemoryUsageBeforeGc");
final Method getMemoryUsageAfterGc = getGcInfo.getReturnType().getMethod("getMemoryUsageAfterGc");
listener = new NotificationListener() {
@Override
public void handleNotification(Notification n, Object o) {
try {
if (n.getType().equals(notifNameField.get(null))) {
StringBuilder debugLine = new StringBuilder();
Object info = infoMethod.invoke(null, n.getUserData());
Object gcInfo = getGcInfo.invoke(info);
Map<String, MemoryUsage> mapBefore = (Map<String, MemoryUsage>) getMemoryUsageBeforeGc.invoke(gcInfo);
Map<String, MemoryUsage> mapAfter = (Map<String, MemoryUsage>) getMemoryUsageAfterGc.invoke(gcInfo);
long committed = 0;
long used = 0;
for (Map.Entry<String, MemoryUsage> entry : mapAfter.entrySet()) {
String name = entry.getKey();
MemoryUsage after = entry.getValue();
committed += after.getCommitted();
used += after.getUsed();
debugLine.append(name).append("=").append(after.getUsed()).append(", ");
MemoryUsage before = mapBefore.get(name);
long c = before.getUsed() - after.getUsed();
if (c > 0) {
churn.add(name, c);
}
}
usedAfterGc.add(used);
committedAfterGc.add(committed);
System.out.println("[GC Notification Listener] " + debugLine + "Total used=" + used + ", Total committed=" + committed);
}
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
}
};
} catch (ClassNotFoundException e) {
throw new ProfilerException(e);
} catch (NoSuchFieldException e) {
throw new ProfilerException(e);
} catch (NoSuchMethodException e) {
throw new ProfilerException(e);
}
this.listener = listener;
}
@Override
public String getDescription() {
return "GC profiling via standard MBeans";
}
@Override
public void beforeIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams) {
installHooks();
long gcTime = 0;
long gcCount = 0;
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
gcCount += bean.getCollectionCount();
gcTime += bean.getCollectionTime();
}
this.beforeGCCount = gcCount;
this.beforeGCTime = gcTime;
this.beforeAllocated = HotspotAllocationSnapshot.create();
this.beforeTime = System.nanoTime();
}
@Override
public Collection<? extends Result> afterIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams, IterationResult iResult) {
try {
Thread.sleep(567);
} catch (InterruptedException ignore) {
}
uninstallHooks();
long afterTime = System.nanoTime();
HotspotAllocationSnapshot newSnapshot = HotspotAllocationSnapshot.create();
long gcTime = 0;
long gcCount = 0;
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
gcCount += bean.getCollectionCount();
gcTime += bean.getCollectionTime();
}
List<Result<?>> results = new ArrayList<>();
if (beforeAllocated == HotspotAllocationSnapshot.EMPTY) {
results.add(new ScalarResult(Defaults.PREFIX + "gc.alloc.rate",
Double.NaN,
"MB/sec", AggregationPolicy.AVG));
} else {
long allocated = newSnapshot.subtract(beforeAllocated);
results.add(new ScalarResult(Defaults.PREFIX + "gc.alloc.rate",
(afterTime != beforeTime) ?
1.0 * allocated / 1024 / 1024 * TimeUnit.SECONDS.toNanos(1) / (afterTime - beforeTime) :
Double.NaN,
"MB/sec", AggregationPolicy.AVG));
if (allocated != 0) {
long allOps = iResult.getMetadata().getAllOps();
results.add(new ScalarResult(Defaults.PREFIX + "gc.alloc.rate.norm",
(allOps != 0) ?
1.0 * allocated / allOps :
Double.NaN,
"B/op", AggregationPolicy.AVG));
}
}
results.add(new ScalarResult(
Defaults.PREFIX + "gc.count",
gcCount - beforeGCCount,
"counts",
AggregationPolicy.SUM));
if (gcCount != beforeGCCount || gcTime != beforeGCTime) {
results.add(new ScalarResult(
Defaults.PREFIX + "gc.time",
gcTime - beforeGCTime,
"ms",
AggregationPolicy.SUM));
}
for (String space : churn.keys()) {
double churnRate = (afterTime != beforeTime) ?
1.0 * churn.count(space) * TimeUnit.SECONDS.toNanos(1) / (afterTime - beforeTime) / 1024 / 1024 :
Double.NaN;
double churnNorm = 1.0 * churn.count(space) / iResult.getMetadata().getAllOps();
String spaceName = space.replaceAll(" ", "_");
results.add(new ScalarResult(
Defaults.PREFIX + "gc.churn." + spaceName + "",
churnRate,
"MB/sec",
AggregationPolicy.AVG));
results.add(new ScalarResult(Defaults.PREFIX + "gc.churn." + spaceName + ".norm",
churnNorm,
"B/op",
AggregationPolicy.AVG));
}
if (!usedAfterGc.isEmpty()) {
Collections.sort(usedAfterGc);
long _maximumUsedAfterGc = usedAfterGc.get(usedAfterGc.size() - 1);
results.add(new ScalarResult(Defaults.PREFIX + "gc.maximumUsedAfterGc",
_maximumUsedAfterGc,
"bytes",
AggregationPolicy.AVG));
System.out.println("maximumUsedAfterGc=" + _maximumUsedAfterGc);
}
if (!committedAfterGc.isEmpty()) {
Collections.sort(committedAfterGc);
long _committedUsedAfterGc = committedAfterGc.get(committedAfterGc.size() - 1);
results.add(new ScalarResult(Defaults.PREFIX + "gc.maximumCommittedAfterGc",
_committedUsedAfterGc,
"bytes",
AggregationPolicy.AVG));
System.out.println("maximumCommittedAfterGc=" + _committedUsedAfterGc);
}
return results;
}
private boolean hooksInstalled;
public synchronized void installHooks() {
if (hooksInstalled) return;
hooksInstalled = true;
churn = new HashMultiset<String>();
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
((NotificationEmitter) bean).addNotificationListener(listener, null, null);
}
}
public synchronized void uninstallHooks() {
if (!hooksInstalled) return;
hooksInstalled = false;
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
try {
((NotificationEmitter) bean).removeNotificationListener(listener);
} catch (ListenerNotFoundException e) {
}
}
}
static class HotspotAllocationSnapshot {
public final static HotspotAllocationSnapshot EMPTY = new HotspotAllocationSnapshot(new long[0], new long[0]);
private static volatile Method GET_THREAD_ALLOCATED_BYTES;
private static volatile boolean allocationNotAvailable;
private final long[] threadIds;
private final long[] allocatedBytes;
private HotspotAllocationSnapshot(long[] threadIds, long[] allocatedBytes) {
this.threadIds = threadIds;
this.allocatedBytes = allocatedBytes;
}
/**
* Takes a snapshot of thread allocation counters.
* The method might allocate, however it is assumed that allocations made by "current thread" will
* be excluded from the result while performing {@link HotspotAllocationSnapshot#subtract(HotspotAllocationSnapshot)}
*
* @return snapshot of thread allocation counters
*/
public static HotspotAllocationSnapshot create() {
Method getBytes = getAllocatedBytesGetter();
if (getBytes == null) {
return HotspotAllocationSnapshot.EMPTY;
}
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
try {
long[] threadIds = threadMXBean.getAllThreadIds();
long[] allocatedBytes = (long[]) getBytes.invoke(threadMXBean, (Object) threadIds);
return new HotspotAllocationSnapshot(threadIds, allocatedBytes);
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
return HotspotAllocationSnapshot.EMPTY;
}
/**
* Estimates allocated bytes based on two snapshots.
* The problem is threads can come and go while performing the benchmark,
* thus we would miss allocations made in a thread that was created and died between the snapshots.
* <p/>
* <p>Current thread is intentionally excluded since it believed to execute jmh infrastructure code only.
*
* @return estimated number of allocated bytes between profiler calls
*/
public long subtract(HotspotAllocationSnapshot other) {
HashMap<Long, Integer> prevIndex = new HashMap<Long, Integer>();
for (int i = 0; i < other.threadIds.length; i++) {
long id = other.threadIds[i];
prevIndex.put(id, i);
}
long currentThreadId = Thread.currentThread().getId();
long allocated = 0;
for (int i = 0; i < threadIds.length; i++) {
long id = threadIds[i];
if (id == currentThreadId) {
continue;
}
allocated += allocatedBytes[i];
Integer prev = prevIndex.get(id);
if (prev != null) {
allocated -= other.allocatedBytes[prev];
}
}
return allocated;
}
private static Method getAllocatedBytesGetter() {
Method getBytes = GET_THREAD_ALLOCATED_BYTES;
if (getBytes != null || allocationNotAvailable) {
return getBytes;
}
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
try {
getBytes = threadMXBean.getClass().getMethod("getThreadAllocatedBytes", long[].class);
getBytes.setAccessible(true);
} catch (Throwable e) { // To avoid jmh failure in case of incompatible JDK and/or inaccessible method
getBytes = null;
allocationNotAvailable = true;
System.out.println("Allocation profiling is not available: " + e.getMessage());
}
GET_THREAD_ALLOCATED_BYTES = getBytes;
return getBytes;
}
}
static class Defaults {
static final String PREFIX = "+c2k.";
}
}