content: /*
   * 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 com.google.errorprone.annotations.concurrent.GuardedBy;
  import io.airlift.log.Logger;
  import io.airlift.units.Duration;
  import jakarta.annotation.PostConstruct;
  import jakarta.annotation.PreDestroy;
  import org.HdrHistogram.Histogram;
  import org.HdrHistogram.HistogramIterationValue;
  import org.weakref.jmx.Managed;
  
  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;
      }
  }
  