スレッド実行率を表示するJTopもどき

JDKのデモに付属してるスレッド実行時間を表示するJTopを基にして、単位時間当たりの実行率を表示させてみた。なんか合計が100%超えてるけど、マルチコアのせいなのか精度が良くないのかバグってるのか。このときのOSでみたCPUの使用率は80〜90%だった。
追記:ループでまわしてるうちに時間が経過して分母のtimeが合わなくなってる?そんなに長くない?
f:id:ihiroky:20090219001216p:image
クラスは小分けにしたけど、全体的な処理はJDK付属デモとそんなに変わらない。取得した実行時間をスレッドIDごとに覚えておいて、次回の実行時間取得時にその差分から実行率を計算する。ThreadMXBeanから時間をとってきてからTableModelに放り込むまでの処理なのでSwingWorkerにやってもらう。

package pstyle.jmx.ejtop;

import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.SwingWorker;

public class SwingWorkerFactory {

    private ThreadMXBean threadMXBean;
    private EJTopTableModel tableModel;
    private long workerPrevTime;
    private Map<Long,Long[]> workerPrevThreadTime;
    
    private static RowComparator rowComparatorById = new RowComparator(0);
    
    protected SwingWorkerFactory(ThreadMXBean threadMXBean, EJTopTableModel tableModel) {
        if ( ! threadMXBean.isThreadCpuTimeSupported()) {
            throw new IllegalArgumentException("this vm doesn't support thread cpu time monitoring.");
        }
        threadMXBean.setThreadCpuTimeEnabled(true);
        this.threadMXBean = threadMXBean;
        this.tableModel = tableModel;
        this.workerPrevTime = System.currentTimeMillis();
        this.workerPrevThreadTime = new HashMap<Long,Long[]>(256);
    }
    
    public SwingWorker<?,?> createSwingWorker() {
        return new UpdateWorker(threadMXBean, tableModel);
    }
    
    private class UpdateWorker extends SwingWorker<List<Object[]>,Object> {

        private ThreadMXBean threadMXBean;
        private EJTopTableModel tableModel;
        
        public UpdateWorker(ThreadMXBean threadMXBean, EJTopTableModel tableModel) {
            this.threadMXBean = threadMXBean;
            this.tableModel = tableModel;
        }
        
        private List<Object[]> getEntryList() {
            long[] tids = threadMXBean.getAllThreadIds();
            List<Object[]> entryList = new ArrayList<Object[]>(128);
            final long curTime = System.currentTimeMillis();
            final double time = curTime - workerPrevTime;
            Map<Long,Long[]> threadTimeMap = new HashMap<Long,Long[]>(256);
            for (long tid : tids) {
                Long[] prev = workerPrevThreadTime.get(tid);
                if (prev == null) { prev = new Long[]{0L, 0L}; }
                long nCpuTimeTotal  = threadMXBean.getThreadCpuTime(tid);
                long nUserTimeTotal = threadMXBean.getThreadUserTime(tid);
                double mCpuTime = (nCpuTimeTotal - prev[0]) / 1000d / 1000d;
                double mUserTime = (nUserTimeTotal - prev[1]) / 1000d / 1000d;
                ThreadInfo info = threadMXBean.getThreadInfo(tid);
                
                Object[] values = new Object[] {
                        tid, // TID
                        info.getThreadName(), // Name
                        mUserTime / time * 100, // CPU(user)
                        (mCpuTime - mUserTime) / time * 100, // CPU(sys)
                        nCpuTimeTotal / 1000000000, // CPU(sec)
                        info.getThreadState() // State
                };
                entryList.add(values);
                threadTimeMap.put(tid, new Long[]{nCpuTimeTotal, nUserTimeTotal});
            }
            workerPrevTime = curTime;
            workerPrevThreadTime = threadTimeMap;
            Collections.sort(entryList, rowComparatorById);
            return entryList;
        }
        
        @Override
        protected List<Object[]> doInBackground() throws Exception {
            return getEntryList();
        }
        
        @Override
        protected void done() {
            try {
                tableModel.setEntryList(get());
                tableModel.fireTableDataChanged();
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

main()でThreadMXBeanを外から入れてやる。Swing関連はクラス内で。JMXClientは監視対象VMが動いているホストとそのポート、URIを指定してMBeanにアクセスするためのクラス。

package pstyle.jmx.ejtop;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;

import pstyle.jmx.JMXClient;

public class EJTop extends JPanel {

    private static final long serialVersionUID = 769684291256475167L;
    
    private SwingWorkerFactory updaterFactory;
        
    public EJTop(ThreadMXBean threadMXBean) {
        super(new GridLayout(1, 0));
        
        EJTopTableModel tableModel = new EJTopTableModel();
        JTable table = new JTable(tableModel);
        table.setDefaultRenderer(Double.class, new DoubleCellRenderer());
        table.setPreferredScrollableViewportSize(new Dimension(500, 300));
        table.setIntercellSpacing(new Dimension(6, 3));
        table.setRowHeight(table.getRowHeight() + 4);
        table.setAutoCreateRowSorter(true);
        
        JScrollPane scrollPane = new JScrollPane(table);
        add(scrollPane);
        
        updaterFactory = new SwingWorkerFactory(threadMXBean, tableModel);
    }
    
    public SwingWorkerFactory createSwingWorkerFactory() {
        return updaterFactory;
    }
    
    public void show() {
        JFrame frame = new JFrame("JTopExt");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JComponent pane = (JComponent)frame.getContentPane();
        pane.add(this, BorderLayout.CENTER);
        pane.setOpaque(true);
        pane.setBorder(new EmptyBorder(1, 1, 1, 1));
        frame.setContentPane(pane);
        frame.pack();
        frame.setVisible(true);
    }
    
    public static void main(String[] args) {
        if (args.length < 2) { usage(); }
        
        String host = args[0];
        int port = -1;
        try {
            port = Integer.parseInt(args[1]);
        } catch (NumberFormatException e) {
            usage();
        }
        
        try {
            // UIManager.setLookAndFeel("com.sun.java.swing.plaf.NimbusLookAndFeel");
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel");
            
            JMXClient client = new JMXClient(host, port, "/jmxrmi");
            ThreadMXBean mxbean = client.getMXBean(ManagementFactory.THREAD_MXBEAN_NAME, ThreadMXBean.class);
            final EJTop jtop = new EJTop(mxbean);
            SwingWorkerFactory factory = jtop.createSwingWorkerFactory();
            SwingUtilities.invokeAndWait(new Runnable() {
                public void run() { jtop.show(); }
            });
            
            // TODO ScheduledThreadPoolExecutor
            TimerTask task = new UpdateTimerTask(factory);
            Timer timer = new Timer("EJTop timer thread");
            timer.schedule(task, 0, 4000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static void usage() {
        System.out.println("usage : java EJTop <address> <port>");
        System.exit(1);
    }
}

監視対象VMには以下のプロパティをつけておく。認証関連はめんどくさいのでとりあえずfalse。

-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=12345 # 上で出てきたポート
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false

Attach APIを利用すればローカルVMに対して上記のプロパティ設定をしなくても監視できる。試さなきゃ。