/*
* 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 io.airlift.stats;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import io.airlift.log.Logger;
import io.airlift.units.Duration;
import org.HdrHistogram.Histogram;
import org.HdrHistogram.HistogramIterationValue;
import org.weakref.jmx.Managed;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.concurrent.GuardedBy;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
public class PauseMeter
{
private static final Logger LOG = Logger.get(PauseMeter.class);
private final long sleepNanos;
@GuardedBy("histogram")
private final Histogram histogram = new Histogram(3);
@GuardedBy("histogram")
private long totalPauseNanos;
private final Supplier<Histogram> snapshot = Suppliers.memoizeWithExpiration(this::makeSnapshot, 1, TimeUnit.SECONDS);
private final Thread thread;
// public to make it less likely for the VM to optimize it out
public volatile Object allocatedObject;
public PauseMeter()
{
this(new Duration(10, TimeUnit.MILLISECONDS));
}
public PauseMeter(Duration sleepTime)
{
this.sleepNanos = sleepTime.roundTo(TimeUnit.NANOSECONDS);
thread = new Thread(this::run, "VM Pause Meter");
thread.setDaemon(true);
}
@PostConstruct
public void start()
{
thread.start();
}
@PreDestroy
public void stop()
{
thread.interrupt();
}
private Histogram makeSnapshot()
{
synchronized (histogram) {
return histogram.copy();
}
}
private void run()
{
long shortestObservableInterval = Long.MAX_VALUE;
while (!Thread.currentThread().isInterrupted()) {
try {
long before = System.nanoTime();
TimeUnit.NANOSECONDS.sleep(sleepNanos);
// attempt to allocate an object to capture any effects due to allocation stalls
allocatedObject = new Long[] {before};
long after = System.nanoTime();
long delta = after - before;
shortestObservableInterval = Math.min(shortestObservableInterval, delta);
long pauseNanos = delta - shortestObservableInterval;
synchronized (histogram) {
histogram.recordValue(pauseNanos);
totalPauseNanos += pauseNanos;
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
catch (Throwable e) {
LOG.warn(e, "Unexpected error");
}
}
}
@Managed(description = "< 10ms")
public long getLessThan10msPauses()
{
return snapshot.get().getCountBetweenValues(0, TimeUnit.MILLISECONDS.toNanos(10));
}
@Managed(description = "10ms to 50ms")
public long get10msTo50msPauses()
{
return snapshot.get().getCountBetweenValues(TimeUnit.MILLISECONDS.toNanos(10), TimeUnit.MILLISECONDS.toNanos(50));
}
@Managed(description = "50ms to 500ms")
public long get50msTo500msPauses()
{
return snapshot.get().getCountBetweenValues(TimeUnit.MILLISECONDS.toNanos(50), TimeUnit.MILLISECONDS.toNanos(500));
}
@Managed(description = "500ms to 1s")
public long get500msTo1sPauses()
{
return snapshot.get().getCountBetweenValues(TimeUnit.MILLISECONDS.toNanos(500), TimeUnit.SECONDS.toNanos(1));
}
@Managed(description = "1s to 10s")
public long get1sTo10sPauses()
{
return snapshot.get().getCountBetweenValues(TimeUnit.SECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10));
}
@Managed(description = "10s to 1m")
public long get10sTo1mPauses()
{
return snapshot.get().getCountBetweenValues(TimeUnit.SECONDS.toNanos(10), TimeUnit.MINUTES.toNanos(1));
}
@Managed(description = "> 1m")
public long getGreaterThan1mPauses()
{
return snapshot.get().getCountBetweenValues(TimeUnit.MINUTES.toNanos(1), Long.MAX_VALUE);
}
@Managed(description = "Per-bucket counts")
public Map<Double, Long> getCounts()
{
Map<Double, Long> result = new TreeMap<>();
for (HistogramIterationValue entry : snapshot.get().logarithmicBucketValues(TimeUnit.MILLISECONDS.toNanos(1), 2)) {
double median = (entry.getValueIteratedTo() + entry.getValueIteratedFrom()) / 2.0;
result.put(round(median / (double) TimeUnit.MILLISECONDS.toNanos(1), 2), entry.getCountAddedInThisIterationStep());
}
return result;
}
@Managed(description = "Per-bucket total pause time in s")
public Map<Double, Double> getSums()
{
long previous = 0;
Map<Double, Double> result = new TreeMap<>();
for (HistogramIterationValue entry : snapshot.get().logarithmicBucketValues(TimeUnit.MILLISECONDS.toNanos(1), 2)) {
double median = (entry.getValueIteratedTo() + entry.getValueIteratedFrom()) / 2.0;
long current = entry.getTotalValueToThisValue();
result.put(round(median / TimeUnit.MILLISECONDS.toNanos(1), 2), round((current - previous) * 1.0 / TimeUnit.SECONDS.toNanos(1), 2));
previous = current;
}
return result;
}
@Managed
public double getTotalPauseSeconds()
{
synchronized (histogram) {
return totalPauseNanos * 1.0 / TimeUnit.SECONDS.toNanos(1);
}
}
private static double round(double value, int digits)
{
double scale = Math.pow(10, digits);
return Math.round(value * scale) / scale;
}
}