/* * Copyright (C) 2011 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.heap; import com.android.ddmlib.Client; import com.android.ddmlib.Log; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeLibraryMapInfo; import com.android.ddmlib.NativeStackCallInfo; import com.android.ddmuilib.Addr2Line; import com.android.ddmuilib.BaseHeapPanel; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.TableHelper; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; 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.Combo; 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.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** Panel to display native heap information. */ public class NativeHeapPanel extends BaseHeapPanel { private static final boolean USE_OLD_RESOLVER; static { String useOldResolver = System.getenv("ANDROID_DDMS_OLD_SYMRESOLVER"); if (useOldResolver != null && useOldResolver.equalsIgnoreCase("true")) { USE_OLD_RESOLVER = true; } else { USE_OLD_RESOLVER = false; } } private final int MAX_DISPLAYED_ERROR_ITEMS = 5; private static final String TOOLTIP_EXPORT_DATA = "Export Heap Data"; private static final String TOOLTIP_ZYGOTE_ALLOCATIONS = "Show Zygote Allocations"; private static final String TOOLTIP_DIFFS_ONLY = "Only show new allocations not present in previous snapshot"; private static final String TOOLTIP_GROUPBY = "Group allocations by library."; private static final String EXPORT_DATA_IMAGE = "save.png"; private static final String ZYGOTE_IMAGE = "zygote.png"; private static final String DIFFS_ONLY_IMAGE = "diff.png"; private static final String GROUPBY_IMAGE = "groupby.png"; private static final String SNAPSHOT_HEAP_BUTTON_TEXT = "Snapshot Current Native Heap Usage"; private static final String LOAD_HEAP_DATA_BUTTON_TEXT = "Import Heap Data"; private static final String SYMBOL_SEARCH_PATH_LABEL_TEXT = "Symbol Search Path:"; private static final String SYMBOL_SEARCH_PATH_TEXT_MESSAGE = "List of colon separated paths to search for symbol debug information. See tooltip for examples."; private static final String SYMBOL_SEARCH_PATH_TOOLTIP_TEXT = "Colon separated paths that contain unstripped libraries with debug symbols.\n" + "e.g.: <android-src>/out/target/product/generic/symbols/system/lib:/path/to/my/app/obj/local/armeabi"; private static final String PREFS_SHOW_DIFFS_ONLY = "nativeheap.show.diffs.only"; private static final String PREFS_SHOW_ZYGOTE_ALLOCATIONS = "nativeheap.show.zygote"; private static final String PREFS_GROUP_BY_LIBRARY = "nativeheap.grouby.library"; private static final String PREFS_SYMBOL_SEARCH_PATH = "nativeheap.search.path"; private static final String PREFS_SASH_HEIGHT_PERCENT = "nativeheap.sash.percent"; private static final String PREFS_LAST_IMPORTED_HEAPPATH = "nativeheap.last.import.path"; private IPreferenceStore mPrefStore; private List<NativeHeapSnapshot> mNativeHeapSnapshots; // Maintain the differences between a snapshot and its predecessor. // mDiffSnapshots[i] = mNativeHeapSnapshots[i] - mNativeHeapSnapshots[i-1] // The zeroth entry is null since there is no predecessor. // The list is filled lazily on demand. private List<NativeHeapSnapshot> mDiffSnapshots; private Map<Integer, List<NativeHeapSnapshot>> mImportedSnapshotsPerPid; private Button mSnapshotHeapButton; private Button mLoadHeapDataButton; private Text mSymbolSearchPathText; private Combo mSnapshotIndexCombo; private Label mMemoryAllocatedText; private TreeViewer mDetailsTreeViewer; private TreeViewer mStackTraceTreeViewer; private NativeHeapProviderByAllocations mContentProviderByAllocations; private NativeHeapProviderByLibrary mContentProviderByLibrary; private NativeHeapLabelProvider mDetailsTreeLabelProvider; private ToolBar mDetailsToolBar; private ToolItem mGroupByButton; private ToolItem mDiffsOnlyButton; private ToolItem mShowZygoteAllocationsButton; private ToolItem mExportHeapDataButton; public NativeHeapPanel(IPreferenceStore prefStore) { mPrefStore = prefStore; mPrefStore.setDefault(PREFS_SASH_HEIGHT_PERCENT, 75); mPrefStore.setDefault(PREFS_SYMBOL_SEARCH_PATH, ""); mPrefStore.setDefault(PREFS_GROUP_BY_LIBRARY, false); mPrefStore.setDefault(PREFS_SHOW_ZYGOTE_ALLOCATIONS, true); mPrefStore.setDefault(PREFS_SHOW_DIFFS_ONLY, false); mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>(); mDiffSnapshots = new ArrayList<NativeHeapSnapshot>(); mImportedSnapshotsPerPid = new HashMap<Integer, List<NativeHeapSnapshot>>(); } /** {@inheritDoc} */ @Override public void clientChanged(final Client client, int changeMask) { if (client != getCurrentClient()) { return; } if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) != Client.CHANGE_NATIVE_HEAP_DATA) { return; } List<NativeAllocationInfo> allocations = client.getClientData().getNativeAllocationList(); if (allocations.size() == 0) { return; } // We need to clone this list since getClientData().getNativeAllocationList() clobbers // the list on future updates final List<NativeAllocationInfo> nativeAllocations = shallowCloneList(allocations); addNativeHeapSnapshot(new NativeHeapSnapshot(nativeAllocations)); updateDisplay(); // Attempt to resolve symbols in a separate thread. // The UI should be refreshed once the symbols have been resolved. if (USE_OLD_RESOLVER) { Thread t = new Thread(new SymbolResolverTask(nativeAllocations, client.getClientData().getMappedNativeLibraries())); t.setName("Address to Symbol Resolver"); t.start(); } else { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { resolveSymbols(); mDetailsTreeViewer.refresh(); mStackTraceTreeViewer.refresh(); } public void resolveSymbols() { Shell shell = Display.getDefault().getActiveShell(); ProgressMonitorDialog d = new ProgressMonitorDialog(shell); NativeSymbolResolverTask resolver = new NativeSymbolResolverTask( nativeAllocations, client.getClientData().getMappedNativeLibraries(), mSymbolSearchPathText.getText()); try { d.run(true, true, resolver); } catch (InvocationTargetException e) { MessageDialog.openError(shell, "Error Resolving Symbols", e.getCause().getMessage()); return; } catch (InterruptedException e) { return; } MessageDialog.openInformation(shell, "Symbol Resolution Status", getResolutionStatusMessage(resolver)); } }); } } private String getResolutionStatusMessage(NativeSymbolResolverTask resolver) { StringBuilder sb = new StringBuilder(); sb.append("Symbol Resolution Complete.\n\n"); // show addresses that were not mapped Set<Long> unmappedAddresses = resolver.getUnmappedAddresses(); if (unmappedAddresses.size() > 0) { sb.append(String.format("Unmapped addresses (%d): ", unmappedAddresses.size())); sb.append(getSampleForDisplay(unmappedAddresses)); sb.append('\n'); } // show libraries that were not present on disk Set<String> notFoundLibraries = resolver.getNotFoundLibraries(); if (notFoundLibraries.size() > 0) { sb.append(String.format("Libraries not found on disk (%d): ", notFoundLibraries.size())); sb.append(getSampleForDisplay(notFoundLibraries)); sb.append('\n'); } // show addresses that were mapped but not resolved Set<Long> unresolvableAddresses = resolver.getUnresolvableAddresses(); if (unresolvableAddresses.size() > 0) { sb.append(String.format("Unresolved addresses (%d): ", unresolvableAddresses.size())); sb.append(getSampleForDisplay(unresolvableAddresses)); sb.append('\n'); } if (resolver.getAddr2LineErrorMessage() != null) { sb.append("Error launching addr2line: "); sb.append(resolver.getAddr2LineErrorMessage()); } return sb.toString(); } /** * Get the string representation for a collection of items. * If there are more items than {@link #MAX_DISPLAYED_ERROR_ITEMS}, then only the first * {@link #MAX_DISPLAYED_ERROR_ITEMS} items are taken into account, * and an ellipsis is added at the end. */ private String getSampleForDisplay(Collection<?> items) { StringBuilder sb = new StringBuilder(); int c = 1; Iterator<?> it = items.iterator(); while (it.hasNext()) { Object item = it.next(); if (item instanceof Long) { sb.append(String.format("0x%x", item)); } else { sb.append(item); } if (c == MAX_DISPLAYED_ERROR_ITEMS && it.hasNext()) { sb.append(", ..."); break; } else if (it.hasNext()) { sb.append(", "); } c++; } return sb.toString(); } private void addNativeHeapSnapshot(NativeHeapSnapshot snapshot) { mNativeHeapSnapshots.add(snapshot); // The diff snapshots are filled in lazily on demand. // But the list needs to be the same size as mNativeHeapSnapshots, so we add a null. mDiffSnapshots.add(null); } private List<NativeAllocationInfo> shallowCloneList(List<NativeAllocationInfo> allocations) { List<NativeAllocationInfo> clonedList = new ArrayList<NativeAllocationInfo>(allocations.size()); for (NativeAllocationInfo i : allocations) { clonedList.add(i); } return clonedList; } @Override public void deviceSelected() { // pass } @Override public void clientSelected() { Client c = getCurrentClient(); if (c == null) { // if there is no client selected, then we disable the buttons but leave the // display as is so that whatever snapshots are displayed continue to stay // visible to the user. mSnapshotHeapButton.setEnabled(false); mLoadHeapDataButton.setEnabled(false); return; } mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>(); mDiffSnapshots = new ArrayList<NativeHeapSnapshot>(); mSnapshotHeapButton.setEnabled(true); mLoadHeapDataButton.setEnabled(true); List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get( c.getClientData().getPid()); if (importedSnapshots != null) { for (NativeHeapSnapshot n : importedSnapshots) { addNativeHeapSnapshot(n); } } List<NativeAllocationInfo> allocations = c.getClientData().getNativeAllocationList(); allocations = shallowCloneList(allocations); if (allocations.size() > 0) { addNativeHeapSnapshot(new NativeHeapSnapshot(allocations)); } updateDisplay(); } private void updateDisplay() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { updateSnapshotIndexCombo(); updateToolbars(); int lastSnapshotIndex = mNativeHeapSnapshots.size() - 1; displaySnapshot(lastSnapshotIndex); displayStackTraceForSelection(); } }); } private void displaySelectedSnapshot() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { int idx = mSnapshotIndexCombo.getSelectionIndex(); displaySnapshot(idx); } }); } private void displaySnapshot(int index) { if (index < 0 || mNativeHeapSnapshots.size() == 0) { mDetailsTreeViewer.setInput(null); mMemoryAllocatedText.setText(""); return; } assert index < mNativeHeapSnapshots.size() : "Invalid snapshot index"; NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(index); if (mDiffsOnlyButton.getSelection() && index > 0) { snapshot = getDiffSnapshot(index); } mMemoryAllocatedText.setText(snapshot.getFormattedMemorySize()); mMemoryAllocatedText.pack(); mDetailsTreeLabelProvider.setTotalSize(snapshot.getTotalSize()); mDetailsTreeViewer.setInput(snapshot); mDetailsTreeViewer.refresh(); } /** Obtain the diff of snapshot[index] & snapshot[index-1] */ private NativeHeapSnapshot getDiffSnapshot(int index) { // if it was already computed, simply return that NativeHeapSnapshot diffSnapshot = mDiffSnapshots.get(index); if (diffSnapshot != null) { return diffSnapshot; } // compute the diff NativeHeapSnapshot cur = mNativeHeapSnapshots.get(index); NativeHeapSnapshot prev = mNativeHeapSnapshots.get(index - 1); diffSnapshot = new NativeHeapDiffSnapshot(cur, prev); // cache for future use mDiffSnapshots.set(index, diffSnapshot); return diffSnapshot; } private void updateDisplayGrouping() { boolean groupByLibrary = mGroupByButton.getSelection(); mPrefStore.setValue(PREFS_GROUP_BY_LIBRARY, groupByLibrary); if (groupByLibrary) { mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); } else { mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); } } private void updateDisplayForZygotes() { boolean displayZygoteMemory = mShowZygoteAllocationsButton.getSelection(); mPrefStore.setValue(PREFS_SHOW_ZYGOTE_ALLOCATIONS, displayZygoteMemory); // inform the content providers of the zygote display setting mContentProviderByLibrary.displayZygoteMemory(displayZygoteMemory); mContentProviderByAllocations.displayZygoteMemory(displayZygoteMemory); // refresh the UI mDetailsTreeViewer.refresh(); } private void updateSnapshotIndexCombo() { List<String> items = new ArrayList<String>(); int numSnapshots = mNativeHeapSnapshots.size(); for (int i = 0; i < numSnapshots; i++) { // offset indices by 1 so that users see index starting at 1 rather than 0 items.add("Snapshot " + (i + 1)); } mSnapshotIndexCombo.setItems(items.toArray(new String[0])); if (numSnapshots > 0) { mSnapshotIndexCombo.setEnabled(true); mSnapshotIndexCombo.select(numSnapshots - 1); } else { mSnapshotIndexCombo.setEnabled(false); } } private void updateToolbars() { int numSnapshots = mNativeHeapSnapshots.size(); mExportHeapDataButton.setEnabled(numSnapshots > 0); } @Override protected Control createControl(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(1, false)); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createControlsSection(c); createDetailsSection(c); // Initialize widget state based on whether a client // is selected or not. clientSelected(); return c; } private void createControlsSection(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(3, false)); c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); createGetHeapDataSection(c); Label l = new Label(c, SWT.SEPARATOR | SWT.VERTICAL); l.setLayoutData(new GridData(GridData.FILL_VERTICAL)); createDisplaySection(c); } private void createGetHeapDataSection(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(1, false)); createTakeHeapSnapshotButton(c); Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); createLoadHeapDataButton(c); } private void createTakeHeapSnapshotButton(Composite parent) { mSnapshotHeapButton = new Button(parent, SWT.BORDER | SWT.PUSH); mSnapshotHeapButton.setText(SNAPSHOT_HEAP_BUTTON_TEXT); mSnapshotHeapButton.setLayoutData(new GridData()); // disable by default, enabled only when a client is selected mSnapshotHeapButton.setEnabled(false); mSnapshotHeapButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent evt) { snapshotHeap(); } }); } private void snapshotHeap() { Client c = getCurrentClient(); assert c != null : "Snapshot Heap could not have been enabled w/o a selected client."; // send an async request c.requestNativeHeapInformation(); } private void createLoadHeapDataButton(Composite parent) { mLoadHeapDataButton = new Button(parent, SWT.BORDER | SWT.PUSH); mLoadHeapDataButton.setText(LOAD_HEAP_DATA_BUTTON_TEXT); mLoadHeapDataButton.setLayoutData(new GridData()); // disable by default, enabled only when a client is selected mLoadHeapDataButton.setEnabled(false); mLoadHeapDataButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent evt) { loadHeapDataFromFile(); } }); } private void loadHeapDataFromFile() { // pop up a file dialog and get the file to load final String path = getHeapDumpToImport(); if (path == null) { return; } Reader reader = null; try { reader = new FileReader(path); } catch (FileNotFoundException e) { // cannot occur since user input was via a FileDialog } Shell shell = Display.getDefault().getActiveShell(); ProgressMonitorDialog d = new ProgressMonitorDialog(shell); NativeHeapDataImporter importer = new NativeHeapDataImporter(reader); try { d.run(true, true, importer); } catch (InvocationTargetException e) { // exception while parsing, display error to user and then return MessageDialog.openError(shell, "Error Importing Heap Data", e.getCause().getMessage()); return; } catch (InterruptedException e) { // operation cancelled by user, simply return return; } NativeHeapSnapshot snapshot = importer.getImportedSnapshot(); addToImportedSnapshots(snapshot); // save imported snapshot for future use addNativeHeapSnapshot(snapshot); // add to currently displayed snapshots as well updateDisplay(); } private void addToImportedSnapshots(NativeHeapSnapshot snapshot) { Client c = getCurrentClient(); if (c == null) { return; } Integer pid = c.getClientData().getPid(); List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get(pid); if (importedSnapshots == null) { importedSnapshots = new ArrayList<NativeHeapSnapshot>(); } importedSnapshots.add(snapshot); mImportedSnapshotsPerPid.put(pid, importedSnapshots); } private String getHeapDumpToImport() { FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), SWT.OPEN); fileDialog.setText("Import Heap Dump"); fileDialog.setFilterExtensions(new String[] {"*.txt"}); fileDialog.setFilterPath(mPrefStore.getString(PREFS_LAST_IMPORTED_HEAPPATH)); String selectedFile = fileDialog.open(); if (selectedFile != null) { // save the path to restore in future dialog open mPrefStore.setValue(PREFS_LAST_IMPORTED_HEAPPATH, new File(selectedFile).getParent()); } return selectedFile; } private void createDisplaySection(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(2, false)); c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // Create: Display: __________________ createLabel(c, "Display:"); mSnapshotIndexCombo = new Combo(c, SWT.NONE | SWT.READ_ONLY); mSnapshotIndexCombo.setItems(new String[] {"No heap snapshots available."}); mSnapshotIndexCombo.setEnabled(false); mSnapshotIndexCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { displaySelectedSnapshot(); } }); // Create: Memory Allocated (bytes): _________________ createLabel(c, "Memory Allocated:"); mMemoryAllocatedText = new Label(c, SWT.NONE); GridData gd = new GridData(); gd.widthHint = 100; mMemoryAllocatedText.setLayoutData(gd); // Create: Search Path: __________________ createLabel(c, SYMBOL_SEARCH_PATH_LABEL_TEXT); mSymbolSearchPathText = new Text(c, SWT.BORDER); mSymbolSearchPathText.setMessage(SYMBOL_SEARCH_PATH_TEXT_MESSAGE); mSymbolSearchPathText.setToolTipText(SYMBOL_SEARCH_PATH_TOOLTIP_TEXT); mSymbolSearchPathText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { String path = mSymbolSearchPathText.getText(); updateSearchPath(path); mPrefStore.setValue(PREFS_SYMBOL_SEARCH_PATH, path); } }); mSymbolSearchPathText.setText(mPrefStore.getString(PREFS_SYMBOL_SEARCH_PATH)); mSymbolSearchPathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); } private void updateSearchPath(String path) { Addr2Line.setSearchPath(path); } private void createLabel(Composite parent, String text) { Label l = new Label(parent, SWT.NONE); l.setText(text); GridData gd = new GridData(); gd.horizontalAlignment = SWT.RIGHT; l.setLayoutData(gd); } /** * Create the details section displaying the details table and the stack trace * corresponding to the selection. * * The details is laid out like so: * Details Toolbar * Details Table * ------------sash--- * Stack Trace Label * Stack Trace Text * There is a sash in between the two sections, and we need to save/restore the sash * preferences. Using FormLayout seems like the easiest solution here, but the layout * code looks ugly as a result. */ private void createDetailsSection(Composite parent) { final Composite c = new Composite(parent, SWT.NONE); c.setLayout(new FormLayout()); c.setLayoutData(new GridData(GridData.FILL_BOTH)); mDetailsToolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER); initializeDetailsToolBar(mDetailsToolBar); Tree detailsTree = new Tree(c, SWT.VIRTUAL | SWT.BORDER | SWT.MULTI); initializeDetailsTree(detailsTree); final Sash sash = new Sash(c, SWT.HORIZONTAL | SWT.BORDER); Label stackTraceLabel = new Label(c, SWT.NONE); stackTraceLabel.setText("Stack Trace:"); Tree stackTraceTree = new Tree(c, SWT.BORDER | SWT.MULTI); initializeStackTraceTree(stackTraceTree); // layout the widgets created above FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); mDetailsToolBar.setLayoutData(data); data = new FormData(); data.top = new FormAttachment(mDetailsToolBar, 0); data.bottom = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); detailsTree.setLayoutData(data); final FormData sashData = new FormData(); sashData.top = new FormAttachment(mPrefStore.getInt(PREFS_SASH_HEIGHT_PERCENT), 0); 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.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); stackTraceLabel.setLayoutData(data); data = new FormData(); data.top = new FormAttachment(stackTraceLabel, 0); data.left = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); data.right = new FormAttachment(100, 0); stackTraceTree.setLayoutData(data); sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = c.getClientArea(); int sashPercent = sashRect.y * 100 / panelRect.height; mPrefStore.setValue(PREFS_SASH_HEIGHT_PERCENT, sashPercent); sashData.top = new FormAttachment(0, e.y); c.layout(); } }); } private void initializeDetailsToolBar(ToolBar toolbar) { mGroupByButton = new ToolItem(toolbar, SWT.CHECK); mGroupByButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(GROUPBY_IMAGE, toolbar.getDisplay())); mGroupByButton.setToolTipText(TOOLTIP_GROUPBY); mGroupByButton.setSelection(mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)); mGroupByButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { updateDisplayGrouping(); } }); mDiffsOnlyButton = new ToolItem(toolbar, SWT.CHECK); mDiffsOnlyButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(DIFFS_ONLY_IMAGE, toolbar.getDisplay())); mDiffsOnlyButton.setToolTipText(TOOLTIP_DIFFS_ONLY); mDiffsOnlyButton.setSelection(mPrefStore.getBoolean(PREFS_SHOW_DIFFS_ONLY)); mDiffsOnlyButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { // simply refresh the display, as the display logic takes care of // the current state of the diffs only checkbox. int idx = mSnapshotIndexCombo.getSelectionIndex(); displaySnapshot(idx); } }); mShowZygoteAllocationsButton = new ToolItem(toolbar, SWT.CHECK); mShowZygoteAllocationsButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( ZYGOTE_IMAGE, toolbar.getDisplay())); mShowZygoteAllocationsButton.setToolTipText(TOOLTIP_ZYGOTE_ALLOCATIONS); mShowZygoteAllocationsButton.setSelection( mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS)); mShowZygoteAllocationsButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { updateDisplayForZygotes(); } }); mExportHeapDataButton = new ToolItem(toolbar, SWT.PUSH); mExportHeapDataButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( EXPORT_DATA_IMAGE, toolbar.getDisplay())); mExportHeapDataButton.setToolTipText(TOOLTIP_EXPORT_DATA); mExportHeapDataButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { exportSnapshot(); } }); } /** Export currently displayed snapshot to a file */ private void exportSnapshot() { int idx = mSnapshotIndexCombo.getSelectionIndex(); String snapshotName = mSnapshotIndexCombo.getItem(idx); FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), SWT.SAVE); fileDialog.setText("Save " + snapshotName); fileDialog.setFileName("allocations.txt"); final String fileName = fileDialog.open(); if (fileName == null) { return; } final NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(idx); Thread t = new Thread(new Runnable() { @Override public void run() { PrintWriter out; try { out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); } catch (IOException e) { displayErrorMessage(e.getMessage()); return; } for (NativeAllocationInfo alloc : snapshot.getAllocations()) { out.println(alloc.toString()); } out.close(); } private void displayErrorMessage(final String message) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { MessageDialog.openError(Display.getDefault().getActiveShell(), "Failed to export heap data", message); } }); } }); t.setName("Saving Heap Data to File..."); t.start(); } private void initializeDetailsTree(Tree tree) { tree.setHeaderVisible(true); tree.setLinesVisible(true); List<String> properties = Arrays.asList(new String[] { "Library", "Total", "Percentage", "Count", "Size", "Method", }); List<String> sampleValues = Arrays.asList(new String[] { "/path/in/device/to/system/library.so", "123456789", " 100%", "123456789", "123456789", "PossiblyLongDemangledMethodName", }); // right align numeric values List<Integer> swtFlags = Arrays.asList(new Integer[] { SWT.LEFT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.LEFT, }); for (int i = 0; i < properties.size(); i++) { String p = properties.get(i); String v = sampleValues.get(i); int flags = swtFlags.get(i); TableHelper.createTreeColumn(tree, p, flags, v, getPref("details", p), mPrefStore); } mDetailsTreeViewer = new TreeViewer(tree); mDetailsTreeViewer.setUseHashlookup(true); boolean displayZygotes = mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS); mContentProviderByAllocations = new NativeHeapProviderByAllocations(mDetailsTreeViewer, displayZygotes); mContentProviderByLibrary = new NativeHeapProviderByLibrary(mDetailsTreeViewer, displayZygotes); if (mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)) { mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); } else { mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); } mDetailsTreeLabelProvider = new NativeHeapLabelProvider(); mDetailsTreeViewer.setLabelProvider(mDetailsTreeLabelProvider); mDetailsTreeViewer.setInput(null); tree.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { displayStackTraceForSelection(); } }); } private void initializeStackTraceTree(Tree tree) { tree.setHeaderVisible(true); tree.setLinesVisible(true); List<String> properties = Arrays.asList(new String[] { "Address", "Library", "Method", "File", "Line", }); List<String> sampleValues = Arrays.asList(new String[] { "0x1234_5678", "/path/in/device/to/system/library.so", "PossiblyLongDemangledMethodName", "/android/out/prefix/in/home/directory/to/path/in/device/to/system/library.so", "2000", }); for (int i = 0; i < properties.size(); i++) { String p = properties.get(i); String v = sampleValues.get(i); TableHelper.createTreeColumn(tree, p, SWT.LEFT, v, getPref("stack", p), mPrefStore); } mStackTraceTreeViewer = new TreeViewer(tree); mStackTraceTreeViewer.setContentProvider(new NativeStackContentProvider()); mStackTraceTreeViewer.setLabelProvider(new NativeStackLabelProvider()); mStackTraceTreeViewer.setInput(null); } private void displayStackTraceForSelection() { TreeItem []items = mDetailsTreeViewer.getTree().getSelection(); if (items.length == 0) { mStackTraceTreeViewer.setInput(null); return; } Object data = items[0].getData(); if (!(data instanceof NativeAllocationInfo)) { mStackTraceTreeViewer.setInput(null); return; } NativeAllocationInfo info = (NativeAllocationInfo) data; if (info.isStackCallResolved()) { mStackTraceTreeViewer.setInput(info.getResolvedStackCall()); } else { mStackTraceTreeViewer.setInput(info.getStackCallAddresses()); } } private String getPref(String prefix, String s) { return "nativeheap.tree." + prefix + "." + s; } @Override public void setFocus() { } private ITableFocusListener mTableFocusListener; @Override public void setTableFocusListener(ITableFocusListener listener) { mTableFocusListener = listener; final Tree heapSitesTree = mDetailsTreeViewer.getTree(); final IFocusedTableActivator heapSitesActivator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { TreeItem[] items = heapSitesTree.getSelection(); copyToClipboard(items, clipboard); } @Override public void selectAll() { heapSitesTree.selectAll(); } }; heapSitesTree.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent arg0) { mTableFocusListener.focusLost(heapSitesActivator); } @Override public void focusGained(FocusEvent arg0) { mTableFocusListener.focusGained(heapSitesActivator); } }); final Tree stackTraceTree = mStackTraceTreeViewer.getTree(); final IFocusedTableActivator stackTraceActivator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { TreeItem[] items = stackTraceTree.getSelection(); copyToClipboard(items, clipboard); } @Override public void selectAll() { stackTraceTree.selectAll(); } }; stackTraceTree.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent arg0) { mTableFocusListener.focusLost(stackTraceActivator); } @Override public void focusGained(FocusEvent arg0) { mTableFocusListener.focusGained(stackTraceActivator); } }); } private void copyToClipboard(TreeItem[] items, Clipboard clipboard) { StringBuilder sb = new StringBuilder(); for (TreeItem item : items) { Object data = item.getData(); if (data != null) { sb.append(data.toString()); sb.append('\n'); } } String content = sb.toString(); if (content.length() > 0) { clipboard.setContents( new Object[] {sb.toString()}, new Transfer[] {TextTransfer.getInstance()} ); } } private class SymbolResolverTask implements Runnable { private List<NativeAllocationInfo> mCallSites; private List<NativeLibraryMapInfo> mMappedLibraries; private Map<Long, NativeStackCallInfo> mResolvedSymbolCache; public SymbolResolverTask(List<NativeAllocationInfo> callSites, List<NativeLibraryMapInfo> mappedLibraries) { mCallSites = callSites; mMappedLibraries = mappedLibraries; mResolvedSymbolCache = new HashMap<Long, NativeStackCallInfo>(); } @Override public void run() { for (NativeAllocationInfo callSite : mCallSites) { if (callSite.isStackCallResolved()) { continue; } List<Long> addresses = callSite.getStackCallAddresses(); List<NativeStackCallInfo> resolvedStackInfo = new ArrayList<NativeStackCallInfo>(addresses.size()); for (Long address : addresses) { NativeStackCallInfo info = mResolvedSymbolCache.get(address); if (info != null) { resolvedStackInfo.add(info); } else { info = resolveAddress(address); resolvedStackInfo.add(info); mResolvedSymbolCache.put(address, info); } } callSite.setResolvedStackCall(resolvedStackInfo); } Display.getDefault().asyncExec(new Runnable() { @Override public void run() { mDetailsTreeViewer.refresh(); mStackTraceTreeViewer.refresh(); } }); } private NativeStackCallInfo resolveAddress(long addr) { NativeLibraryMapInfo library = getLibraryFor(addr); if (library != null) { Addr2Line process = Addr2Line.getProcess(library); if (process != null) { NativeStackCallInfo info = process.getAddress(addr); if (info != null) { return info; } } } return new NativeStackCallInfo(addr, library != null ? library.getLibraryName() : null, Long.toHexString(addr), ""); } private NativeLibraryMapInfo getLibraryFor(long addr) { for (NativeLibraryMapInfo info : mMappedLibraries) { if (info.isWithinLibrary(addr)) { return info; } } Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); return null; } } }