Building a Custom JMC JMX Console Plug-in

Last week I ran into Gerrit Grünwald (@hansolo_) whilst demoing JMC in the Oracle booth at JFokus. I’d built a little RPi sensor board to have in my summer house, and since there is a lot of interest in the Internet of Things, I thought I’d contribute by showing how easy it is to hook up the JMX Console to one of those things.

sensor_board  temp

We also had a very neat demo sporting a plug-in for the JMC console for controlling our resident LEGO robot (caught in an intimate moment with another robot in the picture below).

robot_love mindstorms_demo_ui

Anyways, Gerrit wanted to know how to extend the JMC JMX Console with custom JavaFX visualization. Since the Java FX part will be much easier once JDK 8 and JMC 5.3.0 is out, I’ll leave that for a (not too distant) future blog post. Here is the first step though – how to build your own JMC console plug-in!

Preparations

The easiest way to get going is by installing the JMC PDE plug-in into Eclipse.

    1. Get an Eclipse classic
      For JMC 5.2, get Eclipse 3.8.2 up to 4.2.x (Juno)
      For JMC 5.3 (out soon), get Eclipse 3.8.2 and any 4.x (Kepler will work fine)
    2. Install JMC from the update site
      The update site link can be found here: http://www.oracle.com/missioncontrol
      Follow the installation instructions.
      Note: Later releases of Eclipse are pickier about certificate validation – if you run into problems because download.oracle.com maps to akamai hosts, try using http instead of https in the update site URL.
    3. Next install the PDE plug-in from the experimental update site
      The URL for the experimental update site can be found on the Mission Control page too: http://www.oracle.com/missioncontrol
      The plug-in to install is the Development/Java Mission Control PDE Integration one.

Next you need to set the target platform to your JMC installation.

    1. Go to Preferences
    2. Type targ in the filter box and select the “Target Platform” node in the preferences tree
    3. Click Add, to open the wizard for adding a new target platform
    4. Click next, since you want to start with an empty target definition
    5. Name the target JMC and click Add
    6. Select Installation and hit Next
    7. Select your JAVA_HOME/lib/missioncontrol folder
    8. Hit Finish

You are now set to start developing your first Mission Control plug-in.

JMC JMX Plug-in Hello World

First we need to create a new plug-in project. Select File | New | Other. Select Plug-in Project and hit next. Name your project and hit next. Name your plug-in and hit next. Now you should be able to select a custom wizard for creating a JMC console plug-in:

templates

Select Simple Mission Control Console Tab Wizard and hit Next. Change the class name and tab name to your liking and finish the wizard. You should now have a new project. With the current 5.2.0 PDE plug-in, you may have a weird compilation error due to an unnecessary import as well. If you do, simply right click on the offending class and choose organize imports to get rid of the import (remember to save the class after fixing the problem):

organize_imports
You can now run Mission Control with your new plug-in by selecting the project and choosing Run As | Eclipse Application from the context menu. If you connect with the management console, you should see something like this:

HelloJMC

Building the Plug-in

This is how you build your plug-in:

    1. Select the plug-in project and choose Export… from the context menu
    2. In the following wizard, select Deployable plug-ins and fragments
    3. Select a Directory for where to put the plug-in and hit Finish.

That’s it. You should now have a plug-in jar file.

Installing the Plug-in

How easy it is to install your plug-in depends on the version of JMC you are using.

For 5.3.0 you simply copy the plug-in into the JMC_HOME (i.e. JDK_HOME/lib/missioncontrol) dropins folder. Done!

For 5.2.0 this is quite a lot more elaborate. You first need to generate some p2 metadata. This can be done using Eclipse like this:

eclipse -application org.eclipse.equinox.p2.publisher.FeaturesAndBundlesPublisher -metadataRepository <metadatadir> -artifactRepository <metadatadir> -bundles <pluginsfolder> -compress –publishArtifacts

Where <metadatadir> is where you want the metadata published, and <pluginsfolder> is the plug-ins folder where you exported your plug-in.

For example:

macbookpro:~marcus$ /Users/marcus/Applications/eclipse_4.2.2/Eclipse.app/Contents/MacOS/eclipse -consoleLog -application org.eclipse.equinox.p2.publisher.FeaturesAndBundlesPublisher -metadataRepository file:/Users/marcus/Builds/repo -artifactRepository file:/Users/marcus/Builds/repo -bundles /Users/marcus/Builds/plugins -compress -publishArtifacts

The result in <metadatadir> should be two jars and a plugins folder with your plug-in in it.

Next you need to tell p2 to install the plug-in. JMC includes the p2 director, so installing can be done like this (using sudo for administrative rights – on windows you might want to open a shell with admin rights):

sudo jmc -nosplash -application org.eclipse.equinox.p2.director –repository <metadatadir> –installIU <pluginname>/<full version> -destination <jmchome> -profile JMC

