| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ddmuilib; |
| |
| import com.android.ddmlib.Client; |
| import com.android.ddmlib.ThreadInfo; |
| import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; |
| |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.jface.viewers.DoubleClickEvent; |
| import org.eclipse.jface.viewers.IDoubleClickListener; |
| import org.eclipse.jface.viewers.ILabelProviderListener; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.IStructuredContentProvider; |
| import org.eclipse.jface.viewers.IStructuredSelection; |
| import org.eclipse.jface.viewers.ITableLabelProvider; |
| import org.eclipse.jface.viewers.SelectionChangedEvent; |
| import org.eclipse.jface.viewers.TableViewer; |
| import org.eclipse.jface.viewers.Viewer; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.SWTException; |
| import org.eclipse.swt.custom.StackLayout; |
| import org.eclipse.swt.events.SelectionAdapter; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.layout.FormAttachment; |
| import org.eclipse.swt.layout.FormData; |
| import org.eclipse.swt.layout.FormLayout; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Button; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.Sash; |
| import org.eclipse.swt.widgets.Table; |
| |
| import java.util.Date; |
| |
| /** |
| * Base class for our information panels. |
| */ |
| public class ThreadPanel extends TablePanel { |
| |
| private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$ |
| private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$ |
| private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$ |
| private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$ |
| private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$ |
| private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$ |
| |
| private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$ |
| |
| private static final String PREFS_STACK_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$ |
| private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$ |
| private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$ |
| private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$ |
| private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$ |
| |
| private Display mDisplay; |
| private Composite mBase; |
| private Label mNotEnabled; |
| private Label mNotSelected; |
| |
| private Composite mThreadBase; |
| private Table mThreadTable; |
| private TableViewer mThreadViewer; |
| |
| private Composite mStackTraceBase; |
| private Button mRefreshStackTraceButton; |
| private Label mStackTraceTimeLabel; |
| private StackTracePanel mStackTracePanel; |
| private Table mStackTraceTable; |
| |
| /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */ |
| private boolean mMustStopRecurringThreadUpdate = false; |
| /** Flag to tell the recurring thread update to stop running */ |
| private boolean mRecurringThreadUpdateRunning = false; |
| |
| private Object mLock = new Object(); |
| |
| private static final String[] THREAD_STATUS = { |
| "zombie", "running", "timed-wait", "monitor", |
| "wait", "init", "start", "native", "vmwait" |
| }; |
| |
| /** |
| * Content Provider to display the threads of a client. |
| * Expected input is a {@link Client} object. |
| */ |
| private static class ThreadContentProvider implements IStructuredContentProvider { |
| public Object[] getElements(Object inputElement) { |
| if (inputElement instanceof Client) { |
| return ((Client)inputElement).getClientData().getThreads(); |
| } |
| |
| return new Object[0]; |
| } |
| |
| public void dispose() { |
| // pass |
| } |
| |
| public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { |
| // pass |
| } |
| } |
| |
| |
| /** |
| * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be |
| * of type {@link ThreadInfo}. |
| */ |
| private static class ThreadLabelProvider implements ITableLabelProvider { |
| |
| public Image getColumnImage(Object element, int columnIndex) { |
| return null; |
| } |
| |
| public String getColumnText(Object element, int columnIndex) { |
| if (element instanceof ThreadInfo) { |
| ThreadInfo thread = (ThreadInfo)element; |
| switch (columnIndex) { |
| case 0: |
| return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$ |
| String.valueOf(thread.getThreadId()); |
| case 1: |
| return String.valueOf(thread.getTid()); |
| case 2: |
| if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length) |
| return THREAD_STATUS[thread.getStatus()]; |
| return "unknown"; |
| case 3: |
| return String.valueOf(thread.getUtime()); |
| case 4: |
| return String.valueOf(thread.getStime()); |
| case 5: |
| return thread.getThreadName(); |
| } |
| } |
| |
| return null; |
| } |
| |
| public void addListener(ILabelProviderListener listener) { |
| // pass |
| } |
| |
| public void dispose() { |
| // pass |
| } |
| |
| public boolean isLabelProperty(Object element, String property) { |
| // pass |
| return false; |
| } |
| |
| public void removeListener(ILabelProviderListener listener) { |
| // pass |
| } |
| } |
| |
| /** |
| * Create our control(s). |
| */ |
| @Override |
| protected Control createControl(Composite parent) { |
| mDisplay = parent.getDisplay(); |
| |
| final IPreferenceStore store = DdmUiPreferences.getStore(); |
| |
| mBase = new Composite(parent, SWT.NONE); |
| mBase.setLayout(new StackLayout()); |
| |
| // UI for thread not enabled |
| mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP); |
| mNotEnabled.setText("Thread updates not enabled for selected client\n" |
| + "(use toolbar button to enable)"); |
| |
| // UI for not client selected |
| mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP); |
| mNotSelected.setText("no client is selected"); |
| |
| // base composite for selected client with enabled thread update. |
| mThreadBase = new Composite(mBase, SWT.NONE); |
| mThreadBase.setLayout(new FormLayout()); |
| |
| // table above the sash |
| mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION); |
| mThreadTable.setHeaderVisible(true); |
| mThreadTable.setLinesVisible(true); |
| |
| TableHelper.createTableColumn( |
| mThreadTable, |
| "ID", |
| SWT.RIGHT, |
| "888", //$NON-NLS-1$ |
| PREFS_THREAD_COL_ID, store); |
| |
| TableHelper.createTableColumn( |
| mThreadTable, |
| "Tid", |
| SWT.RIGHT, |
| "88888", //$NON-NLS-1$ |
| PREFS_THREAD_COL_TID, store); |
| |
| TableHelper.createTableColumn( |
| mThreadTable, |
| "Status", |
| SWT.LEFT, |
| "timed-wait", //$NON-NLS-1$ |
| PREFS_THREAD_COL_STATUS, store); |
| |
| TableHelper.createTableColumn( |
| mThreadTable, |
| "utime", |
| SWT.RIGHT, |
| "utime", //$NON-NLS-1$ |
| PREFS_THREAD_COL_UTIME, store); |
| |
| TableHelper.createTableColumn( |
| mThreadTable, |
| "stime", |
| SWT.RIGHT, |
| "utime", //$NON-NLS-1$ |
| PREFS_THREAD_COL_STIME, store); |
| |
| TableHelper.createTableColumn( |
| mThreadTable, |
| "Name", |
| SWT.LEFT, |
| "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$ |
| PREFS_THREAD_COL_NAME, store); |
| |
| mThreadViewer = new TableViewer(mThreadTable); |
| mThreadViewer.setContentProvider(new ThreadContentProvider()); |
| mThreadViewer.setLabelProvider(new ThreadLabelProvider()); |
| |
| mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() { |
| public void selectionChanged(SelectionChangedEvent event) { |
| ThreadInfo selectedThread = getThreadSelection(event.getSelection()); |
| updateThreadStackTrace(selectedThread); |
| } |
| }); |
| mThreadViewer.addDoubleClickListener(new IDoubleClickListener() { |
| public void doubleClick(DoubleClickEvent event) { |
| ThreadInfo selectedThread = getThreadSelection(event.getSelection()); |
| if (selectedThread != null) { |
| Client client = (Client)mThreadViewer.getInput(); |
| |
| if (client != null) { |
| client.requestThreadStackTrace(selectedThread.getThreadId()); |
| } |
| } |
| } |
| }); |
| |
| // the separating sash |
| final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL); |
| Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); |
| sash.setBackground(darkGray); |
| |
| // the UI below the sash |
| mStackTraceBase = new Composite(mThreadBase, SWT.NONE); |
| mStackTraceBase.setLayout(new GridLayout(2, false)); |
| |
| mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH); |
| mRefreshStackTraceButton.setText("Refresh"); |
| mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| ThreadInfo selectedThread = getThreadSelection(null); |
| if (selectedThread != null) { |
| Client currentClient = getCurrentClient(); |
| if (currentClient != null) { |
| currentClient.requestThreadStackTrace(selectedThread.getThreadId()); |
| } |
| } |
| } |
| }); |
| |
| mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE); |
| mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); |
| |
| mStackTracePanel = new StackTracePanel(); |
| mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, |
| PREFS_STACK_COL_CLASS, |
| PREFS_STACK_COL_METHOD, |
| PREFS_STACK_COL_FILE, |
| PREFS_STACK_COL_LINE, |
| PREFS_STACK_COL_NATIVE, |
| store); |
| |
| GridData gd; |
| mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); |
| gd.horizontalSpan = 2; |
| |
| // now setup the sash. |
| // form layout data |
| FormData data = new FormData(); |
| data.top = new FormAttachment(0, 0); |
| data.bottom = new FormAttachment(sash, 0); |
| data.left = new FormAttachment(0, 0); |
| data.right = new FormAttachment(100, 0); |
| mThreadTable.setLayoutData(data); |
| |
| final FormData sashData = new FormData(); |
| if (store != null && store.contains(PREFS_THREAD_SASH)) { |
| sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH)); |
| } else { |
| sashData.top = new FormAttachment(50,0); // 50% across |
| } |
| sashData.left = new FormAttachment(0, 0); |
| sashData.right = new FormAttachment(100, 0); |
| sash.setLayoutData(sashData); |
| |
| data = new FormData(); |
| data.top = new FormAttachment(sash, 0); |
| data.bottom = new FormAttachment(100, 0); |
| data.left = new FormAttachment(0, 0); |
| data.right = new FormAttachment(100, 0); |
| mStackTraceBase.setLayoutData(data); |
| |
| // allow resizes, but cap at minPanelWidth |
| sash.addListener(SWT.Selection, new Listener() { |
| public void handleEvent(Event e) { |
| Rectangle sashRect = sash.getBounds(); |
| Rectangle panelRect = mThreadBase.getClientArea(); |
| int bottom = panelRect.height - sashRect.height - 100; |
| e.y = Math.max(Math.min(e.y, bottom), 100); |
| if (e.y != sashRect.y) { |
| sashData.top = new FormAttachment(0, e.y); |
| store.setValue(PREFS_THREAD_SASH, e.y); |
| mThreadBase.layout(); |
| } |
| } |
| }); |
| |
| ((StackLayout)mBase.getLayout()).topControl = mNotSelected; |
| |
| return mBase; |
| } |
| |
| /** |
| * Sets the focus to the proper control inside the panel. |
| */ |
| @Override |
| public void setFocus() { |
| mThreadTable.setFocus(); |
| } |
| |
| /** |
| * Sent when an existing client information changed. |
| * <p/> |
| * This is sent from a non UI thread. |
| * @param client the updated client. |
| * @param changeMask the bit mask describing the changed properties. It can contain |
| * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} |
| * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, |
| * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, |
| * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} |
| * |
| * @see IClientChangeListener#clientChanged(Client, int) |
| */ |
| public void clientChanged(final Client client, int changeMask) { |
| if (client == getCurrentClient()) { |
| if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 || |
| (changeMask & Client.CHANGE_THREAD_DATA) != 0) { |
| try { |
| mThreadTable.getDisplay().asyncExec(new Runnable() { |
| public void run() { |
| clientSelected(); |
| } |
| }); |
| } catch (SWTException e) { |
| // widget is disposed, we do nothing |
| } |
| } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) { |
| try { |
| mThreadTable.getDisplay().asyncExec(new Runnable() { |
| public void run() { |
| updateThreadStackCall(); |
| } |
| }); |
| } catch (SWTException e) { |
| // widget is disposed, we do nothing |
| } |
| } |
| } |
| } |
| |
| /** |
| * Sent when a new device is selected. The new device can be accessed |
| * with {@link #getCurrentDevice()}. |
| */ |
| @Override |
| public void deviceSelected() { |
| // pass |
| } |
| |
| /** |
| * Sent when a new client is selected. The new client can be accessed |
| * with {@link #getCurrentClient()}. |
| */ |
| @Override |
| public void clientSelected() { |
| if (mThreadTable.isDisposed()) { |
| return; |
| } |
| |
| Client client = getCurrentClient(); |
| |
| mStackTracePanel.setCurrentClient(client); |
| |
| if (client != null) { |
| if (!client.isThreadUpdateEnabled()) { |
| ((StackLayout)mBase.getLayout()).topControl = mNotEnabled; |
| mThreadViewer.setInput(null); |
| |
| // if we are currently updating the thread, stop doing it. |
| mMustStopRecurringThreadUpdate = true; |
| } else { |
| ((StackLayout)mBase.getLayout()).topControl = mThreadBase; |
| mThreadViewer.setInput(client); |
| |
| synchronized (mLock) { |
| // if we're not updating we start the process |
| if (mRecurringThreadUpdateRunning == false) { |
| startRecurringThreadUpdate(); |
| } else if (mMustStopRecurringThreadUpdate) { |
| // else if there's a runnable that's still going to get called, lets |
| // simply cancel the stop, and keep going |
| mMustStopRecurringThreadUpdate = false; |
| } |
| } |
| } |
| } else { |
| ((StackLayout)mBase.getLayout()).topControl = mNotSelected; |
| mThreadViewer.setInput(null); |
| } |
| |
| mBase.layout(); |
| } |
| |
| /** |
| * Updates the stack call of the currently selected thread. |
| * <p/> |
| * This <b>must</b> be called from the UI thread. |
| */ |
| private void updateThreadStackCall() { |
| Client client = getCurrentClient(); |
| if (client != null) { |
| // get the current selection in the ThreadTable |
| ThreadInfo selectedThread = getThreadSelection(null); |
| |
| if (selectedThread != null) { |
| updateThreadStackTrace(selectedThread); |
| } else { |
| updateThreadStackTrace(null); |
| } |
| } |
| } |
| |
| /** |
| * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied |
| * of current data. |
| * @param thread |
| */ |
| private void updateThreadStackTrace(ThreadInfo thread) { |
| mStackTracePanel.setViewerInput(thread); |
| |
| if (thread != null) { |
| mRefreshStackTraceButton.setEnabled(true); |
| long stackcallTime = thread.getStackCallTime(); |
| if (stackcallTime != 0) { |
| String label = new Date(stackcallTime).toString(); |
| mStackTraceTimeLabel.setText(label); |
| } else { |
| mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ |
| } |
| } else { |
| mRefreshStackTraceButton.setEnabled(true); |
| mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ |
| } |
| } |
| |
| @Override |
| protected void setTableFocusListener() { |
| addTableToFocusListener(mThreadTable); |
| addTableToFocusListener(mStackTraceTable); |
| } |
| |
| /** |
| * Initiate recurring events. We use a shorter "initialWait" so we do the |
| * first execution sooner. We don't do it immediately because we want to |
| * give the clients a chance to get set up. |
| */ |
| private void startRecurringThreadUpdate() { |
| mRecurringThreadUpdateRunning = true; |
| int initialWait = 1000; |
| |
| mDisplay.timerExec(initialWait, new Runnable() { |
| public void run() { |
| synchronized (mLock) { |
| // lets check we still want updates. |
| if (mMustStopRecurringThreadUpdate == false) { |
| Client client = getCurrentClient(); |
| if (client != null) { |
| client.requestThreadUpdate(); |
| |
| mDisplay.timerExec( |
| DdmUiPreferences.getThreadRefreshInterval() * 1000, this); |
| } else { |
| // we don't have a Client, which means the runnable is not |
| // going to be called through the timer. We reset the running flag. |
| mRecurringThreadUpdateRunning = false; |
| } |
| } else { |
| // else actually stops (don't call the timerExec) and reset the flags. |
| mRecurringThreadUpdateRunning = false; |
| mMustStopRecurringThreadUpdate = false; |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Returns the current thread selection or <code>null</code> if none is found. |
| * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection |
| * is returned, otherwise, the <code>ISelection</code> returned by |
| * {@link TableViewer#getSelection()} is used. |
| * @param selection the {@link ISelection} to use, or <code>null</code> |
| */ |
| private ThreadInfo getThreadSelection(ISelection selection) { |
| if (selection == null) { |
| selection = mThreadViewer.getSelection(); |
| } |
| |
| if (selection instanceof IStructuredSelection) { |
| IStructuredSelection structuredSelection = (IStructuredSelection)selection; |
| Object object = structuredSelection.getFirstElement(); |
| if (object instanceof ThreadInfo) { |
| return (ThreadInfo)object; |
| } |
| } |
| |
| return null; |
| } |
| |
| } |
| |