Geertjan is a DZone Zone Leader and has posted 457 posts at DZone. You can read more from them at their website. View Full User Profile

Another Look at JConsole Plugins

01.30.2008
| 9096 views |
  • submit to reddit
Charlie Hunt, one of Sun's performance gurus, is in town (where "town" equals "Prague, Czech Republic") and we've been studying the JDK's JTop plugin for JConsole, with a view to porting it to a NetBeans module. Let me tell you, it was not easy to figure out, at least initially. JTop consists of a class that extends JConsolePlugin, which integrates with JConsole, as well as a JPanel, which provides the user interface. We ended up dumping the JConsolePlugin class completely. There was nothing we needed from it.

Then we moved most of the content from the sample's other class, i.e, the JPanel, into a TopComponent, which integrates with the NetBeans Platform. We initially simply wanted to call the JPanel from the TopComponent and then add it to the TopComponent. I think that probably failed because the JPanel might have been getting instantiated (at least) twice. So we moved everything into the TopComponent, ran the NetBeans Platform, and there it was:

We also installed it into the Visual VM:

...and into NetBeans IDE:

In the latter case, we encountered a lot of problems because, as we eventually discovered, the Visual Web Pack has a security manager that conflicts with the security manager set in the tools.jar, which is where the JConsole API is located. We tried in vain to set security policies in various places before we discovered the culprit and then simply excluded the Visual Web Pack from the IDE. Then the JTop plugin worked without a problem.

And, because the original JTop plugin uses the cool new JDK6 SwingWorker class, the TopComponent is updated automatically in the background and the thread information is continuously current.

Here is the complete TopComponent. You'll see that we didn't actually add any code ourselves. We had to hardcode the server and port number, currently there doesn't seem to be an obvious way to detect these. The original JTop plugin integrated with JConsole such that JConsole provided this information. Now that JConsole is no longer in the picture, that information needs to be obtained somehow. So, currently we've hardcoded it.

Here's most of the code, excluding the standard TopComponent code that is created by the Window Component wizard, at the end of this snippet:

final class JTopTopComponent extends TopComponent {

private static JTopTopComponent instance;
private static final String PREFERRED_ID = "JTopTopComponent";
private MBeanServerConnection server;
private ThreadMXBean tmbean;
private MyTableModel tmodel;
private String hostname;
private int port;

public JTopTopComponent() {

try {
initComponents();
setName(NbBundle.getMessage(JTopTopComponent.class, "CTL_JTopTopComponent"));
setToolTipText(NbBundle.getMessage(JTopTopComponent.class, "HINT_JTopTopComponent"));

//Here we hardcode localhost and port:
main(new String[]{"localhost:1090"});

//Here's the table, from the original plugin
tmodel = new MyTableModel();
JTable table = new JTable(tmodel);
table.setPreferredScrollableViewportSize(new Dimension(500, 300));

// Set the renderer to format Double
table.setDefaultRenderer(Double.class, new DoubleRenderer());
// Add some space
table.setIntercellSpacing(new Dimension(6, 3));
table.setRowHeight(table.getRowHeight() + 4);

// Create the scroll pane and add the table to it.
JScrollPane scrollPane = new JScrollPane(table);

// Add the scroll pane to this panel.
add(scrollPane);


} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}

}

public void main(String[] args) throws Exception {
// Validate the input arguments
if (args.length != 1) {
usage();
}

String[] arg2 = args[0].split(":");
if (arg2.length != 2) {
usage();
}
hostname = arg2[0];
port = -1;
try {
port = Integer.parseInt(arg2[1]);
} catch (NumberFormatException x) {
usage();
}
if (port < 0) {
usage();
}

//Making the connection:
System.setProperty("netbeans.security.nocheck", "true");
MBeanServerConnection serverMBean = connect(hostname, port);
setMBeanServerConnection(serverMBean);

// A timer task to update GUI per each interval
TimerTask timerTask = new TimerTask() {

public void run() {
// Schedule the SwingWorker to update the GUI
newSwingWorker().execute();
}
};

// refresh every 2 seconds
Timer timer = new Timer("JTop Sampling thread");
timer.schedule(timerTask, 0, 2000);

}

