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 
  26 package sun.tools.jconsole;
  27 
  28 import java.awt.*;
  29 import java.awt.event.*;
  30 import java.io.*;
  31 import java.lang.management.*;
  32 import java.lang.reflect.*;
  33 
  34 import javax.swing.*;
  35 import javax.swing.border.*;
  36 import javax.swing.event.*;
  37 
  38 
  39 import java.util.*;
  40 import java.util.concurrent.*;
  41 import java.util.List;
  42 
  43 import static sun.tools.jconsole.Utilities.*;
  44 
  45 
  46 @SuppressWarnings("serial")
  47 class 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().isEmpty()) {
 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 }