For example:

macbookpro:~marcus$ sudo /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/lib/missioncontrol/Java\ Mission\ Control.app/Contents/MacOS/jmc -nosplash -application org.eclipse.equinox.p2.director -repository file:/Users/marcus/Builds/repo -installIU se.hirt.blogs.jmxconsole/1.0.0.201402132124 -destination /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/lib/missioncontrol/ -profile JMC

Uninstalling

If you wish to uninstall the plug-in, you just remove it from the dropins folder in 5.3.0. For 5.2.0 you use the uninstallIU command:

sudo jmc -nosplash -application org.eclipse.equinox.p2.director –uninstallIU <pluginname>/<full version> -destination <jmchome> -profile JMC

For example:

macbookpro:~marcus$ sudo /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/lib/missioncontrol/Java\ Mission\ Control.app/Contents/MacOS/jmc -nosplash -application org.eclipse.equinox.p2.director -uninstallIU se.hirt.blogs.jmxconsole/1.0.0.201402132124 -destination /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/lib/missioncontrol/ -profile JMC

Summary

This blog entry described how to create a custom plug-in for the Java Mission Control JMX Console, and how to install the newly created plug-in. The installation process of custom plug-ins is simpler in the upcoming 5.3.0 version of Mission Control.

Using the JVM Performance Counters

So, in JRockit there was this neat little dynamic MBean from which you could access all the JVM performance counters as attributes. Tonight I ended up in an e-mail thread leading me to think about how to retrieve the HotSpot ones. This is of course all very unsupported, and counter names/content and even the very API is subject to change at any given release. I am providing this information mostly as a reminder to myself. Who knows, someone might find this useful.

I will now go ahead and show how something similar to the PerformanceCounters MBean available in JRockit can be built. I recommend against doing this in any kind of production scenario, as the PerformanceCounter API is totally unsupported. Please don’t ask me what a certain counter means – you probably shouldn’t be using these in the first place. This is all for lolz. 🙂

Here comes the usual disclaimer:

The following blog entry will describe UNSUPPORTED functionality. This means that relying on the described APIs or functionality may BREAK your code/plugin with any given update of the JDK and/or Mission Control.

Performance Counter Hello World

If you ever used JRockit, you may fondly remember the old jrcmd utility, which now exists in a HotSpot incarnation as jcmd. Well, jrcmd had an option, –l, which would list all the the performance counters in the target JVM. The following code will let you implement something similar to the jrcmd –l command. Note that the PerformanceCounter API resides in a package most IDEs will restrict access to. You will need to change your project settings accordingly for the following to compile.

Anyways, here is a hack to list all the performance counters for a particular PID:

import java.io.IOException;
import java.nio.ByteBuffer;

import sun.management.counter.Counter;
import sun.management.counter.perf.PerfInstrumentation;
import sun.misc.Perf;

public class PerfCounterTest {

    public static void main(String[] args) throws IOException {
        if (args.length != 1) {
            System.out.println(“Usage: PerfCounterTest <PID>”);
            System.exit(2);
        }
        Perf p = Perf.getPerf();
        ByteBuffer buffer = p.attach(Integer.parseInt(args[0]), “r”);
        PerfInstrumentation perfInstrumentation = new PerfInstrumentation(buffer);
        for (Counter counter : perfInstrumentation.getAllCounters()) {
            System.out.println(String.format(
                    “%s = %s [Variability: %s, Units: %s]”, counter.getName(),
                    String.valueOf(counter.getValue()),
                    counter.getVariability(), counter.getUnits()));
        }

    }
}

Note that you will need the “sun.misc.Perf.getPerf” permission to be able to access the Perf instance when running with a security manager.

From here to a fully functional Dynamic MBean, the step is pretty short.

PerformanceCounterMBean á la JRockit for HotSpot

Here is an example of how code exposing the Performance Counters as a Dynamic MBean can look like:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeOperationsException;

import sun.misc.Perf;
import sun.management.counter.Counter;
import sun.management.counter.perf.PerfInstrumentation;

public class PerfCounters implements DynamicMBean {
    public final static ObjectName MBEAN_OBJECT_NAME;
    public final Map<String, Counter> counterMap;
    private final MBeanInfo info;

    static {
        MBEAN_OBJECT_NAME = createObjectName(“se.hirt.management:type=PerfCounters”);
    }

    public PerfCounters() {
        counterMap = setUpCounters();
        info = createMBeanInfo();
    }

    @Override
    public Object getAttribute(String attribute)
            throws AttributeNotFoundException, MBeanException,
            ReflectionException {
        if (attribute == null) {
            throw new RuntimeOperationsException(new IllegalArgumentException(
                    “The attribute name cannot be null.”),
                    “Cannot invoke getAttribute on ” + MBEAN_OBJECT_NAME
                            + ” with null as attribute name.”);
        }
        Counter c = counterMap.get(attribute);
        if (c == null) {
            throw new AttributeNotFoundException(
                    “Could not find the attribute ” + attribute);
        }
        return c.getValue();
    }