// SwingWorker responsible for updating the GUI
//
// It first gets the thread and CPU usage information as a
// background task done by a worker thread so that
// it will not block the event dispatcher thread.
//
// When the worker thread finishes, the event dispatcher
// thread will invoke the done() method which will update
// the UI.
class Worker extends SwingWorker<List<Map.Entry<Long, ThreadInfo>>, Object> {

private MyTableModel tmodel;

Worker(MyTableModel tmodel) {
this.tmodel = tmodel;
}

// Get the current thread info and CPU time
public List<Map.Entry<Long, ThreadInfo>> doInBackground() {
return getThreadList();
}

// fire table data changed to trigger GUI update
// when doInBackground() is finished
protected void done() {
try {
// Set table model with the new thread list
tmodel.setThreadList(get());
// refresh the table model
tmodel.fireTableDataChanged();
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}
}

private MBeanServerConnection getMBeanServerConnection() {
MBeanServerConnection serverMBean = connect(hostname, port);
return serverMBean;
}

// Return a new SwingWorker for UI update
public SwingWorker<?, ?> newSwingWorker() {
return new Worker(tmodel);
}

// Set the MBeanServerConnection object for communicating
// with the target VM
public void setMBeanServerConnection(MBeanServerConnection mbs) {
this.server = mbs;
try {
this.tmbean = newPlatformMXBeanProxy(server,
THREAD_MXBEAN_NAME,
ThreadMXBean.class);
} catch (IOException e) {
e.printStackTrace();
}
if (!tmbean.isThreadCpuTimeSupported()) {
System.err.println("This VM does not support thread CPU time monitoring");
} else {
tmbean.setThreadCpuTimeEnabled(true);
}
}

class MyTableModel extends AbstractTableModel {

private String[] columnNames = {"ThreadName",
"CPU(sec)",
"State"
};
// List of all threads. The key of each entry is the CPU time
// and its value is the ThreadInfo object with no stack trace.
private List<Map.Entry<Long, ThreadInfo>> threadList =
Collections.EMPTY_LIST;

public MyTableModel() {
}

public int getColumnCount() {
return columnNames.length;
}

public int getRowCount() {
return threadList.size();
}

public String getColumnName(int col) {
return columnNames[col];
}

public Object getValueAt(int row, int col) {
Map.Entry<Long, ThreadInfo> me = threadList.get(row);
switch (col) {
case 0:
// Column 0 shows the thread name
return me.getValue().getThreadName();
case 1:
// Column 1 shows the CPU usage
long ns = me.getKey().longValue();
double sec = ns / 1000000000;
return new Double(sec);
case 2:
// Column 2 shows the thread state
return me.getValue().getThreadState();
default:
return null;
}
}

public Class getColumnClass(int c) {
return getValueAt(0, c).getClass();
}

void setThreadList(List<Map.Entry<Long, ThreadInfo>> list) {
threadList = list;
}
}

/**
* Get the thread list with CPU consumption and the ThreadInfo
* for each thread sorted by the CPU time.
*/
private List<Map.Entry<Long, ThreadInfo>> getThreadList() {
// Get all threads and their ThreadInfo objects
// with no stack trace
long[] tids = tmbean.getAllThreadIds();
ThreadInfo[] tinfos = tmbean.getThreadInfo(tids);

// build a map with key = CPU time and value = ThreadInfo
SortedMap<Long, ThreadInfo> map = new TreeMap<Long, ThreadInfo>();
for (int i = 0; i < tids.length; i++) {
long cpuTime = tmbean.getThreadCpuTime(tids[i]);
// filter out threads that have been terminated
if (cpuTime != -1 && tinfos[i] != null) {
map.put(new Long(cpuTime), tinfos[i]);
}
}

// build the thread list and sort it with CPU time
// in decreasing order
Set<Map.Entry<Long, ThreadInfo>> set = map.entrySet();
List<Map.Entry<Long, ThreadInfo>> list =
new ArrayList<Map.Entry<Long, ThreadInfo>>(set);
Collections.reverse(list);
return list;
}

/**
* Format Double with 4 fraction digits
*/
class DoubleRenderer extends DefaultTableCellRenderer {

NumberFormat formatter;

public DoubleRenderer() {
super();
setHorizontalAlignment(JLabel.RIGHT);
}

public void setValue(Object value) {
if (formatter == null) {
formatter = NumberFormat.getInstance();
formatter.setMinimumFractionDigits(4);
}
setText((value == null) ? "" : formatter.format(value));
}
}

public static MBeanServerConnection connect(String hostname, int port) {
// Create an RMI connector client and connect it to
// the RMI connector server
String urlPath = "/jndi/rmi://" + hostname + ":" + port + "/jmxrmi";
MBeanServerConnection server = null;
try {
JMXServiceURL url = new JMXServiceURL("rmi", "", 0, urlPath);
JMXConnector jmxc = JMXConnectorFactory.connect(url);
server = jmxc.getMBeanServerConnection();
} catch (MalformedURLException e) {
// should not reach here
} catch (IOException e) {
System.err.println("\nCommunication error: " + e.getMessage());
System.exit(1);
}
return server;
}

private static void usage() {
System.out.println("Usage: java JTop <hostname>:<port>");
System.exit(1);
}
//From here on, it's just standard TopComponent boilerplate code:
...
...
...


And that's about it! (Also no reason anymore for the META-INF/services folder with its content, by the way.) If you know the original JTop plugin, you'll recognize most of the code above. The main method indicates that the original was intended to run as a standalone application, as well as a new tab in the JConsole. That's why there's a main method in the above code at all. We should probably rename that to something else and then our work here is done.

Published at DZone with permission of its author, Geertjan Wielenga.
Tags: