Home | History | Annotate | Download | only in ddmuilib
      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