    @Override
    public void setAttribute(Attribute attribute)
            throws AttributeNotFoundException, InvalidAttributeValueException,
            MBeanException, ReflectionException {
        if (attribute == null) {
            throw new RuntimeOperationsException(new IllegalArgumentException(
                    “The attribute name cannot be null.”),
                    “Cannot invoke setAttribute on ” + MBEAN_OBJECT_NAME
                            + ” with null as attribute name.”);
        }
        Counter c = counterMap.get(attribute);
        if (c == null) {
            throw new AttributeNotFoundException(
                    “Could not find the attribute ” + attribute + “.”);
        }
        throw new RuntimeOperationsException(
                new UnsupportedOperationException(),
                “All attributes on the PerfCounters MBean are read only.”);
    }

    @Override
    public AttributeList getAttributes(String[] attributes) {
        AttributeList attributeList = new AttributeList();
        for (String attribute : attributes) {
            try {
                attributeList.add(new Attribute(attribute,
                        getAttribute(attribute)));
            } catch (AttributeNotFoundException | MBeanException
                    | ReflectionException e) {
                // Seems this one is not supposed to throw exceptions. Try to
                // get as many as possible.
            }
        }
        return attributeList;
    }

    @Override
    public AttributeList setAttributes(AttributeList attributes) {
        // Seems this one is not supposed to throw exceptions.
        // Just ignore.
        return null;
    }

    @Override
    public Object invoke(String actionName, Object[] params, String[] signature)
            throws MBeanException, ReflectionException {
        throw new MBeanException(new UnsupportedOperationException(
                MBEAN_OBJECT_NAME + ” does not have any operations.”));
    }

    @Override
    public MBeanInfo getMBeanInfo() {
        return info;
    }

    private static ObjectName createObjectName(String name) {
        try {
            return new ObjectName(name);
        } catch (MalformedObjectNameException e) {
            // This will not happen – known to be wellformed.
            e.printStackTrace();
        }
        return null;
    }

    private Map<String, Counter> setUpCounters() {
        Map<String, Counter> counters = new HashMap<>();
        Perf p = Perf.getPerf();
        try {
            ByteBuffer buffer = p.attach(0, “r”);
            PerfInstrumentation perfInstrumentation = new PerfInstrumentation(
                    buffer);
            for (Counter counter : perfInstrumentation.getAllCounters()) {
                counters.put(counter.getName(), counter);
            }
        } catch (IllegalArgumentException | IOException e) {
            System.err.println(“Failed to access performance counters. No counters will be available!”);
            e.printStackTrace();
        }
        return counters;
    }

    private MBeanInfo createMBeanInfo() {
        Collection<Counter> counters = counterMap.values();
        List<MBeanAttributeInfo> attributes = new ArrayList<>(counters.size());
        for (Counter c : counters) {
            if (!c.isVector()) {
                String typeName = “java.lang.String”;
                synchronized (c) {
                    Object value = c.getValue();
                    if (value != null) {
                        typeName = value.getClass().getName();
                    }
                }
                attributes.add(new MBeanAttributeInfo(c.getName(), typeName,
                        String.format(“%s [%s,%s]”, c.getName(), c.getUnits(),
                                c.getVariability()), true, false, false));
            }
        }
        MBeanAttributeInfo[] attributesArray = attributes.toArray(new MBeanAttributeInfo[attributes.size()]);
        return new MBeanInfo(
                this.getClass().getName(),
                “An MBean exposing the available JVM Performance Counters as attributes.”,
                attributesArray, null, null, null);
    }
}

And here is a little test application that registers the MBean with the platform MBean server and then waits around, giving you an opportunity to hook up with Mission Control and try it out:

import java.io.IOException;
import java.lang.management.ManagementFactory;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.NotCompliantMBeanException;

public class PerfCounterMBeanRunner {

    public static void main(String[] args) throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException, IOException {
        ManagementFactory.getPlatformMBeanServer().registerMBean(new PerfCounters(), PerfCounters.MBEAN_OBJECT_NAME);
        System.out.println(“Press enter to quit!”);
        System.in.read();
    }

}

If you use the JMX Console to connect to a JVM where the PerfCounters MBean is registered, you should be able to find the PerfCounters MBean using the MBean Browser. There should be quite a few performance counters available:

JMC_perf_counters

Conclusion

Sample code for using the proprietary JVM performance counter API was provided, as well as example code on how to implement a version of the old jrcmd –l command, as well as the old PerformanceCounter MBean of JRockit. Again, these examples are using proprietary APIs that may break at any given release. Use responsibly!

jrockit_duke_love