/**
   * Background process to harvest readings from the {@code garbageCollectorMetricSet}, which are
   * cumulative over all time. The process updates two maps: one that stores the previous values of
   * all gauges, and one that stores the non-accumulating values for those gauges (the difference
   * between the last reading and the current reading).
   */
  private void getMetricsOnSchedule() {
    Map<String, Metric> metricMap = garbageCollectorMetricSet.getMetrics();
    for (Map.Entry<String, Metric> metricEntry : metricMap.entrySet()) {
      Gauge currentGauge = (Gauge) metricEntry.getValue();
      String currentKey = metricEntry.getKey();
      Long currentValue = (Long) currentGauge.getValue();

      // first reading of gauges will have no previous value. Simply store the current readings.
      if (previousValues.get(currentKey) == null) {
        previousValues.put(currentKey, currentValue);
        nonAccumulatingValues.put(currentKey, currentValue);
      } else {
        // subtract the previous gauge readings from the current readings, and store result
        Long difference = currentValue - previousValues.get(currentKey);
        nonAccumulatingValues.put(currentKey, difference);
        previousValues.put(currentKey, currentValue);
      }
    }
  }
  /**
   * Creates a map of non-accumulating gauges for the underlying gauges in {@code
   * garbageCollectorMetricSet} Also, add an additional gauge for GC throughput.
   *
   * @return the map of metrics that contains non-cumulative gauges
   */
  @Override
  public Map<String, Metric> getMetrics() {
    Map<String, Metric> metricMap = garbageCollectorMetricSet.getMetrics();
    Map<String, Metric> nonAccumulatingMetricMap = new HashMap<String, Metric>();

    for (final Map.Entry<String, Metric> metricEntry : metricMap.entrySet()) {
      Gauge nonAccumulatingGauge =
          new Gauge<Long>() {
            /**
             * Instead of reading from the accumulating gauges in {@code garbageCollectorMetricSet},
             * we read from the map of non-accumulating values (which is updated by the background
             * task running in {@code scheduledExecutorService}).
             *
             * <p>For reporters that call this method, it makes sense to align the frequency of
             * those calls with the frequency of the background updates of non-accumulating gauges.
             * For example, if a reporter calls this method every minute, then the {@code interval}
             * setting of this metric-set should also be 1 minute. Calling this method more or less
             * frequently does not impact the operation of this class--but the caller will either
             * get repeated values (for more frequent calls), or will miss some values (for less
             * frequent calls).
             *
             * @return the gauge value representing only the current reporting interval
             */
            @Override
            public Long getValue() {
              Long value = nonAccumulatingValues.get(metricEntry.getKey());
              return value != null ? value : 0L;
            }
          };
      nonAccumulatingMetricMap.put(metricEntry.getKey(), nonAccumulatingGauge);
    }

    Gauge gcThroughputGauge =
        new Gauge<Double>() {
          /**
           * This gauge returns readings for garbage collector throughput. GC throughput is derived
           * from the readings of time spent by all garbage collectors in the current {@code
           * interval} of time.
           *
           * <p>Example: for an interval of 1 minute, we have 60,000 ms of total time. If we spent
           * 200 ms on minor GC's and 100 ms on major GC's, then 59,700 ms were available to the
           * application. 59,700 / 60,000 = 99.5% GC throughput.
           *
           * @return a value between 0.0 and 100.0 that represents the garbage collector throughput
           *     for the current interval (as defined by {@code interval}
           */
          @Override
          public Double getValue() {
            Double totalGCTime = 0.0;

            for (Map.Entry<String, Long> metricEntry : nonAccumulatingValues.entrySet()) {
              if (metricEntry.getKey().endsWith("time")) {
                totalGCTime += metricEntry.getValue();
              }
            }

            return 100 - ((totalGCTime / interval) * 100);
          }
        };
    nonAccumulatingMetricMap.put(GC_THROUGHPUT_METRIC_NAME, gcThroughputGauge);

    return Collections.unmodifiableMap(nonAccumulatingMetricMap);
  }