1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ddmuilib; 18 19 import com.android.ddmlib.Client; 20 import com.android.ddmlib.ThreadInfo; 21 import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; 22 23 import org.eclipse.jface.preference.IPreferenceStore; 24 import org.eclipse.jface.viewers.DoubleClickEvent; 25 import org.eclipse.jface.viewers.IDoubleClickListener; 26 import org.eclipse.jface.viewers.ILabelProviderListener; 27 import org.eclipse.jface.viewers.ISelection; 28 import org.eclipse.jface.viewers.ISelectionChangedListener; 29 import org.eclipse.jface.viewers.IStructuredContentProvider; 30 import org.eclipse.jface.viewers.IStructuredSelection; 31 import org.eclipse.jface.viewers.ITableLabelProvider; 32 import org.eclipse.jface.viewers.SelectionChangedEvent; 33 import org.eclipse.jface.viewers.TableViewer; 34 import org.eclipse.jface.viewers.Viewer; 35 import org.eclipse.swt.SWT; 36 import org.eclipse.swt.SWTException; 37 import org.eclipse.swt.custom.StackLayout; 38 import org.eclipse.swt.events.SelectionAdapter; 39 import org.eclipse.swt.events.SelectionEvent; 40 import org.eclipse.swt.graphics.Color; 41 import org.eclipse.swt.graphics.Image; 42 import org.eclipse.swt.graphics.Rectangle; 43 import org.eclipse.swt.layout.FormAttachment; 44 import org.eclipse.swt.layout.FormData; 45 import org.eclipse.swt.layout.FormLayout; 46 import org.eclipse.swt.layout.GridData; 47 import org.eclipse.swt.layout.GridLayout; 48 import org.eclipse.swt.widgets.Button; 49 import org.eclipse.swt.widgets.Composite; 50 import org.eclipse.swt.widgets.Control; 51 import org.eclipse.swt.widgets.Display; 52 import org.eclipse.swt.widgets.Event; 53 import org.eclipse.swt.widgets.Label; 54 import org.eclipse.swt.widgets.Listener; 55 import org.eclipse.swt.widgets.Sash; 56 import org.eclipse.swt.widgets.Table; 57 58 import java.util.Date; 59 60 /** 61 * Base class for our information panels. 62 */ 63 public class ThreadPanel extends TablePanel { 64 65 private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$ 66 private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$ 67 private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$ 68 private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$ 69 private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$ 70 private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$ 71 72 private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$ 73 74 private static final String PREFS_STACK_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$ 75 private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$ 76 private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$ 77 private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$ 78 private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$ 79 80 private Display mDisplay; 81 private Composite mBase; 82 private Label mNotEnabled; 83 private Label mNotSelected; 84 85 private Composite mThreadBase; 86 private Table mThreadTable; 87 private TableViewer mThreadViewer; 88 89 private Composite mStackTraceBase; 90 private Button mRefreshStackTraceButton; 91 private Label mStackTraceTimeLabel; 92 private StackTracePanel mStackTracePanel; 93 private Table mStackTraceTable; 94 95 /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */ 96 private boolean mMustStopRecurringThreadUpdate = false; 97 /** Flag to tell the recurring thread update to stop running */ 98 private boolean mRecurringThreadUpdateRunning = false; 99 100 private Object mLock = new Object(); 101 102 private static final String[] THREAD_STATUS = { 103 "zombie", "running", "timed-wait", "monitor", 104 "wait", "init", "start", "native", "vmwait" 105 }; 106 107 /** 108 * Content Provider to display the threads of a client. 109 * Expected input is a {@link Client} object. 110 */ 111 private static class ThreadContentProvider implements IStructuredContentProvider { 112 public Object[] getElements(Object inputElement) { 113 if (inputElement instanceof Client) { 114 return ((Client)inputElement).getClientData().getThreads(); 115 } 116 117 return new Object[0]; 118 } 119 120 public void dispose() { 121 // pass 122 } 123 124 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 125 // pass 126 } 127 } 128 129 130 /** 131 * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be 132 * of type {@link ThreadInfo}. 133 */ 134 private static class ThreadLabelProvider implements ITableLabelProvider { 135 136 public Image getColumnImage(Object element, int columnIndex) { 137 return null; 138 } 139 140 public String getColumnText(Object element, int columnIndex) { 141 if (element instanceof ThreadInfo) { 142 ThreadInfo thread = (ThreadInfo)element; 143 switch (columnIndex) { 144 case 0: 145 return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$ 146 String.valueOf(thread.getThreadId()); 147 case 1: 148 return String.valueOf(thread.getTid()); 149 case 2: 150 if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length) 151 return THREAD_STATUS[thread.getStatus()]; 152 return "unknown"; 153 case 3: 154 return String.valueOf(thread.getUtime()); 155 case 4: 156 return String.valueOf(thread.getStime()); 157 case 5: 158 return thread.getThreadName(); 159 } 160 } 161 162 return null; 163 } 164 165 public void addListener(ILabelProviderListener listener) { 166 // pass 167 } 168 169 public void dispose() { 170 // pass 171 } 172 173 public boolean isLabelProperty(Object element, String property) { 174 // pass 175 return false; 176 } 177 178 public void removeListener(ILabelProviderListener listener) { 179 // pass 180 } 181 } 182 183 /** 184 * Create our control(s). 185 */ 186 @Override 187 protected Control createControl(Composite parent) { 188 mDisplay = parent.getDisplay(); 189 190 final IPreferenceStore store = DdmUiPreferences.getStore(); 191 192 mBase = new Composite(parent, SWT.NONE); 193 mBase.setLayout(new StackLayout()); 194 195 // UI for thread not enabled 196 mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP); 197 mNotEnabled.setText("Thread updates not enabled for selected client\n" 198 + "(use toolbar button to enable)"); 199 200 // UI for not client selected 201 mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP); 202 mNotSelected.setText("no client is selected"); 203 204 // base composite for selected client with enabled thread update. 205 mThreadBase = new Composite(mBase, SWT.NONE); 206 mThreadBase.setLayout(new FormLayout()); 207 208 // table above the sash 209 mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION); 210 mThreadTable.setHeaderVisible(true); 211 mThreadTable.setLinesVisible(true); 212 213 TableHelper.createTableColumn( 214 mThreadTable, 215 "ID", 216 SWT.RIGHT, 217 "888", //$NON-NLS-1$ 218 PREFS_THREAD_COL_ID, store); 219 220 TableHelper.createTableColumn( 221 mThreadTable, 222 "Tid", 223 SWT.RIGHT, 224 "88888", //$NON-NLS-1$ 225 PREFS_THREAD_COL_TID, store); 226 227 TableHelper.createTableColumn( 228 mThreadTable, 229 "Status", 230 SWT.LEFT, 231 "timed-wait", //$NON-NLS-1$ 232 PREFS_THREAD_COL_STATUS, store); 233 234 TableHelper.createTableColumn( 235 mThreadTable, 236 "utime", 237 SWT.RIGHT, 238 "utime", //$NON-NLS-1$ 239 PREFS_THREAD_COL_UTIME, store); 240 241 TableHelper.createTableColumn( 242 mThreadTable, 243 "stime", 244 SWT.RIGHT, 245 "utime", //$NON-NLS-1$ 246 PREFS_THREAD_COL_STIME, store); 247 248 TableHelper.createTableColumn( 249 mThreadTable, 250 "Name", 251 SWT.LEFT, 252 "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$ 253 PREFS_THREAD_COL_NAME, store); 254 255 mThreadViewer = new TableViewer(mThreadTable); 256 mThreadViewer.setContentProvider(new ThreadContentProvider()); 257 mThreadViewer.setLabelProvider(new ThreadLabelProvider()); 258 259 mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() { 260 public void selectionChanged(SelectionChangedEvent event) { 261 ThreadInfo selectedThread = getThreadSelection(event.getSelection()); 262 updateThreadStackTrace(selectedThread); 263 } 264 }); 265 mThreadViewer.addDoubleClickListener(new IDoubleClickListener() { 266 public void doubleClick(DoubleClickEvent event) { 267 ThreadInfo selectedThread = getThreadSelection(event.getSelection()); 268 if (selectedThread != null) { 269 Client client = (Client)mThreadViewer.getInput(); 270 271 if (client != null) { 272 client.requestThreadStackTrace(selectedThread.getThreadId()); 273 } 274 } 275 } 276 }); 277 278 // the separating sash 279 final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL); 280 Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); 281 sash.setBackground(darkGray); 282 283 // the UI below the sash 284 mStackTraceBase = new Composite(mThreadBase, SWT.NONE); 285 mStackTraceBase.setLayout(new GridLayout(2, false)); 286 287 mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH); 288 mRefreshStackTraceButton.setText("Refresh"); 289 mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() { 290 @Override 291 public void widgetSelected(SelectionEvent e) { 292 ThreadInfo selectedThread = getThreadSelection(null); 293 if (selectedThread != null) { 294 Client currentClient = getCurrentClient(); 295 if (currentClient != null) { 296 currentClient.requestThreadStackTrace(selectedThread.getThreadId()); 297 } 298 } 299 } 300 }); 301 302 mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE); 303 mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 304 305 mStackTracePanel = new StackTracePanel(); 306 mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, 307 PREFS_STACK_COL_CLASS, 308 PREFS_STACK_COL_METHOD, 309 PREFS_STACK_COL_FILE, 310 PREFS_STACK_COL_LINE, 311 PREFS_STACK_COL_NATIVE, 312 store); 313 314 GridData gd; 315 mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); 316 gd.horizontalSpan = 2; 317 318 // now setup the sash. 319 // form layout data 320 FormData data = new FormData(); 321 data.top = new FormAttachment(0, 0); 322 data.bottom = new FormAttachment(sash, 0); 323 data.left = new FormAttachment(0, 0); 324 data.right = new FormAttachment(100, 0); 325 mThreadTable.setLayoutData(data); 326 327 final FormData sashData = new FormData(); 328 if (store != null && store.contains(PREFS_THREAD_SASH)) { 329 sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH)); 330 } else { 331 sashData.top = new FormAttachment(50,0); // 50% across 332 } 333 sashData.left = new FormAttachment(0, 0); 334 sashData.right = new FormAttachment(100, 0); 335 sash.setLayoutData(sashData); 336 337 data = new FormData(); 338 data.top = new FormAttachment(sash, 0); 339 data.bottom = new FormAttachment(100, 0); 340 data.left = new FormAttachment(0, 0); 341 data.right = new FormAttachment(100, 0); 342 mStackTraceBase.setLayoutData(data); 343 344 // allow resizes, but cap at minPanelWidth 345 sash.addListener(SWT.Selection, new Listener() { 346 public void handleEvent(Event e) { 347 Rectangle sashRect = sash.getBounds(); 348 Rectangle panelRect = mThreadBase.getClientArea(); 349 int bottom = panelRect.height - sashRect.height - 100; 350 e.y = Math.max(Math.min(e.y, bottom), 100); 351 if (e.y != sashRect.y) { 352 sashData.top = new FormAttachment(0, e.y); 353 store.setValue(PREFS_THREAD_SASH, e.y); 354 mThreadBase.layout(); 355 } 356 } 357 }); 358 359 ((StackLayout)mBase.getLayout()).topControl = mNotSelected; 360 361 return mBase; 362 } 363 364 /** 365 * Sets the focus to the proper control inside the panel. 366 */ 367 @Override 368 public void setFocus() { 369 mThreadTable.setFocus(); 370 } 371 372 /** 373 * Sent when an existing client information changed. 374 * <p/> 375 * This is sent from a non UI thread. 376 * @param client the updated client. 377 * @param changeMask the bit mask describing the changed properties. It can contain 378 * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} 379 * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, 380 * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, 381 * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} 382 * 383 * @see IClientChangeListener#clientChanged(Client, int) 384 */ 385 public void clientChanged(final Client client, int changeMask) { 386 if (client == getCurrentClient()) { 387 if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 || 388 (changeMask & Client.CHANGE_THREAD_DATA) != 0) { 389 try { 390 mThreadTable.getDisplay().asyncExec(new Runnable() { 391 public void run() { 392 clientSelected(); 393 } 394 }); 395 } catch (SWTException e) { 396 // widget is disposed, we do nothing 397 } 398 } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) { 399 try { 400 mThreadTable.getDisplay().asyncExec(new Runnable() { 401 public void run() { 402 updateThreadStackCall(); 403 } 404 }); 405 } catch (SWTException e) { 406 // widget is disposed, we do nothing 407 } 408 } 409 } 410 } 411 412 /** 413 * Sent when a new device is selected. The new device can be accessed 414 * with {@link #getCurrentDevice()}. 415 */ 416 @Override 417 public void deviceSelected() { 418 // pass 419 } 420 421 /** 422 * Sent when a new client is selected. The new client can be accessed 423 * with {@link #getCurrentClient()}. 424 */ 425 @Override 426 public void clientSelected() { 427 if (mThreadTable.isDisposed()) { 428 return; 429 } 430 431 Client client = getCurrentClient(); 432 433 mStackTracePanel.setCurrentClient(client); 434 435 if (client != null) { 436 if (!client.isThreadUpdateEnabled()) { 437 ((StackLayout)mBase.getLayout()).topControl = mNotEnabled; 438 mThreadViewer.setInput(null); 439 440 // if we are currently updating the thread, stop doing it. 441 mMustStopRecurringThreadUpdate = true; 442 } else { 443 ((StackLayout)mBase.getLayout()).topControl = mThreadBase; 444 mThreadViewer.setInput(client); 445 446 synchronized (mLock) { 447 // if we're not updating we start the process 448 if (mRecurringThreadUpdateRunning == false) { 449 startRecurringThreadUpdate(); 450 } else if (mMustStopRecurringThreadUpdate) { 451 // else if there's a runnable that's still going to get called, lets 452 // simply cancel the stop, and keep going 453 mMustStopRecurringThreadUpdate = false; 454 } 455 } 456 } 457 } else { 458 ((StackLayout)mBase.getLayout()).topControl = mNotSelected; 459 mThreadViewer.setInput(null); 460 } 461 462 mBase.layout(); 463 } 464 465 /** 466 * Updates the stack call of the currently selected thread. 467 * <p/> 468 * This <b>must</b> be called from the UI thread. 469 */ 470 private void updateThreadStackCall() { 471 Client client = getCurrentClient(); 472 if (client != null) { 473 // get the current selection in the ThreadTable 474 ThreadInfo selectedThread = getThreadSelection(null); 475 476 if (selectedThread != null) { 477 updateThreadStackTrace(selectedThread); 478 } else { 479 updateThreadStackTrace(null); 480 } 481 } 482 } 483 484 /** 485 * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied 486 * of current data. 487 * @param thread 488 */ 489 private void updateThreadStackTrace(ThreadInfo thread) { 490 mStackTracePanel.setViewerInput(thread); 491 492 if (thread != null) { 493 mRefreshStackTraceButton.setEnabled(true); 494 long stackcallTime = thread.getStackCallTime(); 495 if (stackcallTime != 0) { 496 String label = new Date(stackcallTime).toString(); 497 mStackTraceTimeLabel.setText(label); 498 } else { 499 mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ 500 } 501 } else { 502 mRefreshStackTraceButton.setEnabled(true); 503 mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ 504 } 505 } 506 507 @Override 508 protected void setTableFocusListener() { 509 addTableToFocusListener(mThreadTable); 510 addTableToFocusListener(mStackTraceTable); 511 } 512 513 /** 514 * Initiate recurring events. We use a shorter "initialWait" so we do the 515 * first execution sooner. We don't do it immediately because we want to 516 * give the clients a chance to get set up. 517 */ 518 private void startRecurringThreadUpdate() { 519 mRecurringThreadUpdateRunning = true; 520 int initialWait = 1000; 521 522 mDisplay.timerExec(initialWait, new Runnable() { 523 public void run() { 524 synchronized (mLock) { 525 // lets check we still want updates. 526 if (mMustStopRecurringThreadUpdate == false) { 527 Client client = getCurrentClient(); 528 if (client != null) { 529 client.requestThreadUpdate(); 530 531 mDisplay.timerExec( 532 DdmUiPreferences.getThreadRefreshInterval() * 1000, this); 533 } else { 534 // we don't have a Client, which means the runnable is not 535 // going to be called through the timer. We reset the running flag. 536 mRecurringThreadUpdateRunning = false; 537 } 538 } else { 539 // else actually stops (don't call the timerExec) and reset the flags. 540 mRecurringThreadUpdateRunning = false; 541 mMustStopRecurringThreadUpdate = false; 542 } 543 } 544 } 545 }); 546 } 547 548 /** 549 * Returns the current thread selection or <code>null</code> if none is found. 550 * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection 551 * is returned, otherwise, the <code>ISelection</code> returned by 552 * {@link TableViewer#getSelection()} is used. 553 * @param selection the {@link ISelection} to use, or <code>null</code> 554 */ 555 private ThreadInfo getThreadSelection(ISelection selection) { 556 if (selection == null) { 557 selection = mThreadViewer.getSelection(); 558 } 559 560 if (selection instanceof IStructuredSelection) { 561 IStructuredSelection structuredSelection = (IStructuredSelection)selection; 562 Object object = structuredSelection.getFirstElement(); 563 if (object instanceof ThreadInfo) { 564 return (ThreadInfo)object; 565 } 566 } 567 568 return null; 569 } 570 571 } 572 573