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