001/*
002 * Copyright 2013 Prometheus Team Licensed under the Apache License, Version 2.0
003 * (the "License"); you may not use this file except in compliance with the
004 * License. You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
010 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
011 * License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package io.prometheus.client.utility.jvmstat;
016
017import io.prometheus.client.Prometheus;
018import io.prometheus.client.metrics.Gauge;
019import sun.jvmstat.monitor.HostIdentifier;
020import sun.jvmstat.monitor.Monitor;
021import sun.jvmstat.monitor.MonitorException;
022import sun.jvmstat.monitor.MonitoredHost;
023import sun.jvmstat.monitor.MonitoredVm;
024import sun.jvmstat.monitor.VmIdentifier;
025
026import java.lang.management.ManagementFactory;
027import java.net.URISyntaxException;
028import java.util.List;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.atomic.AtomicBoolean;
031import java.util.concurrent.atomic.AtomicInteger;
032import java.util.logging.Logger;
033import java.util.regex.PatternSyntaxException;
034
035/**
036 * <p>
037 * {@link JvmstatMonitor} provides HotSpot-specific JVM metrics through the {@link
038 * sun.jvmstat.monitor.MonitoredVm} facilities.
039 * </p>
040 *
041 * <p>
042 * These <em>low-level</em> metrics are defined in the C++ bowels of the HotSpot VM through
043 * internal measurement types.  The VM exposes these through the <em>HSPerfData</em> interface
044 * for customers.  Typically one accesses this data via a MMAPed direct buffer—namely Java native
045 * I/O (nio).
046 * </p>
047 *
048 * <p>
049 * <h1>Important Notes</h1>
050 * Due to inconsistencies of virtual machine packages and vendoring, the following remarks should
051 * be carefully observed:
052 * <ul>
053 * <li>
054 *   Users may need to explicitly add {@code ${JAVA_HOME}/lib/tools.jar} to the <em>CLASSPATH</em>
055 *   when using this helper.  This is because the {@code sun.jvmstat.monitor} package hierarchy
056 *   is not a part of the standard Java library.  These packages are, however, typically included
057 *   in Sun-, Oracle-, and OpenJDK-provided virtual machines, which is to say they are ubiquitous
058 *   in most deployment environments.
059 * </li>
060 * <li>
061 *   Users <em>may</em> need to explicitly set {@code -XX:+UsePerfData} in the VM's flags to enable
062 *   low-level telemetric export.
063 * </li>
064 * </ul>
065 * </p>
066 *
067 * <p>
068 * The metrics that this class exposes are dependent upon the release of HotSpot, including even
069 * minor releases.  Depending on that, it may be possible to use adapt this utility to be more
070 * flexible by incorporating support for common metric aliases, as defined in {@code
071 * sun/jvmstat/perfdata/resources/aliasmap}.
072 * </p>
073 *
074 * @author Matt T. Proud (matt.proud@gmail.com)
075 * @see Prometheus#addPreexpositionHook(io.prometheus.client.Prometheus.ExpositionHook)
076 * @see sun.jvmstat.monitor.MonitoredVm
077 */
078public class JvmstatMonitor implements Prometheus.ExpositionHook {
079  private static final int REFRESH_INTERVAL = 5;
080  private static final Gauge.Builder gaugePrototype = Gauge.newBuilder()
081      .namespace("jvmstat");
082  private static final Logger log = Logger.getLogger(JvmstatMonitor.class.getName());
083
084  private final MonitoredVm bridge;
085
086  private final MonitorVisitor clsInstrumentation = new ClassLoaderInstrumentation();
087  private final MonitorVisitor nccInstrumentation = new NativeCodeCompilerInstrumentation();
088  private final MonitorVisitor gcInstrumentation = new GarbageCollectionInstrumentation();
089  private final MonitorVisitor mmInstrumentation =  new ManagedMemoryInstrumentation();
090
091  private final AtomicInteger refreshes = new AtomicInteger(0);
092  private final ConcurrentHashMap<String, Monitor> monitors = new ConcurrentHashMap<>(400);
093
094  /**
095   * <p>Create a {@link JvmstatMonitor} for the local virtual machine associated with this
096   * specific Java server.</p>
097   *
098   * @throws {@link AttachmentError} if the VM cannot be attached to.
099   */
100  public JvmstatMonitor() throws AttachmentError {
101    this(getLocalVM());
102  }
103
104  /**
105   * <p>Create a {@link JvmstatMonitor} monitoring bridge with a supplied {@link
106   * sun.jvmstat.monitor.MonitoredVm}.</p>
107   *
108   * <p><strong>This is only useful for reusing an existing {@link sun.jvmstat.monitor.MonitoredVm}
109   * or for testing purposes.</strong>  Users are encouraged to use the {@link #JvmstatMonitor()}
110   * default constructor.</p>
111   *
112   * @param b
113   */
114  public JvmstatMonitor(final MonitoredVm b) {
115    bridge = b;
116  }
117
118  private void refreshMetrics() {
119    final List<Monitor> monitors;
120    try {
121      monitors = bridge.findByPattern(".*");
122    } catch (final MonitorException ex) {
123      log.warning(String.format("could not extract telemetry: %s", ex));
124      return;
125    } catch (final PatternSyntaxException ex) {
126      log.warning(String.format("could not extract telemetry: %s", ex));
127      return;
128    }
129    if (monitors == null) {
130      return;
131    }
132    for (final Monitor monitor : monitors) {
133      this.monitors.putIfAbsent(monitor.getName(), monitor);
134    }
135  }
136
137  @Override
138  public void run() {
139    if (refreshes.getAndIncrement() % REFRESH_INTERVAL == 0) {
140      /* This model presumes that metrics are only added throughout the course of runtime and never
141       * removed.  I think that is a reasonable assumption for now.  Handling the removal case
142       * would be easy.
143       */
144      refreshMetrics();
145    }
146    /*
147     * There are a few ways we could make this cleaner, through the current implementation is just
148     * fine.  Here are a few candidates:
149     *
150     * - Direct buffer access of HSPerfData MMAP.
151     * -- Caveat: We would need to implement our own HSPerfData processor, which becomes a huge
152     *            point of fragility with even minor HotSpot releases.
153     * - Extending the Metric and its children's mutation model to use a callback when a value
154     *   is requested.
155     * -- Caveat: Unimplemented, but would need to be really careful with the API's design.
156     * - Persistent VmListener registered with HotSpot.
157     * -- Caveat: HotSpot just uses polls as well in the underlying implementation for event
158     *            dispatching.
159     */
160    for (final String monitorName : monitors.keySet()) {
161      final Monitor monitor = monitors.get(monitorName);
162
163      /*
164       * Dynamically visit any metrics if they are found.  Unfound metrics are never
165       * registered and exported, because we do not want to clutter the namespace and
166       * confuse users.
167       *
168       * The VM offers no contract about metric order, so it seems imprudent to accumulate
169       * state and infer the existence of extra metrics solely by the presence of an earlier one.
170       */
171      if (clsInstrumentation.visit(monitorName, monitor)) {
172        continue;
173      }
174      if (nccInstrumentation.visit(monitorName, monitor)) {
175        continue;
176      }
177      if (gcInstrumentation.visit(monitorName, monitor)) {
178        continue;
179      }
180      if (mmInstrumentation.visit(monitorName, monitor)) {
181        continue;
182      }
183    }
184  }
185
186  private static MonitoredVm getLocalVM() throws AttachmentError {
187    final String name = ManagementFactory.getRuntimeMXBean().getName();
188    final int pidOffset = name.indexOf('@');
189    final int pid;
190    try {
191      pid = Integer.valueOf(name.substring(0, pidOffset));
192    } catch (final IndexOutOfBoundsException ex) {
193      throw new AttachmentError("illegal instance name", ex);
194    } catch (final NumberFormatException ex) {
195      throw new AttachmentError("illegal instance name format", ex);
196    }
197
198    try {
199      final HostIdentifier hostId = new HostIdentifier((String) null);
200      final MonitoredHost host = MonitoredHost.getMonitoredHost(hostId);
201      final String localAddr = String.format("//%d?mode=r", pid);
202      final VmIdentifier id = new VmIdentifier(localAddr);
203
204      return host.getMonitoredVm(id);
205    } catch (final URISyntaxException ex) {
206      throw new AttachmentError("invalid target URI", ex);
207    } catch (final MonitorException ex) {
208      throw new AttachmentError("could not resolve host", ex);
209    }
210  }
211
212  /**
213   * <p>A {@link AttachmentError} indicates that it was impossible to attach to the requested
214   * virtual machine.</p>
215   */
216  public static class AttachmentError extends Exception {
217    AttachmentError(final String message, final Throwable cause) {
218      super(message, cause);
219    }
220  }
221
222  /**
223   * <p>A {@link UnknownMonitorError} indicates that we have an invalid entry in the processing
224   * pipeline.</p>
225   */
226  static class UnknownMonitorError extends IllegalArgumentException {
227    UnknownMonitorError(final Monitor m) {
228      super(String.format("unhandled jvmstat monitor: %s", m.getName()));
229    }
230  }
231
232  /**
233   * <p>Provide visibility about the internals of the JVM's class loader.</p>
234   *
235   * <h1>Generated Metrics</h1>
236   * <h2>{@code jvmstat_classloader_operations_total}</h2>
237   * This metric tracks the number of classloader operations by event type.
238   * <h3>Metric Dimensions</h3>
239   * <ul>
240   * <li>
241   *   {@code event}: The type of event.
242   * <ul>
243   * <li>
244   *   {@code loaded}: The virtual machine loaded a class.  This occurs when the virtual
245   *   machine starts and builds its initial class dependency graph.  Alternatively it may occur at
246   *   runtime when using the reflection facilities or programmatic class loader: {@code
247   *   Class.forName("java.lang.String")}.  As noted in {@code unloaded}, this can also
248   *   occur if an unneeded class is unloaded due to memory pressure in the <em>permanent
249   *   generation</em> and later needs to be reloaded.  The virtual machine's default behavior is
250   *   to try to keep all classes statically loaded for the lifetime of the process.  See {@code
251   *   ClassLoadingService::notify_class_loaded} in the HotSpot source.
252   * </li>
253   * <li>
254   *   {@code unloaded}: The virtual machine unloaded a class.  This event is likely to be rare
255   *   and will likely occur when either <em>permanent generation</em> memory space is too small
256   *   for all of the needed classes, or memory pressure occurs from excessive class loading churn.
257   *   Examples for this latter case of churn and memory pressure may be from using code generation
258   *   facilities from mock or dependency injection frameworks, though this case is most likely to
259   *   occur only in test suite runs.  Most likely, memory pressure is a result from using a
260   *   poorly-designed dynamic language on the virtual machine that unintelligently uses code
261   *   generation for ad hoc type generation, thereby blasting the <em>permanent generation</em>
262   *   with leaky class metadata descriptors.  See @{code
263   *   ClassLoadingService::notify_class_unloaded} in the HotSpot source.
264   * </li>
265   * <li>
266   *   {@code initialized}: The virtual machine initializes a class.  The process of initializing
267   *   a class is different from loading it, in that initialization takes the process of loading
268   *   one important step further: <em>it initializes all static fields and instantiates all
269   *   objects required for the fulfillment of that class</em>, including performing further class
270   *   loading and initialization of dependent classes.  The initialization process includes
271   *   running a class' static initializers: {@code static {}} blocks.  Consult the <em>Virtual
272   *   Machine Specification sections 2.16.4-5</em> and {@code InstanceClass::initialize} in the
273   *   HotSpot source for a further discussion.
274   * </li>
275   * </ul>
276   * </li>
277   * </ul>
278   *
279   * <h2>{@code jvmstat_classloader_duration_ms}</h2>
280   * This metric tracks the amount of time the class loader spends in various types of
281   * operations.
282   * <h3>Metric Dimensions</h3>
283   * <ul>
284   * <li>
285   *   {@code operation}: The operation type.
286   * </li>
287   * <li>
288   *   {@code find}
289   * </li>
290   * <li>
291   *   {@code parse}
292   * </li>
293   * </ul>
294   *
295   * <h2>{@code jvmstat_classloader_loaded_bytes}</h2>
296   * This metric tracks the total number of bytes the classloader has loaded and processed.
297   */
298  public static class ClassLoaderInstrumentation implements MonitorVisitor {
299    // This class has public visibility solely for Javadoc production.
300    private static final Gauge.Builder gaugePrototype = JvmstatMonitor.gaugePrototype
301        .subsystem("classloader");
302
303    private final AtomicBoolean statesOnce = new AtomicBoolean(false);
304    private final AtomicBoolean sizesOnce = new AtomicBoolean(false);
305    private final AtomicBoolean durationsOnce = new AtomicBoolean(false);
306
307    private Gauge states;
308    private Gauge sizes;
309    private Gauge durations;
310
311    public boolean visit(final String name, final Monitor monitor) {
312      switch (name) {
313        case "java.cls.loadedClasses":
314        case "java.cls.unloadedClasses":
315        case "sun.cls.initializedClasses":
316          return visitClassEvents(name, monitor);
317
318        case "sun.cls.loadedBytes":
319          return visitSizes(monitor);
320
321        case "sun.classloader.findClassTime":
322        case "sun.cls.parseClassTime":
323          return visitDurations(name, monitor);
324      }
325
326      return false;
327    }
328
329    private boolean remarkDuration(final String duration, final Monitor monitor) {
330      final Double value = decodeMetric(monitor);
331      if (value == null) {
332        return false;
333      }
334
335      durations.newPartial()
336          .labelPair("operation", duration)
337          .apply()
338          .set(value);
339
340      return true;
341    }
342
343    private boolean visitDurations(final String name, final Monitor monitor) {
344      if (durations == null && durationsOnce.getAndSet(true) == false) {
345        durations = gaugePrototype
346            .name("duration_ms")
347            .documentation("The time it has taken the classloader to perform loadings partitioned by operation.")
348            .labelNames("operation")
349            .build();
350      }
351
352      switch (name) {
353        case "sun.classloader.findClassTime":
354          return remarkDuration("find", monitor);
355        case "sun.cls.parseClassTime":
356          return remarkDuration("parse", monitor);
357        default:
358          throw new UnknownMonitorError(monitor);
359      }
360    }
361
362    private boolean visitSizes(final Monitor monitor) {
363      if (sizes == null && sizesOnce.getAndSet(true) == false) {
364        sizes = gaugePrototype
365            .name("loaded_bytes")
366            .documentation("The number of bytes the classloader has loaded.")
367            .build();
368      }
369
370      final Double value = decodeMetric(monitor);
371      if (value == null) {
372        return false;
373      }
374
375      sizes.newPartial()
376          .apply()
377          .set(value);
378      return true;
379    }
380
381    private boolean remarkClassEvent(final String event, final Monitor monitor) {
382      final Double value = decodeMetric(monitor);
383      if (value == null) {
384        return false;
385      }
386
387      states.newPartial()
388          .labelPair("event", event)
389          .apply()
390          .set(value);
391      return true;
392    }
393
394    private boolean visitClassEvents(final String name, final Monitor monitor) {
395      if (states == null && statesOnce.getAndSet(true) == false) {
396        states = gaugePrototype
397            .name("operations_total")
398            .documentation("The number of classes the loader has touched by disposition.")
399            .labelNames("event")
400            .build();
401      }
402
403      switch (name) {
404        case "java.cls.loadedClasses":
405          return remarkClassEvent("loaded", monitor);
406        case "java.cls.unloadedClasses":
407          return remarkClassEvent("unloaded", monitor);
408        case "sun.cls.initializedClasses":
409          return remarkClassEvent("initialized", monitor);
410        default:
411          throw new UnknownMonitorError(monitor);
412      }
413    }
414  }
415
416  /**
417   * <p>Provide visibility about the internals of the JVM's just-in-time (JIT) compiler.</p>
418   *
419   * <p>Even if you do not run your Java server with {@code -server} mode, these metrics will be
420   * beneficial to understand how much is being optimized and whether a deeper analysis to find
421   * classes to blacklist from JIT rewriting exist.</p>
422   *
423   * <h1>Generated Metrics</h1>
424   * <h2>{@code jvmstat_jit_compilation_time_ms}</h2>
425   * This metric tracks the amount of time the JIT spends compiling byte code into native code
426   * partitioned by JIT thread.
427   * <h3>Metric Dimensions</h3>
428   * <ul>
429   * <li>
430   *   {@code thread}: The thread ID associated with the work.
431   * </li>
432   * <ul>
433   *
434   * <h2>{@code jvmstat_jit_compilation_count}</h2>
435   * This metric tracks the number of classes the JIT has compiled and potentially recompiled after
436   * determining either the initial byte code definition or re-interpreted machine code
437   * representation is suboptimal.
438   * <h3>Metric Dimensions</h3>
439   * <ul>
440   * <li>
441   *   {@code thread}: The thread ID associated with the work.
442   * </li>
443   * <ul>
444   */
445  public static class NativeCodeCompilerInstrumentation implements MonitorVisitor {
446    // This class has public visibility solely for Javadoc production.
447    private static final Gauge.Builder gaugePrototype = JvmstatMonitor.gaugePrototype
448        .subsystem("jit");
449
450    private final AtomicBoolean compilationOnce = new AtomicBoolean(false);
451    private final AtomicBoolean durationOnce = new AtomicBoolean(false);
452
453    private Gauge compilations;
454    private Gauge durations;
455
456    public boolean visit(final String name, final Monitor monitor) {
457      switch (name) {
458        /*
459         * Incorporate sun.ci.threads for accurate counting.  It is unlikely that there will
460         * be more than two threads at any time.  If we see index "2", we will need to revisit
461         * this exposition bridge.
462         */
463        case "sun.ci.compilerThread.0.compiles":
464        case "sun.ci.compilerThread.1.compiles":
465        case "sun.ci.compilerThread.2.compiles":
466          return visitCompilations(name, monitor);
467
468        case "sun.ci.compilerThread.0.time":
469        case "sun.ci.compilerThread.1.time":
470        case "sun.ci.compilerThread.2.time":
471          return visitDurations(name, monitor);
472      }
473
474      return false;
475    }
476
477    private boolean remarkDuration(final String thread, final Monitor monitor) {
478      final Double value = decodeMetric(monitor);
479      if (value == null) {
480        return false;
481      }
482
483      durations.newPartial()
484          .labelPair("thread", thread)
485          .apply()
486          .set(value);
487
488      return true;
489    }
490
491    private boolean visitDurations(final String name, final Monitor monitor) {
492      if (durations == null && durationOnce.getAndSet(true) == false) {
493        durations = gaugePrototype
494            .name("compilation_time_ms")
495            .documentation("The count of JIT compilation events.")
496            .labelNames("thread")
497            .build();
498      }
499
500      switch (name) {
501        case "sun.ci.compilerThread.0.time":
502          return remarkDuration("0", monitor);
503        case "sun.ci.compilerThread.1.time":
504          return remarkDuration("1", monitor);
505        case "sun.ci.compilerThread.2.time":
506          return remarkDuration("2", monitor);
507        default:
508          throw new UnknownMonitorError(monitor);
509      }
510    }
511
512    private boolean remarkCompilation(final String thread, final Monitor monitor) {
513      final Double value = decodeMetric(monitor);
514      if (value == null) {
515        return false;
516      }
517
518      compilations.newPartial()
519          .labelPair("thread", thread)
520          .apply()
521          .set(value);
522
523      return true;
524    }
525
526    private boolean visitCompilations(final String name, final Monitor monitor) {
527      if (compilations == null && compilationOnce.getAndSet(true) == false) {
528        compilations = gaugePrototype
529            .name("compilation_count")
530            .documentation("The count of JIT compilation events.")
531            .labelNames("thread")
532            .build();
533      }
534
535      switch (name) {
536        case "sun.ci.compilerThread.0.compiles":
537          return remarkCompilation("0", monitor);
538        case "sun.ci.compilerThread.1.compiles":
539          return remarkCompilation("1", monitor);
540        case "sun.ci.compilerThread.2.compiles":
541          return remarkCompilation("2", monitor);
542        default:
543          throw new UnknownMonitorError(monitor);
544      }
545    }
546  }
547
548  /**
549   * <p>Provide visibility about the internals of the JVM's garbage collector.</p>
550   *
551   * <h1>Generated Metrics</h1>
552   * <h2>{@code jvmstat_garbage_collection_invocations_total}</h2>
553   * <p>Since JRE 1.1, the Java Virtual Machine has used a generational model for managing memory
554   * due to object lifecycle in well-designed systems typically following an exponential
555   * distribution whereby the majority of object born die young.  This means great efficiency gains
556   * can be achieved when long-standing tenured objects are ignored (i.e., collected less
557   * frequently) and the focus of collection becomes those newly-birthed objects</p>
558   *
559   * <pre>
560   *  o  |
561   *  b  | X
562   *  j  | X
563   *  e  | X  X
564   *  c  | X  X
565   *  t  | X  X  X
566   *  s  | X  X  X
567   *     | X  X  X  X  X  X
568   *  #   —————————————————————
569   *       0  1  2  3  4  5 … n
570   *
571   *      object cohort age
572   * </pre>
573   *
574   * <p>This is to say, assuming a small amount of persistent in-memory state within a server, the
575   * majority of objects are allocated during units of work (e.g., HTTP requests, RPCs, etc.) and
576   * have no bearing or produce little by way of side effects that need to be retained later
577   * in the life of the server.  <strong>This is the ideal case.</strong></p>
578   *
579   * <h3>Metric Dimensions</h3>
580   * <ul>
581   * <li>
582   *   {@code generation}: The managed memory region name.
583   * </li>
584   * <ul>
585   *
586   * <h2>{@code jvmstat_garbage_collection_duration_ms_total}</h2>
587   * This metric tracks the amount of time spent spent collecting the respective generation.
588   * <h3>Metric Dimensions</h3>
589   * <ul>
590   * <li>
591   *   {@code generation}: The managed memory region name.
592   * </li>
593   * <ul>
594   */
595  public static class GarbageCollectionInstrumentation implements MonitorVisitor {
596    // This class has public visibility solely for Javadoc production.
597    private static final Gauge.Builder gaugePrototype = JvmstatMonitor.gaugePrototype
598        .subsystem("garbage_collection");
599
600    final AtomicBoolean invocationsOnce = new AtomicBoolean(false);
601    final AtomicBoolean durationsOnce = new AtomicBoolean(false);
602
603    Gauge invocations;
604    Gauge durations;
605
606    /*
607     * TODO: Add garbage collection failure mode registration instrumentation after coming to
608     *       consensus on internal naming inside of the Virtual Machine.
609     *
610     * TODO: Add metrics for "full GC" events from failure modes.
611     */
612    public boolean visit(final String name, final Monitor monitor) {
613      switch (name) {
614        case "sun.gc.collector.0.invocations":
615        case "sun.gc.collector.1.invocations":
616          return visitInvocations(name, monitor);
617
618        case "sun.gc.collector.0.time":
619        case "sun.gc.collector.1.time":
620          return visitDurations(name, monitor);
621      }
622
623      return false;
624    }
625
626    private boolean remarkInvocation(final String generation, final Monitor monitor) {
627      final Double value = decodeMetric(monitor);
628      if (value == null) {
629        return false;
630      }
631
632      invocations.newPartial()
633          .labelPair("generation", generation)
634          .apply()
635          .set(value);
636
637      return true;
638    }
639
640    private boolean visitInvocations(final String name, final Monitor monitor) {
641      if (invocations == null && invocationsOnce.getAndSet(true) == false) {
642        invocations = gaugePrototype
643            .name("invocations_total")
644            .labelNames("generation")
645            .documentation("The total number of times the garbage collector has been invoked.")
646            .build();
647      }
648
649      switch (name) {
650        case "sun.gc.collector.0.invocations":
651          return remarkInvocation("new", monitor);
652        case "sun.gc.collector.1.invocations":
653          return remarkInvocation("old", monitor);
654        default:
655          throw new UnknownMonitorError(monitor);
656      }
657    }
658
659    private boolean remarkDuration(final String generation, final Monitor monitor) {
660      final Double valueMicros = decodeMetric(monitor);
661      if (valueMicros == null) {
662        return false;
663      }
664
665      final Double valueMillis = valueMicros / 1000;
666
667      durations.newPartial()
668          .labelPair("generation", generation)
669          .apply()
670          .set(valueMillis);
671
672      return true;
673    }
674
675    private boolean visitDurations(final String name, final Monitor monitor) {
676      if (durations == null && durationsOnce.getAndSet(true) == false) {
677        durations = gaugePrototype
678            .name("durations_ms_total")
679            .documentation("The total time spent in garbage collection for a generation.")
680            .build();
681      }
682
683      switch (name) {
684        case "sun.gc.collector.0.time":
685          return remarkDuration("new", monitor);
686        case "sun.gc.collector.1.time":
687          return remarkDuration("old", monitor);
688        default:
689          throw new UnknownMonitorError(monitor);
690      }
691    }
692  }
693
694  /**
695   * <p>Provide visibility about the internals of the JVM memory regions.</p>
696   *
697   * <h1>Generated Metrics</h1>
698   * <h2>{@code jvmstat_managed_memory_generation_limit_bytes}</h2>
699   * <h3>Metric Dimensions</h3>
700   * <ul>
701   * <li>
702   *   {@code generation}: The managed memory region name.
703   * </li>
704   * <ul>
705   *
706   * <h2>{@code jvmstat_managed_memory_generation_usage_bytes}</h2>
707   * <h3>Metric Dimensions</h3>
708   * <ul>
709   * <li>
710   *   {@code generation}: The managed memory region name.
711   * </li>
712   * <ul>
713   *
714   * <h2>{@code jvmstat_managed_memory_survivor_space_agetable_size_bytes}</h2>
715   * <h3>Metric Dimensions</h3>
716   * <ul>
717   * <li>
718   *   {@code cohort}: The agetable cohort.  (TODO: Discuss the agetable design.)
719   * </li>
720   * <ul>
721   *
722   * <h2>{@code jvmstat_managed_memory_survivor_space_agetable_count}</h2>
723   *(TODO: Discuss the agetable design.)
724   *
725   * @see io.prometheus.client.utility.jvmstat.JvmstatMonitor.GarbageCollectionInstrumentation
726   */
727  public static class ManagedMemoryInstrumentation implements MonitorVisitor {
728    // This class has public visibility solely for Javadoc production.
729    private static final Gauge.Builder gaugePrototype = JvmstatMonitor.gaugePrototype
730        .subsystem("managed_memory");
731
732    private final AtomicBoolean agetableCohortsOnce = new AtomicBoolean(false);
733    private final AtomicBoolean agetableCountOnce = new AtomicBoolean(false);
734    private final AtomicBoolean generationLimitOnce = new AtomicBoolean(false);
735    private final AtomicBoolean generationUsageOnce = new AtomicBoolean(false);
736
737    // TODO: Discuss markOop and oopDesc types.
738    private Gauge agetableCohorts;
739    private Gauge agetableCount;
740    private Gauge generationLimit;
741    private Gauge generationUsage;
742
743    public boolean visit(final String name, final Monitor monitor) {
744      switch (name) {
745        case "sun.gc.generation.0.agetable.bytes.00":
746        case "sun.gc.generation.0.agetable.bytes.01":
747        case "sun.gc.generation.0.agetable.bytes.02":
748        case "sun.gc.generation.0.agetable.bytes.03":
749        case "sun.gc.generation.0.agetable.bytes.04":
750        case "sun.gc.generation.0.agetable.bytes.05":
751        case "sun.gc.generation.0.agetable.bytes.06":
752        case "sun.gc.generation.0.agetable.bytes.07":
753        case "sun.gc.generation.0.agetable.bytes.08":
754        case "sun.gc.generation.0.agetable.bytes.09":
755        case "sun.gc.generation.0.agetable.bytes.10":
756        case "sun.gc.generation.0.agetable.bytes.11":
757        case "sun.gc.generation.0.agetable.bytes.12":
758        case "sun.gc.generation.0.agetable.bytes.13":
759        case "sun.gc.generation.0.agetable.bytes.14":
760        case "sun.gc.generation.0.agetable.bytes.15":
761          return visitSurvivorSpaceAgetableCohorts(name, monitor);
762
763        case "sun.gc.generation.0.agetable.size":
764          return visitSurvivorSpaceAgetableCount(monitor);
765
766        case "sun.gc.generation.0.space.0.capacity":
767        case "sun.gc.generation.0.space.1.capacity":
768        case "sun.gc.generation.0.space.2.capacity":
769        case "sun.gc.generation.1.space.0.capacity":
770        case "sun.gc.generation.2.space.0.capacity":
771          return visitGenerationLimits(name, monitor);
772
773        case "sun.gc.generation.0.space.0.used":
774        case "sun.gc.generation.0.space.1.used":
775        case "sun.gc.generation.0.space.2.used":
776        case "sun.gc.generation.1.space.0.used":
777        case "sun.gc.generation.2.space.0.used":
778          return visitGenerationUsage(name, monitor);
779      }
780
781      return false;
782    }
783
784    private boolean remarkGenerationLimit(final String generation, final Monitor monitor) {
785      final Double value = decodeMetric(monitor);
786      if (value == null) {
787        return false;
788      }
789
790      generationLimit.newPartial()
791          .labelPair("generation", generation)
792          .apply()
793          .set(value);
794      return true;
795    }
796
797    private boolean visitGenerationLimits(final String name, final Monitor monitor) {
798      if (generationLimit == null && generationLimitOnce.getAndSet(true) == false) {
799        generationLimit = gaugePrototype
800            .name("generation_limit_bytes")
801            .labelNames("generation")
802            .documentation("The total allocation/reservation of each managed memory region or generation.")
803            .build();
804      }
805
806      switch (name) {
807        case "sun.gc.generation.0.space.0.capacity":
808          return remarkGenerationLimit("eden", monitor);
809        case "sun.gc.generation.0.space.1.capacity":
810          return remarkGenerationLimit("survivor0", monitor);
811        case "sun.gc.generation.0.space.2.capacity":
812          return remarkGenerationLimit("survivor1", monitor);
813        case "sun.gc.generation.1.space.0.capacity":
814          return remarkGenerationLimit("old", monitor);
815        case "sun.gc.generation.2.space.0.capacity":
816          return remarkGenerationLimit("permgen", monitor);
817        default:
818          throw new UnknownMonitorError(monitor);
819      }
820    }
821
822    private boolean remarkGenerationUsage(final String generation, final Monitor monitor) {
823      final Double value = decodeMetric(monitor);
824      if (value == null) {
825        return false;
826      }
827
828      generationUsage.newPartial()
829          .labelPair("generation", generation)
830          .apply()
831          .set(value);
832
833      return true;
834    }
835
836    private boolean visitGenerationUsage(final String name, final Monitor monitor) {
837      if (generationUsage == null && generationUsageOnce.getAndSet(true) == false) {
838        generationUsage = gaugePrototype
839            .name("generation_usage_bytes")
840            .labelNames("generation")
841            .documentation("The size used of each managed memory region or generation.")
842            .build();
843      }
844
845      switch (name) {
846        case "sun.gc.generation.0.space.0.used":
847          return remarkGenerationUsage("eden", monitor);
848        case "sun.gc.generation.0.space.1.used":
849          return remarkGenerationUsage("survivor0", monitor);
850        case "sun.gc.generation.0.space.2.used":
851          return remarkGenerationUsage("survivor1", monitor);
852        case "sun.gc.generation.1.space.0.used":
853          return remarkGenerationUsage("old", monitor);
854        case "sun.gc.generation.2.space.0.used":
855          return remarkGenerationUsage("permgen", monitor);
856        default:
857          throw new UnknownMonitorError(monitor);
858      }
859    }
860
861    private boolean remarkAgetableCohortSize(final String cohort, final Monitor monitor) {
862      final Double value = decodeMetric(monitor);
863      if (value == null) {
864        return false;
865      }
866
867      agetableCohorts.newPartial()
868          .labelPair("cohort", cohort)
869          .apply()
870          .set(value);
871
872      return true;
873    }
874
875    private boolean visitSurvivorSpaceAgetableCohorts(final String name, final Monitor monitor) {
876      if (agetableCohorts == null && agetableCohortsOnce.getAndSet(true) == false) {
877        agetableCohorts = gaugePrototype
878            .name("survivor_space_agetable_size_bytes")
879            .labelNames("cohort")
880            .documentation("A measure of the size of each survivor space agetable cohort.")
881            .build();
882      }
883
884      switch (name) {
885        case "sun.gc.generation.0.agetable.bytes.00":
886          return remarkAgetableCohortSize("00", monitor);
887        case "sun.gc.generation.0.agetable.bytes.01":
888          return remarkAgetableCohortSize("01", monitor);
889        case "sun.gc.generation.0.agetable.bytes.02":
890          return remarkAgetableCohortSize("02", monitor);
891        case "sun.gc.generation.0.agetable.bytes.03":
892          return remarkAgetableCohortSize("03", monitor);
893        case "sun.gc.generation.0.agetable.bytes.04":
894          return remarkAgetableCohortSize("04", monitor);
895        case "sun.gc.generation.0.agetable.bytes.05":
896          return remarkAgetableCohortSize("05", monitor);
897        case "sun.gc.generation.0.agetable.bytes.06":
898          return remarkAgetableCohortSize("06", monitor);
899        case "sun.gc.generation.0.agetable.bytes.07":
900          return remarkAgetableCohortSize("07", monitor);
901        case "sun.gc.generation.0.agetable.bytes.08":
902          return remarkAgetableCohortSize("08", monitor);
903        case "sun.gc.generation.0.agetable.bytes.09":
904          return remarkAgetableCohortSize("09", monitor);
905        case "sun.gc.generation.0.agetable.bytes.10":
906          return remarkAgetableCohortSize("10", monitor);
907        case "sun.gc.generation.0.agetable.bytes.11":
908          return remarkAgetableCohortSize("11", monitor);
909        case "sun.gc.generation.0.agetable.bytes.12":
910          return remarkAgetableCohortSize("12", monitor);
911        case "sun.gc.generation.0.agetable.bytes.13":
912          return remarkAgetableCohortSize("13", monitor);
913        case "sun.gc.generation.0.agetable.bytes.14":
914          return remarkAgetableCohortSize("14", monitor);
915        case "sun.gc.generation.0.agetable.bytes.15":
916          return remarkAgetableCohortSize("15", monitor);
917        default:
918          throw new UnknownMonitorError(monitor);
919      }
920    }
921
922    private boolean visitSurvivorSpaceAgetableCount(final Monitor monitor) {
923      if (agetableCount == null && agetableCountOnce.getAndSet(true) == false) {
924        agetableCount = gaugePrototype
925            .name("survivor_space_agetable_count")
926            .documentation("The number of survivor space agetable cohorts.")
927            .build();
928      }
929
930      final Double value = decodeMetric(monitor);
931      if (value == null) {
932        return false;
933      }
934
935      agetableCount.newPartial()
936          .apply()
937          .set(value);
938
939      return true;
940    }
941  }
942
943  static interface MonitorVisitor {
944    boolean visit(final String name, final Monitor monitor);
945  }
946
947  private static Double decodeMetric(final Monitor m) {
948    final Object value = m.getValue();
949
950    if (value == null) {
951      return null;
952    } else if (value instanceof Long) {
953      return Double.valueOf((Long) value);
954    } else {
955      try {
956        return Double.valueOf(value.toString());
957      } catch (final NumberFormatException unused) {
958        return null;
959      }
960    }
961  }
962}