/*
 * 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 Long 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;
    }
}

@@ -4985,32 +4985,33 @@
 double median = 
+(
 entry.getValueIt
@@ -5043,32 +5043,33 @@
 ueIteratedFrom()
+)
  / 2.0;%0A        
@@ -5572,16 +5572,17 @@
 edian = 
+(
 entry.ge
@@ -5630,16 +5630,17 @@
 edFrom()
+)
  / 2.0;%0A

Fix incorrect bucket median computation