001package io.prometheus.metrics.instrumentation.jvm;
002
003import com.sun.management.GarbageCollectionNotificationInfo;
004import com.sun.management.GcInfo;
005import io.prometheus.metrics.config.PrometheusProperties;
006import io.prometheus.metrics.core.metrics.Counter;
007import io.prometheus.metrics.model.registry.PrometheusRegistry;
008import java.lang.management.GarbageCollectorMXBean;
009import java.lang.management.ManagementFactory;
010import java.lang.management.MemoryUsage;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Map;
014import javax.management.Notification;
015import javax.management.NotificationEmitter;
016import javax.management.NotificationListener;
017import javax.management.openmbean.CompositeData;
018
019/**
020 * JVM memory allocation metrics. The {@link JvmMemoryPoolAllocationMetrics} are registered as part
021 * of the {@link JvmMetrics} like this:
022 *
023 * <pre>{@code
024 * JvmMetrics.builder().register();
025 * }</pre>
026 *
027 * However, if you want only the {@link JvmMemoryPoolAllocationMetrics} you can also register them
028 * directly:
029 *
030 * <pre>{@code
031 * JvmMemoryAllocationMetrics.builder().register();
032 * }</pre>
033 *
034 * Example metrics being exported:
035 *
036 * <pre>
037 * # HELP jvm_memory_pool_allocated_bytes_total Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously.
038 * # TYPE jvm_memory_pool_allocated_bytes_total counter
039 * jvm_memory_pool_allocated_bytes_total{pool="Code Cache"} 4336448.0
040 * jvm_memory_pool_allocated_bytes_total{pool="Compressed Class Space"} 875016.0
041 * jvm_memory_pool_allocated_bytes_total{pool="Metaspace"} 7480456.0
042 * jvm_memory_pool_allocated_bytes_total{pool="PS Eden Space"} 1.79232824E8
043 * jvm_memory_pool_allocated_bytes_total{pool="PS Old Gen"} 1428888.0
044 * jvm_memory_pool_allocated_bytes_total{pool="PS Survivor Space"} 4115280.0
045 * </pre>
046 */
047public class JvmMemoryPoolAllocationMetrics {
048
049  private static final String JVM_MEMORY_POOL_ALLOCATED_BYTES_TOTAL =
050      "jvm_memory_pool_allocated_bytes_total";
051
052  private final List<GarbageCollectorMXBean> garbageCollectorBeans;
053
054  private JvmMemoryPoolAllocationMetrics(List<GarbageCollectorMXBean> garbageCollectorBeans) {
055    this.garbageCollectorBeans = garbageCollectorBeans;
056  }
057
058  private void register(PrometheusRegistry registry) {
059    Counter allocatedCounter =
060        Counter.builder()
061            .name(JVM_MEMORY_POOL_ALLOCATED_BYTES_TOTAL)
062            .help(
063                "Total bytes allocated in a given JVM memory pool. Only updated after GC, "
064                    + "not continuously.")
065            .labelNames("pool")
066            .register(registry);
067
068    AllocationCountingNotificationListener listener =
069        new AllocationCountingNotificationListener(allocatedCounter);
070    for (GarbageCollectorMXBean bean : garbageCollectorBeans) {
071      if (bean instanceof NotificationEmitter) {
072        ((NotificationEmitter) bean).addNotificationListener(listener, null, null);
073      }
074    }
075  }
076
077  static class AllocationCountingNotificationListener implements NotificationListener {
078
079    private final Map<String, Long> lastMemoryUsage = new HashMap<>();
080    private final Counter counter;
081
082    AllocationCountingNotificationListener(Counter counter) {
083      this.counter = counter;
084    }
085
086    @Override
087    public synchronized void handleNotification(Notification notification, Object handback) {
088      GarbageCollectionNotificationInfo info =
089          GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
090      GcInfo gcInfo = info.getGcInfo();
091      Map<String, MemoryUsage> memoryUsageBeforeGc = gcInfo.getMemoryUsageBeforeGc();
092      Map<String, MemoryUsage> memoryUsageAfterGc = gcInfo.getMemoryUsageAfterGc();
093      for (Map.Entry<String, MemoryUsage> entry : memoryUsageBeforeGc.entrySet()) {
094        String memoryPool = entry.getKey();
095        long before = entry.getValue().getUsed();
096        long after = memoryUsageAfterGc.get(memoryPool).getUsed();
097        handleMemoryPool(memoryPool, before, after);
098      }
099    }
100
101    // Visible for testing
102    void handleMemoryPool(String memoryPool, long before, long after) {
103      /*
104       * Calculate increase in the memory pool by comparing memory used
105       * after last GC, before this GC, and after this GC.
106       * See ascii illustration below.
107       * Make sure to count only increases and ignore decreases.
108       * (Typically a pool will only increase between GCs or during GCs, not both.
109       * E.g. eden pools between GCs. Survivor and old generation pools during GCs.)
110       *
111       *                         |<-- diff1 -->|<-- diff2 -->|
112       * Timeline: |-- last GC --|             |---- GC -----|
113       *                      ___^__        ___^____      ___^___
114       * Mem. usage vars:    / last \      / before \    / after \
115       */
116
117      // Get last memory usage after GC and remember memory used after for next time
118      long last = getAndSet(lastMemoryUsage, memoryPool, after);
119      // Difference since last GC
120      long diff1 = before - last;
121      // Difference during this GC
122      long diff2 = after - before;
123      // Make sure to only count increases
124      if (diff1 < 0) {
125        diff1 = 0;
126      }
127      if (diff2 < 0) {
128        diff2 = 0;
129      }
130      long increase = diff1 + diff2;
131      if (increase > 0) {
132        counter.labelValues(memoryPool).inc(increase);
133      }
134    }
135
136    private static long getAndSet(Map<String, Long> map, String key, long value) {
137      Long last = map.put(key, value);
138      return last == null ? 0 : last;
139    }
140  }
141
142  public static Builder builder() {
143    return new Builder();
144  }
145
146  @SuppressWarnings("unused")
147  public static Builder builder(PrometheusProperties config) {
148    return new Builder();
149  }
150
151  public static class Builder {
152    private List<GarbageCollectorMXBean> garbageCollectorBeans;
153
154    private Builder() {}
155
156    /** Package private. For testing only. */
157    Builder withGarbageCollectorBeans(List<GarbageCollectorMXBean> garbageCollectorBeans) {
158      this.garbageCollectorBeans = garbageCollectorBeans;
159      return this;
160    }
161
162    public void register() {
163      register(PrometheusRegistry.defaultRegistry);
164    }
165
166    public void register(PrometheusRegistry registry) {
167      List<GarbageCollectorMXBean> garbageCollectorBeans = this.garbageCollectorBeans;
168      if (garbageCollectorBeans == null) {
169        garbageCollectorBeans = ManagementFactory.getGarbageCollectorMXBeans();
170      }
171      new JvmMemoryPoolAllocationMetrics(garbageCollectorBeans).register(registry);
172    }
173  }
174}