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