1/*
2 * Copyright (c) 2004, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package sun.tools.jconsole;
27
28import java.awt.*;
29import java.awt.event.*;
30import java.io.*;
31import java.lang.management.*;
32import java.lang.reflect.*;
33
34import javax.swing.*;
35import javax.swing.border.*;
36import javax.swing.event.*;
37
38
39import java.util.*;
40import java.util.concurrent.*;
41import java.util.List;
42
43import static sun.tools.jconsole.Utilities.*;
44
45
46@SuppressWarnings("serial")
47class ThreadTab extends Tab implements ActionListener, DocumentListener, ListSelectionListener {
48    PlotterPanel threadMeter;
49    TimeComboBox timeComboBox;
50    JTabbedPane threadListTabbedPane;
51    DefaultListModel<Long> listModel;
52    JTextField filterTF;
53    JLabel messageLabel;
54    JSplitPane threadsSplitPane;
55    HashMap<Long, String> nameCache = new HashMap<Long, String>();
56
57    private ThreadOverviewPanel overviewPanel;
58    private boolean plotterListening = false;
59
60
61    private static final String threadCountKey   = "threadCount";
62    private static final String peakKey          = "peak";
63
64    private static final Color  threadCountColor = Plotter.defaultColor;
65    private static final Color  peakColor        = Color.red;
66
67    private static final Border thinEmptyBorder  = new EmptyBorder(2, 2, 2, 2);
68
69    /*
70      Hierarchy of panels and layouts for this tab:
71
72        ThreadTab (BorderLayout)
73
74            North:  topPanel (BorderLayout)
75
76                        Center: controlPanel (FlowLayout)
77                                    timeComboBox
78
79            Center: plotterPanel (BorderLayout)
80
81                        Center: plotter
82
83    */
84
85
86    public static String getTabName() {
87        return Messages.THREADS;
88    }
89
90    public ThreadTab(VMPanel vmPanel) {
91        super(vmPanel, getTabName());
92
93        setLayout(new BorderLayout(0, 0));
94        setBorder(new EmptyBorder(4, 4, 3, 4));
95
96        JPanel topPanel     = new JPanel(new BorderLayout());
97        JPanel plotterPanel = new JPanel(new VariableGridLayout(0, 1, 4, 4, true, true));
98
99        add(topPanel, BorderLayout.NORTH);
100        add(plotterPanel,  BorderLayout.CENTER);
101
102        JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 5));
103        topPanel.add(controlPanel, BorderLayout.CENTER);
104
105        threadMeter = new PlotterPanel(Messages.NUMBER_OF_THREADS,
106                                       Plotter.Unit.NONE, true);
107        threadMeter.plotter.createSequence(threadCountKey, Messages.LIVE_THREADS,  threadCountColor, true);
108        threadMeter.plotter.createSequence(peakKey,        Messages.PEAK,         peakColor,        true);
109        setAccessibleName(threadMeter.plotter,
110                          Messages.THREAD_TAB_THREAD_PLOTTER_ACCESSIBLE_NAME);
111
112        plotterPanel.add(threadMeter);
113
114        timeComboBox = new TimeComboBox(threadMeter.plotter);
115        controlPanel.add(new LabeledComponent(Messages.TIME_RANGE_COLON,
116                                              Resources.getMnemonicInt(Messages.TIME_RANGE_COLON),
117                                              timeComboBox));
118
119        listModel = new DefaultListModel<Long>();
120
121        JTextArea textArea = new JTextArea();
122        textArea.setBorder(thinEmptyBorder);
123        textArea.setEditable(false);
124        setAccessibleName(textArea,
125                          Messages.THREAD_TAB_THREAD_INFO_ACCESSIBLE_NAME);
126        ThreadJList list = new ThreadJList(listModel, textArea);
127
128        Dimension di = new Dimension(super.getPreferredSize());
129        di.width = Math.min(di.width, 200);
130
131        JScrollPane threadlistSP = new JScrollPane(list);
132        threadlistSP.setPreferredSize(di);
133        threadlistSP.setBorder(null);
134
135        JScrollPane textAreaSP = new JScrollPane(textArea);
136        textAreaSP.setBorder(null);
137
138        threadListTabbedPane = new JTabbedPane(JTabbedPane.TOP);
139        threadsSplitPane  = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
140                                           threadlistSP, textAreaSP);
141        threadsSplitPane.setOneTouchExpandable(true);
142        threadsSplitPane.setBorder(null);
143
144        JPanel firstTabPanel = new JPanel(new BorderLayout());
145        firstTabPanel.setOpaque(false);
146
147        JPanel firstTabToolPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2));
148        firstTabToolPanel.setOpaque(false);
149
150        filterTF = new PromptingTextField("Filter", 20);
151        filterTF.getDocument().addDocumentListener(this);
152        firstTabToolPanel.add(filterTF);
153
154        JSeparator separator = new JSeparator(JSeparator.VERTICAL);
155        separator.setPreferredSize(new Dimension(separator.getPreferredSize().width,
156                                                 filterTF.getPreferredSize().height));
157        firstTabToolPanel.add(separator);
158
159        JButton detectDeadlockButton = new JButton(Messages.DETECT_DEADLOCK);
160        detectDeadlockButton.setMnemonic(Resources.getMnemonicInt(Messages.DETECT_DEADLOCK));
161        detectDeadlockButton.setActionCommand("detectDeadlock");
162        detectDeadlockButton.addActionListener(this);
163        detectDeadlockButton.setToolTipText(Messages.DETECT_DEADLOCK_TOOLTIP);
164        firstTabToolPanel.add(detectDeadlockButton);
165
166        messageLabel = new JLabel();
167        firstTabToolPanel.add(messageLabel);
168
169        firstTabPanel.add(threadsSplitPane, BorderLayout.CENTER);
170        firstTabPanel.add(firstTabToolPanel, BorderLayout.SOUTH);
171        threadListTabbedPane.addTab(Messages.THREADS, firstTabPanel);
172
173        plotterPanel.add(threadListTabbedPane);
174    }
175
176    private long oldThreads[] = new long[0];
177
178    public SwingWorker<?, ?> newSwingWorker() {
179        final ProxyClient proxyClient = vmPanel.getProxyClient();
180
181        if (!plotterListening) {
182            proxyClient.addWeakPropertyChangeListener(threadMeter.plotter);
183            plotterListening = true;
184        }
185
186        return new SwingWorker<Boolean, Object>() {
187            private int tlCount;
188            private int tpCount;
189            private long ttCount;
190            private long[] threads;
191            private long timeStamp;
192
193            public Boolean doInBackground() {
194                try {
195                    ThreadMXBean threadMBean = proxyClient.getThreadMXBean();
196
197                    tlCount = threadMBean.getThreadCount();
198                    tpCount = threadMBean.getPeakThreadCount();
199                    if (overviewPanel != null) {
200                        ttCount = threadMBean.getTotalStartedThreadCount();
201                    } else {
202                        ttCount = 0L;
203                    }
204
205                    threads = threadMBean.getAllThreadIds();
206                    for (long newThread : threads) {
207                        if (nameCache.get(newThread) == null) {
208                            ThreadInfo ti = threadMBean.getThreadInfo(newThread);
209                            if (ti != null) {
210                                String name = ti.getThreadName();
211                                if (name != null) {
212                                    nameCache.put(newThread, name);
213                                }
214                            }
215                        }
216                    }
217                    timeStamp = System.currentTimeMillis();
218                    return true;
219                } catch (IOException e) {
220                    return false;
221                } catch (UndeclaredThrowableException e) {
222                    return false;
223                }
224            }
225
226            protected void done() {
227                try {
228                    if (!get()) {
229                        return;
230                    }
231                } catch (InterruptedException ex) {
232                    return;
233                } catch (ExecutionException ex) {
234                    if (JConsole.isDebug()) {
235                        ex.printStackTrace();
236                    }
237                    return;
238                }
239
240                threadMeter.plotter.addValues(timeStamp, tlCount, tpCount);
241                threadMeter.setValueLabel(tlCount+"");
242
243                if (overviewPanel != null) {
244                    overviewPanel.updateThreadsInfo(tlCount, tpCount, ttCount, timeStamp);
245                }
246
247                String filter = filterTF.getText().toLowerCase(Locale.ENGLISH);
248                boolean doFilter = (filter.length() > 0);
249
250                ArrayList<Long> l = new ArrayList<Long>();
251                for (long t : threads) {
252                    l.add(t);
253                }
254                Iterator<Long> iterator = l.iterator();
255                while (iterator.hasNext()) {
256                    long newThread = iterator.next();
257                    String name = nameCache.get(newThread);
258                    if (doFilter && name != null &&
259                        name.toLowerCase(Locale.ENGLISH).indexOf(filter) < 0) {
260
261                        iterator.remove();
262                    }
263                }
264                long[] newThreads = threads;
265                if (l.size() < threads.length) {
266                    newThreads = new long[l.size()];
267                    for (int i = 0; i < newThreads.length; i++) {
268                        newThreads[i] = l.get(i);
269                    }
270                }
271
272
273                for (long oldThread : oldThreads) {
274                    boolean found = false;
275                    for (long newThread : newThreads) {
276                        if (newThread == oldThread) {
277                            found = true;
278                            break;
279                        }
280                    }
281                    if (!found) {
282                        listModel.removeElement(oldThread);
283                        if (!doFilter) {
284                            nameCache.remove(oldThread);
285                        }
286                    }
287                }
288
289                // Threads are in reverse chronological order
290                for (int i = newThreads.length - 1; i >= 0; i--) {
291                    long newThread = newThreads[i];
292                    boolean found = false;
293                    for (long oldThread : oldThreads) {
294                        if (newThread == oldThread) {
295                            found = true;
296                            break;
297                        }
298                    }
299                    if (!found) {
300                        listModel.addElement(newThread);
301                    }
302                }
303                oldThreads = newThreads;
304            }
305        };
306    }
307
308    long lastSelected = -1;
309
310    public void valueChanged(ListSelectionEvent ev) {
311        ThreadJList list = (ThreadJList)ev.getSource();
312        final JTextArea textArea = list.textArea;
313
314        Long selected = list.getSelectedValue();
315        if (selected == null) {
316            if (lastSelected != -1) {
317                selected = lastSelected;
318            }
319        } else {
320            lastSelected = selected;
321        }
322        textArea.setText("");
323        if (selected != null) {
324            final long threadID = selected;
325            workerAdd(new Runnable() {
326                public void run() {
327                    ProxyClient proxyClient = vmPanel.getProxyClient();
328                    StringBuilder sb = new StringBuilder();
329                    try {
330                        ThreadMXBean threadMBean = proxyClient.getThreadMXBean();
331                        ThreadInfo ti = null;
332                        MonitorInfo[] monitors = null;
333                        if (proxyClient.isLockUsageSupported() &&
334                              threadMBean.isObjectMonitorUsageSupported()) {
335                            // VMs that support the monitor usage monitoring
336                            ThreadInfo[] infos = threadMBean.dumpAllThreads(true, false);
337                            for (ThreadInfo info : infos) {
338                                if (info.getThreadId() == threadID) {
339                                    ti = info;
340                                    monitors = info.getLockedMonitors();
341                                    break;
342                                }
343                            }
344                        } else {
345                            // VM doesn't support monitor usage monitoring
346                            ti = threadMBean.getThreadInfo(threadID, Integer.MAX_VALUE);
347                        }
348                        if (ti != null) {
349                            if (ti.getLockName() == null) {
350                                sb.append(Resources.format(Messages.NAME_STATE,
351                                              ti.getThreadName(),
352                                              ti.getThreadState().toString()));
353                            } else if (ti.getLockOwnerName() == null) {
354                                sb.append(Resources.format(Messages.NAME_STATE_LOCK_NAME,
355                                              ti.getThreadName(),
356                                              ti.getThreadState().toString(),
357                                              ti.getLockName()));
358                            } else {
359                                sb.append(Resources.format(Messages.NAME_STATE_LOCK_NAME_LOCK_OWNER,
360                                              ti.getThreadName(),
361                                              ti.getThreadState().toString(),
362                                              ti.getLockName(),
363                                              ti.getLockOwnerName()));
364                            }
365                            sb.append(Resources.format(Messages.BLOCKED_COUNT_WAITED_COUNT,
366                                              ti.getBlockedCount(),
367                                              ti.getWaitedCount()));
368                            sb.append(Messages.STACK_TRACE);
369                            int index = 0;
370                            for (StackTraceElement e : ti.getStackTrace()) {
371                                sb.append(e).append('\n');
372                                if (monitors != null) {
373                                    for (MonitorInfo mi : monitors) {
374                                        if (mi.getLockedStackDepth() == index) {
375                                            sb.append(Resources.format(Messages.MONITOR_LOCKED, mi.toString()));
376                                        }
377                                    }
378                                }
379                                index++;
380                            }
381                        }
382                    } catch (IOException ex) {
383                        // Ignore
384                    } catch (UndeclaredThrowableException e) {
385                        proxyClient.markAsDead();
386                    }
387                    final String text = sb.toString();
388                    SwingUtilities.invokeLater(new Runnable() {
389                        public void run() {
390                            textArea.setText(text);
391                            textArea.setCaretPosition(0);
392                        }
393                    });
394                }
395            });
396        }
397    }
398
399    private void doUpdate() {
400        workerAdd(new Runnable() {
401            public void run() {
402                update();
403            }
404        });
405    }
406
407
408    private void detectDeadlock() {
409        workerAdd(new Runnable() {
410            public void run() {
411                try {
412                    final Long[][] deadlockedThreads = getDeadlockedThreadIds();
413
414                    if (deadlockedThreads == null || deadlockedThreads.length == 0) {
415                        // Display message for 30 seconds. Do it on a separate thread so
416                        // the sleep won't hold up the worker queue.
417                        // This will be replaced later by separate statusbar logic.
418                        new Thread() {
419                            public void run() {
420                                try {
421                                    SwingUtilities.invokeAndWait(new Runnable() {
422                                        public void run() {
423                                            String msg = Messages.NO_DEADLOCK_DETECTED;
424                                            messageLabel.setText(msg);
425                                            threadListTabbedPane.revalidate();
426                                        }
427                                    });
428                                    sleep(30 * 1000);
429                                } catch (InterruptedException ex) {
430                                    // Ignore
431                                } catch (InvocationTargetException ex) {
432                                    // Ignore
433                                }
434                                SwingUtilities.invokeLater(new Runnable() {
435                                    public void run() {
436                                        messageLabel.setText("");
437                                    }
438                                });
439                            }
440                        }.start();
441                        return;
442                    }
443
444                    SwingUtilities.invokeLater(new Runnable() {
445                        public void run() {
446                            // Remove old deadlock tabs
447                            while (threadListTabbedPane.getTabCount() > 1) {
448                                threadListTabbedPane.removeTabAt(1);
449                            }
450
451                            if (deadlockedThreads != null) {
452                                for (int i = 0; i < deadlockedThreads.length; i++) {
453                                    DefaultListModel<Long> listModel = new DefaultListModel<Long>();
454                                    JTextArea textArea = new JTextArea();
455                                    textArea.setBorder(thinEmptyBorder);
456                                    textArea.setEditable(false);
457                                    setAccessibleName(textArea,
458                                                      Messages.THREAD_TAB_THREAD_INFO_ACCESSIBLE_NAME);
459                                    ThreadJList list = new ThreadJList(listModel, textArea);
460                                    JScrollPane threadlistSP = new JScrollPane(list);
461                                    JScrollPane textAreaSP = new JScrollPane(textArea);
462                                    threadlistSP.setBorder(null);
463                                    textAreaSP.setBorder(null);
464                                    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
465                                                                                 threadlistSP, textAreaSP);
466                                    splitPane.setOneTouchExpandable(true);
467                                    splitPane.setBorder(null);
468                                    splitPane.setDividerLocation(threadsSplitPane.getDividerLocation());
469                                    String tabName;
470                                    if (deadlockedThreads.length > 1) {
471                                        tabName = Resources.format(Messages.DEADLOCK_TAB_N, i+1);
472                                    } else {
473                                        tabName = Messages.DEADLOCK_TAB;
474                                    }
475                                    threadListTabbedPane.addTab(tabName, splitPane);
476
477                                    for (long t : deadlockedThreads[i]) {
478                                        listModel.addElement(t);
479                                    }
480                                }
481                                threadListTabbedPane.setSelectedIndex(1);
482                            }
483                        }
484                    });
485                } catch (IOException e) {
486                    // Ignore
487                } catch (UndeclaredThrowableException e) {
488                    vmPanel.getProxyClient().markAsDead();
489                }
490            }
491        });
492    }
493
494
495    // Return deadlocked threads or null
496    public Long[][] getDeadlockedThreadIds() throws IOException {
497        ProxyClient proxyClient = vmPanel.getProxyClient();
498        ThreadMXBean threadMBean = proxyClient.getThreadMXBean();
499
500        long[] ids = proxyClient.findDeadlockedThreads();
501        if (ids == null) {
502            return null;
503        }
504        ThreadInfo[] infos = threadMBean.getThreadInfo(ids, Integer.MAX_VALUE);
505
506        List<Long[]> dcycles = new ArrayList<Long[]>();
507        List<Long> cycle = new ArrayList<Long>();
508
509        // keep track of which thread is visited
510        // one thread can only be in one cycle
511        boolean[] visited = new boolean[ids.length];
512
513        int deadlockedThread = -1; // Index into arrays
514        while (true) {
515            if (deadlockedThread < 0) {
516                if (cycle.size() > 0) {
517                    // a cycle found
518                    dcycles.add(cycle.toArray(new Long[0]));
519                    cycle = new ArrayList<Long>();
520                }
521                // start a new cycle from a non-visited thread
522                for (int j = 0; j < ids.length; j++) {
523                    if (!visited[j]) {
524                        deadlockedThread = j;
525                        visited[j] = true;
526                        break;
527                    }
528                }
529                if (deadlockedThread < 0) {
530                    // done
531                    break;
532                }
533            }
534
535            cycle.add(ids[deadlockedThread]);
536            long nextThreadId = infos[deadlockedThread].getLockOwnerId();
537            for (int j = 0; j < ids.length; j++) {
538                ThreadInfo ti = infos[j];
539                if (ti.getThreadId() == nextThreadId) {
540                     if (visited[j]) {
541                         deadlockedThread = -1;
542                     } else {
543                         deadlockedThread = j;
544                         visited[j] = true;
545                     }
546                     break;
547                }
548            }
549        }
550        return dcycles.toArray(new Long[0][0]);
551    }
552
553
554
555
556
557    // ActionListener interface
558    public void actionPerformed(ActionEvent evt) {
559        String cmd = ((AbstractButton)evt.getSource()).getActionCommand();
560
561        if (cmd == "detectDeadlock") {
562            messageLabel.setText("");
563            detectDeadlock();
564        }
565    }
566
567
568
569    // DocumentListener interface
570
571    public void insertUpdate(DocumentEvent e) {
572        doUpdate();
573    }
574
575    public void removeUpdate(DocumentEvent e) {
576        doUpdate();
577    }
578
579    public void changedUpdate(DocumentEvent e) {
580        doUpdate();
581    }
582
583
584
585    private class ThreadJList extends JList<Long> {
586        private JTextArea textArea;
587
588        ThreadJList(DefaultListModel<Long> listModel, JTextArea textArea) {
589            super(listModel);
590
591            this.textArea = textArea;
592
593            setBorder(thinEmptyBorder);
594
595            setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
596            textArea.setText(Messages.THREAD_TAB_INITIAL_STACK_TRACE_MESSAGE);
597            addListSelectionListener(ThreadTab.this);
598            setCellRenderer(new DefaultListCellRenderer() {
599                public Component getListCellRendererComponent(JList<?> list, Object value, int index,
600                                                              boolean isSelected, boolean cellHasFocus) {
601                    super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
602
603                    if (value != null) {
604                        String name = nameCache.get(value);
605                        if (name == null) {
606                            name = value.toString();
607                        }
608                        setText(name);
609                    }
610                    return this;
611                }
612            });
613        }
614
615        public Dimension getPreferredSize() {
616            Dimension d = super.getPreferredSize();
617            d.width = Math.max(d.width, 100);
618            return d;
619        }
620    }
621
622    private class PromptingTextField extends JTextField implements FocusListener {
623        private String prompt;
624        boolean promptRemoved = false;
625        Color fg;
626
627        public PromptingTextField(String prompt, int columns) {
628            super(prompt, columns);
629
630            this.prompt = prompt;
631            updateForeground();
632            addFocusListener(this);
633            setAccessibleName(this, prompt);
634        }
635
636        @Override
637        public void revalidate() {
638            super.revalidate();
639            updateForeground();
640        }
641
642        private void updateForeground() {
643            this.fg = UIManager.getColor("TextField.foreground");
644            if (promptRemoved) {
645                setForeground(fg);
646            } else {
647                setForeground(Color.gray);
648            }
649        }
650
651        public String getText() {
652            if (!promptRemoved) {
653                return "";
654            } else {
655                return super.getText();
656            }
657        }
658
659        public void focusGained(FocusEvent e) {
660            if (!promptRemoved) {
661                setText("");
662                setForeground(fg);
663                promptRemoved = true;
664            }
665        }
666
667        public void focusLost(FocusEvent e) {
668            if (promptRemoved && getText().equals("")) {
669                setText(prompt);
670                setForeground(Color.gray);
671                promptRemoved = false;
672            }
673        }
674
675    }
676
677    OverviewPanel[] getOverviewPanels() {
678        if (overviewPanel == null) {
679            overviewPanel = new ThreadOverviewPanel();
680        }
681        return new OverviewPanel[] { overviewPanel };
682    }
683
684
685    private static class ThreadOverviewPanel extends OverviewPanel {
686        ThreadOverviewPanel() {
687            super(Messages.THREADS, threadCountKey,  Messages.LIVE_THREADS, null);
688        }
689
690        private void updateThreadsInfo(long tlCount, long tpCount, long ttCount, long timeStamp) {
691            getPlotter().addValues(timeStamp, tlCount);
692            getInfoLabel().setText(Resources.format(Messages.THREAD_TAB_INFO_LABEL_FORMAT, tlCount, tpCount, ttCount));
693        }
694    }
695}
696