1 /* 2 * Copyright (C) 2011 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.heap; 18 19 import com.android.ddmlib.Client; 20 import com.android.ddmlib.Log; 21 import com.android.ddmlib.NativeAllocationInfo; 22 import com.android.ddmlib.NativeLibraryMapInfo; 23 import com.android.ddmlib.NativeStackCallInfo; 24 import com.android.ddmuilib.Addr2Line; 25 import com.android.ddmuilib.BaseHeapPanel; 26 import com.android.ddmuilib.ITableFocusListener; 27 import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; 28 import com.android.ddmuilib.ImageLoader; 29 import com.android.ddmuilib.TableHelper; 30 31 import org.eclipse.jface.dialogs.MessageDialog; 32 import org.eclipse.jface.dialogs.ProgressMonitorDialog; 33 import org.eclipse.jface.preference.IPreferenceStore; 34 import org.eclipse.jface.viewers.TreeViewer; 35 import org.eclipse.swt.SWT; 36 import org.eclipse.swt.dnd.Clipboard; 37 import org.eclipse.swt.dnd.TextTransfer; 38 import org.eclipse.swt.dnd.Transfer; 39 import org.eclipse.swt.events.FocusEvent; 40 import org.eclipse.swt.events.FocusListener; 41 import org.eclipse.swt.events.ModifyEvent; 42 import org.eclipse.swt.events.ModifyListener; 43 import org.eclipse.swt.events.SelectionAdapter; 44 import org.eclipse.swt.events.SelectionEvent; 45 import org.eclipse.swt.graphics.Rectangle; 46 import org.eclipse.swt.layout.FormAttachment; 47 import org.eclipse.swt.layout.FormData; 48 import org.eclipse.swt.layout.FormLayout; 49 import org.eclipse.swt.layout.GridData; 50 import org.eclipse.swt.layout.GridLayout; 51 import org.eclipse.swt.widgets.Button; 52 import org.eclipse.swt.widgets.Combo; 53 import org.eclipse.swt.widgets.Composite; 54 import org.eclipse.swt.widgets.Control; 55 import org.eclipse.swt.widgets.Display; 56 import org.eclipse.swt.widgets.Event; 57 import org.eclipse.swt.widgets.FileDialog; 58 import org.eclipse.swt.widgets.Label; 59 import org.eclipse.swt.widgets.Listener; 60 import org.eclipse.swt.widgets.Sash; 61 import org.eclipse.swt.widgets.Shell; 62 import org.eclipse.swt.widgets.Text; 63 import org.eclipse.swt.widgets.ToolBar; 64 import org.eclipse.swt.widgets.ToolItem; 65 import org.eclipse.swt.widgets.Tree; 66 import org.eclipse.swt.widgets.TreeItem; 67 68 import java.io.BufferedWriter; 69 import java.io.File; 70 import java.io.FileNotFoundException; 71 import java.io.FileReader; 72 import java.io.FileWriter; 73 import java.io.IOException; 74 import java.io.PrintWriter; 75 import java.io.Reader; 76 import java.lang.reflect.InvocationTargetException; 77 import java.util.ArrayList; 78 import java.util.Arrays; 79 import java.util.Collection; 80 import java.util.HashMap; 81 import java.util.Iterator; 82 import java.util.List; 83 import java.util.Map; 84 import java.util.Set; 85 86 /** Panel to display native heap information. */ 87 public class NativeHeapPanel extends BaseHeapPanel { 88 private static final boolean USE_OLD_RESOLVER; 89 static { 90 String useOldResolver = System.getenv("ANDROID_DDMS_OLD_SYMRESOLVER"); 91 if (useOldResolver != null && useOldResolver.equalsIgnoreCase("true")) { 92 USE_OLD_RESOLVER = true; 93 } else { 94 USE_OLD_RESOLVER = false; 95 } 96 } 97 private final int MAX_DISPLAYED_ERROR_ITEMS = 5; 98 99 private static final String TOOLTIP_EXPORT_DATA = "Export Heap Data"; 100 private static final String TOOLTIP_ZYGOTE_ALLOCATIONS = "Show Zygote Allocations"; 101 private static final String TOOLTIP_DIFFS_ONLY = "Only show new allocations not present in previous snapshot"; 102 private static final String TOOLTIP_GROUPBY = "Group allocations by library."; 103 104 private static final String EXPORT_DATA_IMAGE = "save.png"; 105 private static final String ZYGOTE_IMAGE = "zygote.png"; 106 private static final String DIFFS_ONLY_IMAGE = "diff.png"; 107 private static final String GROUPBY_IMAGE = "groupby.png"; 108 109 private static final String SNAPSHOT_HEAP_BUTTON_TEXT = "Snapshot Current Native Heap Usage"; 110 private static final String LOAD_HEAP_DATA_BUTTON_TEXT = "Import Heap Data"; 111 private static final String SYMBOL_SEARCH_PATH_LABEL_TEXT = "Symbol Search Path:"; 112 private static final String SYMBOL_SEARCH_PATH_TEXT_MESSAGE = 113 "List of colon separated paths to search for symbol debug information. See tooltip for examples."; 114 private static final String SYMBOL_SEARCH_PATH_TOOLTIP_TEXT = 115 "Colon separated paths that contain unstripped libraries with debug symbols.\n" 116 + "e.g.: <android-src>/out/target/product/generic/symbols/system/lib:/path/to/my/app/obj/local/armeabi"; 117 118 private static final String PREFS_SHOW_DIFFS_ONLY = "nativeheap.show.diffs.only"; 119 private static final String PREFS_SHOW_ZYGOTE_ALLOCATIONS = "nativeheap.show.zygote"; 120 private static final String PREFS_GROUP_BY_LIBRARY = "nativeheap.grouby.library"; 121 private static final String PREFS_SYMBOL_SEARCH_PATH = "nativeheap.search.path"; 122 private static final String PREFS_SASH_HEIGHT_PERCENT = "nativeheap.sash.percent"; 123 private static final String PREFS_LAST_IMPORTED_HEAPPATH = "nativeheap.last.import.path"; 124 private IPreferenceStore mPrefStore; 125 126 private List<NativeHeapSnapshot> mNativeHeapSnapshots; 127 128 // Maintain the differences between a snapshot and its predecessor. 129 // mDiffSnapshots[i] = mNativeHeapSnapshots[i] - mNativeHeapSnapshots[i-1] 130 // The zeroth entry is null since there is no predecessor. 131 // The list is filled lazily on demand. 132 private List<NativeHeapSnapshot> mDiffSnapshots; 133 134 private Map<Integer, List<NativeHeapSnapshot>> mImportedSnapshotsPerPid; 135 136 private Button mSnapshotHeapButton; 137 private Button mLoadHeapDataButton; 138 private Text mSymbolSearchPathText; 139 private Combo mSnapshotIndexCombo; 140 private Label mMemoryAllocatedText; 141 142 private TreeViewer mDetailsTreeViewer; 143 private TreeViewer mStackTraceTreeViewer; 144 private NativeHeapProviderByAllocations mContentProviderByAllocations; 145 private NativeHeapProviderByLibrary mContentProviderByLibrary; 146 private NativeHeapLabelProvider mDetailsTreeLabelProvider; 147 148 private ToolBar mDetailsToolBar; 149 private ToolItem mGroupByButton; 150 private ToolItem mDiffsOnlyButton; 151 private ToolItem mShowZygoteAllocationsButton; 152 private ToolItem mExportHeapDataButton; 153 NativeHeapPanel(IPreferenceStore prefStore)154 public NativeHeapPanel(IPreferenceStore prefStore) { 155 mPrefStore = prefStore; 156 mPrefStore.setDefault(PREFS_SASH_HEIGHT_PERCENT, 75); 157 mPrefStore.setDefault(PREFS_SYMBOL_SEARCH_PATH, ""); 158 mPrefStore.setDefault(PREFS_GROUP_BY_LIBRARY, false); 159 mPrefStore.setDefault(PREFS_SHOW_ZYGOTE_ALLOCATIONS, true); 160 mPrefStore.setDefault(PREFS_SHOW_DIFFS_ONLY, false); 161 162 mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>(); 163 mDiffSnapshots = new ArrayList<NativeHeapSnapshot>(); 164 mImportedSnapshotsPerPid = new HashMap<Integer, List<NativeHeapSnapshot>>(); 165 } 166 167 /** {@inheritDoc} */ 168 @Override clientChanged(final Client client, int changeMask)169 public void clientChanged(final Client client, int changeMask) { 170 if (client != getCurrentClient()) { 171 return; 172 } 173 174 if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) != Client.CHANGE_NATIVE_HEAP_DATA) { 175 return; 176 } 177 178 List<NativeAllocationInfo> allocations = client.getClientData().getNativeAllocationList(); 179 if (allocations.size() == 0) { 180 return; 181 } 182 183 // We need to clone this list since getClientData().getNativeAllocationList() clobbers 184 // the list on future updates 185 final List<NativeAllocationInfo> nativeAllocations = shallowCloneList(allocations); 186 187 addNativeHeapSnapshot(new NativeHeapSnapshot(nativeAllocations)); 188 updateDisplay(); 189 190 // Attempt to resolve symbols in a separate thread. 191 // The UI should be refreshed once the symbols have been resolved. 192 if (USE_OLD_RESOLVER) { 193 Thread t = new Thread(new SymbolResolverTask(nativeAllocations, 194 client.getClientData().getMappedNativeLibraries())); 195 t.setName("Address to Symbol Resolver"); 196 t.start(); 197 } else { 198 Display.getDefault().asyncExec(new Runnable() { 199 @Override 200 public void run() { 201 resolveSymbols(); 202 mDetailsTreeViewer.refresh(); 203 mStackTraceTreeViewer.refresh(); 204 } 205 206 public void resolveSymbols() { 207 Shell shell = Display.getDefault().getActiveShell(); 208 ProgressMonitorDialog d = new ProgressMonitorDialog(shell); 209 210 NativeSymbolResolverTask resolver = new NativeSymbolResolverTask( 211 nativeAllocations, 212 client.getClientData().getMappedNativeLibraries(), 213 mSymbolSearchPathText.getText()); 214 215 try { 216 d.run(true, true, resolver); 217 } catch (InvocationTargetException e) { 218 MessageDialog.openError(shell, 219 "Error Resolving Symbols", 220 e.getCause().getMessage()); 221 return; 222 } catch (InterruptedException e) { 223 return; 224 } 225 226 MessageDialog.openInformation(shell, "Symbol Resolution Status", 227 getResolutionStatusMessage(resolver)); 228 } 229 }); 230 } 231 } 232 getResolutionStatusMessage(NativeSymbolResolverTask resolver)233 private String getResolutionStatusMessage(NativeSymbolResolverTask resolver) { 234 StringBuilder sb = new StringBuilder(); 235 sb.append("Symbol Resolution Complete.\n\n"); 236 237 // show addresses that were not mapped 238 Set<Long> unmappedAddresses = resolver.getUnmappedAddresses(); 239 if (unmappedAddresses.size() > 0) { 240 sb.append(String.format("Unmapped addresses (%d): ", 241 unmappedAddresses.size())); 242 sb.append(getSampleForDisplay(unmappedAddresses)); 243 sb.append('\n'); 244 } 245 246 // show libraries that were not present on disk 247 Set<String> notFoundLibraries = resolver.getNotFoundLibraries(); 248 if (notFoundLibraries.size() > 0) { 249 sb.append(String.format("Libraries not found on disk (%d): ", 250 notFoundLibraries.size())); 251 sb.append(getSampleForDisplay(notFoundLibraries)); 252 sb.append('\n'); 253 } 254 255 // show addresses that were mapped but not resolved 256 Set<Long> unresolvableAddresses = resolver.getUnresolvableAddresses(); 257 if (unresolvableAddresses.size() > 0) { 258 sb.append(String.format("Unresolved addresses (%d): ", 259 unresolvableAddresses.size())); 260 sb.append(getSampleForDisplay(unresolvableAddresses)); 261 sb.append('\n'); 262 } 263 264 if (resolver.getAddr2LineErrorMessage() != null) { 265 sb.append("Error launching addr2line: "); 266 sb.append(resolver.getAddr2LineErrorMessage()); 267 } 268 269 return sb.toString(); 270 } 271 272 /** 273 * Get the string representation for a collection of items. 274 * If there are more items than {@link #MAX_DISPLAYED_ERROR_ITEMS}, then only the first 275 * {@link #MAX_DISPLAYED_ERROR_ITEMS} items are taken into account, 276 * and an ellipsis is added at the end. 277 */ getSampleForDisplay(Collection<?> items)278 private String getSampleForDisplay(Collection<?> items) { 279 StringBuilder sb = new StringBuilder(); 280 281 int c = 1; 282 Iterator<?> it = items.iterator(); 283 while (it.hasNext()) { 284 Object item = it.next(); 285 if (item instanceof Long) { 286 sb.append(String.format("0x%x", item)); 287 } else { 288 sb.append(item); 289 } 290 291 if (c == MAX_DISPLAYED_ERROR_ITEMS && it.hasNext()) { 292 sb.append(", ..."); 293 break; 294 } else if (it.hasNext()) { 295 sb.append(", "); 296 } 297 298 c++; 299 } 300 return sb.toString(); 301 } 302 addNativeHeapSnapshot(NativeHeapSnapshot snapshot)303 private void addNativeHeapSnapshot(NativeHeapSnapshot snapshot) { 304 mNativeHeapSnapshots.add(snapshot); 305 306 // The diff snapshots are filled in lazily on demand. 307 // But the list needs to be the same size as mNativeHeapSnapshots, so we add a null. 308 mDiffSnapshots.add(null); 309 } 310 shallowCloneList(List<NativeAllocationInfo> allocations)311 private List<NativeAllocationInfo> shallowCloneList(List<NativeAllocationInfo> allocations) { 312 List<NativeAllocationInfo> clonedList = 313 new ArrayList<NativeAllocationInfo>(allocations.size()); 314 315 for (NativeAllocationInfo i : allocations) { 316 clonedList.add(i); 317 } 318 319 return clonedList; 320 } 321 322 @Override deviceSelected()323 public void deviceSelected() { 324 // pass 325 } 326 327 @Override clientSelected()328 public void clientSelected() { 329 Client c = getCurrentClient(); 330 331 if (c == null) { 332 // if there is no client selected, then we disable the buttons but leave the 333 // display as is so that whatever snapshots are displayed continue to stay 334 // visible to the user. 335 mSnapshotHeapButton.setEnabled(false); 336 mLoadHeapDataButton.setEnabled(false); 337 return; 338 } 339 340 mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>(); 341 mDiffSnapshots = new ArrayList<NativeHeapSnapshot>(); 342 343 mSnapshotHeapButton.setEnabled(true); 344 mLoadHeapDataButton.setEnabled(true); 345 346 List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get( 347 c.getClientData().getPid()); 348 if (importedSnapshots != null) { 349 for (NativeHeapSnapshot n : importedSnapshots) { 350 addNativeHeapSnapshot(n); 351 } 352 } 353 354 List<NativeAllocationInfo> allocations = c.getClientData().getNativeAllocationList(); 355 allocations = shallowCloneList(allocations); 356 357 if (allocations.size() > 0) { 358 addNativeHeapSnapshot(new NativeHeapSnapshot(allocations)); 359 } 360 361 updateDisplay(); 362 } 363 updateDisplay()364 private void updateDisplay() { 365 Display.getDefault().syncExec(new Runnable() { 366 @Override 367 public void run() { 368 updateSnapshotIndexCombo(); 369 updateToolbars(); 370 371 int lastSnapshotIndex = mNativeHeapSnapshots.size() - 1; 372 displaySnapshot(lastSnapshotIndex); 373 displayStackTraceForSelection(); 374 } 375 }); 376 } 377 displaySelectedSnapshot()378 private void displaySelectedSnapshot() { 379 Display.getDefault().syncExec(new Runnable() { 380 @Override 381 public void run() { 382 int idx = mSnapshotIndexCombo.getSelectionIndex(); 383 displaySnapshot(idx); 384 } 385 }); 386 } 387 displaySnapshot(int index)388 private void displaySnapshot(int index) { 389 if (index < 0 || mNativeHeapSnapshots.size() == 0) { 390 mDetailsTreeViewer.setInput(null); 391 mMemoryAllocatedText.setText(""); 392 return; 393 } 394 395 assert index < mNativeHeapSnapshots.size() : "Invalid snapshot index"; 396 397 NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(index); 398 if (mDiffsOnlyButton.getSelection() && index > 0) { 399 snapshot = getDiffSnapshot(index); 400 } 401 402 mMemoryAllocatedText.setText(snapshot.getFormattedMemorySize()); 403 mMemoryAllocatedText.pack(); 404 405 mDetailsTreeLabelProvider.setTotalSize(snapshot.getTotalSize()); 406 mDetailsTreeViewer.setInput(snapshot); 407 mDetailsTreeViewer.refresh(); 408 } 409 410 /** Obtain the diff of snapshot[index] & snapshot[index-1] */ getDiffSnapshot(int index)411 private NativeHeapSnapshot getDiffSnapshot(int index) { 412 // if it was already computed, simply return that 413 NativeHeapSnapshot diffSnapshot = mDiffSnapshots.get(index); 414 if (diffSnapshot != null) { 415 return diffSnapshot; 416 } 417 418 // compute the diff 419 NativeHeapSnapshot cur = mNativeHeapSnapshots.get(index); 420 NativeHeapSnapshot prev = mNativeHeapSnapshots.get(index - 1); 421 diffSnapshot = new NativeHeapDiffSnapshot(cur, prev); 422 423 // cache for future use 424 mDiffSnapshots.set(index, diffSnapshot); 425 426 return diffSnapshot; 427 } 428 updateDisplayGrouping()429 private void updateDisplayGrouping() { 430 boolean groupByLibrary = mGroupByButton.getSelection(); 431 mPrefStore.setValue(PREFS_GROUP_BY_LIBRARY, groupByLibrary); 432 433 if (groupByLibrary) { 434 mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); 435 } else { 436 mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); 437 } 438 } 439 updateDisplayForZygotes()440 private void updateDisplayForZygotes() { 441 boolean displayZygoteMemory = mShowZygoteAllocationsButton.getSelection(); 442 mPrefStore.setValue(PREFS_SHOW_ZYGOTE_ALLOCATIONS, displayZygoteMemory); 443 444 // inform the content providers of the zygote display setting 445 mContentProviderByLibrary.displayZygoteMemory(displayZygoteMemory); 446 mContentProviderByAllocations.displayZygoteMemory(displayZygoteMemory); 447 448 // refresh the UI 449 mDetailsTreeViewer.refresh(); 450 } 451 updateSnapshotIndexCombo()452 private void updateSnapshotIndexCombo() { 453 List<String> items = new ArrayList<String>(); 454 455 int numSnapshots = mNativeHeapSnapshots.size(); 456 for (int i = 0; i < numSnapshots; i++) { 457 // offset indices by 1 so that users see index starting at 1 rather than 0 458 items.add("Snapshot " + (i + 1)); 459 } 460 461 mSnapshotIndexCombo.setItems(items.toArray(new String[0])); 462 463 if (numSnapshots > 0) { 464 mSnapshotIndexCombo.setEnabled(true); 465 mSnapshotIndexCombo.select(numSnapshots - 1); 466 } else { 467 mSnapshotIndexCombo.setEnabled(false); 468 } 469 } 470 updateToolbars()471 private void updateToolbars() { 472 int numSnapshots = mNativeHeapSnapshots.size(); 473 mExportHeapDataButton.setEnabled(numSnapshots > 0); 474 } 475 476 @Override createControl(Composite parent)477 protected Control createControl(Composite parent) { 478 parent.setLayout(new GridLayout(1, false)); 479 480 Composite c = new Composite(parent, SWT.NONE); 481 c.setLayout(new GridLayout(1, false)); 482 c.setLayoutData(new GridData(GridData.FILL_BOTH)); 483 484 createControlsSection(c); 485 createDetailsSection(c); 486 487 // Initialize widget state based on whether a client 488 // is selected or not. 489 clientSelected(); 490 491 return c; 492 } 493 createControlsSection(Composite parent)494 private void createControlsSection(Composite parent) { 495 Composite c = new Composite(parent, SWT.NONE); 496 c.setLayout(new GridLayout(3, false)); 497 c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 498 499 createGetHeapDataSection(c); 500 501 Label l = new Label(c, SWT.SEPARATOR | SWT.VERTICAL); 502 l.setLayoutData(new GridData(GridData.FILL_VERTICAL)); 503 504 createDisplaySection(c); 505 } 506 createGetHeapDataSection(Composite parent)507 private void createGetHeapDataSection(Composite parent) { 508 Composite c = new Composite(parent, SWT.NONE); 509 c.setLayout(new GridLayout(1, false)); 510 511 createTakeHeapSnapshotButton(c); 512 513 Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL); 514 l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 515 516 createLoadHeapDataButton(c); 517 } 518 createTakeHeapSnapshotButton(Composite parent)519 private void createTakeHeapSnapshotButton(Composite parent) { 520 mSnapshotHeapButton = new Button(parent, SWT.BORDER | SWT.PUSH); 521 mSnapshotHeapButton.setText(SNAPSHOT_HEAP_BUTTON_TEXT); 522 mSnapshotHeapButton.setLayoutData(new GridData()); 523 524 // disable by default, enabled only when a client is selected 525 mSnapshotHeapButton.setEnabled(false); 526 527 mSnapshotHeapButton.addSelectionListener(new SelectionAdapter() { 528 @Override 529 public void widgetSelected(SelectionEvent evt) { 530 snapshotHeap(); 531 } 532 }); 533 } 534 snapshotHeap()535 private void snapshotHeap() { 536 Client c = getCurrentClient(); 537 assert c != null : "Snapshot Heap could not have been enabled w/o a selected client."; 538 539 // send an async request 540 c.requestNativeHeapInformation(); 541 } 542 createLoadHeapDataButton(Composite parent)543 private void createLoadHeapDataButton(Composite parent) { 544 mLoadHeapDataButton = new Button(parent, SWT.BORDER | SWT.PUSH); 545 mLoadHeapDataButton.setText(LOAD_HEAP_DATA_BUTTON_TEXT); 546 mLoadHeapDataButton.setLayoutData(new GridData()); 547 548 // disable by default, enabled only when a client is selected 549 mLoadHeapDataButton.setEnabled(false); 550 551 mLoadHeapDataButton.addSelectionListener(new SelectionAdapter() { 552 @Override 553 public void widgetSelected(SelectionEvent evt) { 554 loadHeapDataFromFile(); 555 } 556 }); 557 } 558 loadHeapDataFromFile()559 private void loadHeapDataFromFile() { 560 // pop up a file dialog and get the file to load 561 final String path = getHeapDumpToImport(); 562 if (path == null) { 563 return; 564 } 565 566 Reader reader = null; 567 try { 568 reader = new FileReader(path); 569 } catch (FileNotFoundException e) { 570 // cannot occur since user input was via a FileDialog 571 } 572 573 Shell shell = Display.getDefault().getActiveShell(); 574 ProgressMonitorDialog d = new ProgressMonitorDialog(shell); 575 576 NativeHeapDataImporter importer = new NativeHeapDataImporter(reader); 577 try { 578 d.run(true, true, importer); 579 } catch (InvocationTargetException e) { 580 // exception while parsing, display error to user and then return 581 MessageDialog.openError(shell, 582 "Error Importing Heap Data", 583 e.getCause().getMessage()); 584 return; 585 } catch (InterruptedException e) { 586 // operation cancelled by user, simply return 587 return; 588 } 589 590 NativeHeapSnapshot snapshot = importer.getImportedSnapshot(); 591 592 addToImportedSnapshots(snapshot); // save imported snapshot for future use 593 addNativeHeapSnapshot(snapshot); // add to currently displayed snapshots as well 594 595 updateDisplay(); 596 } 597 addToImportedSnapshots(NativeHeapSnapshot snapshot)598 private void addToImportedSnapshots(NativeHeapSnapshot snapshot) { 599 Client c = getCurrentClient(); 600 601 if (c == null) { 602 return; 603 } 604 605 Integer pid = c.getClientData().getPid(); 606 List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get(pid); 607 if (importedSnapshots == null) { 608 importedSnapshots = new ArrayList<NativeHeapSnapshot>(); 609 } 610 611 importedSnapshots.add(snapshot); 612 mImportedSnapshotsPerPid.put(pid, importedSnapshots); 613 } 614 getHeapDumpToImport()615 private String getHeapDumpToImport() { 616 FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), 617 SWT.OPEN); 618 619 fileDialog.setText("Import Heap Dump"); 620 fileDialog.setFilterExtensions(new String[] {"*.txt"}); 621 fileDialog.setFilterPath(mPrefStore.getString(PREFS_LAST_IMPORTED_HEAPPATH)); 622 623 String selectedFile = fileDialog.open(); 624 if (selectedFile != null) { 625 // save the path to restore in future dialog open 626 mPrefStore.setValue(PREFS_LAST_IMPORTED_HEAPPATH, new File(selectedFile).getParent()); 627 } 628 return selectedFile; 629 } 630 createDisplaySection(Composite parent)631 private void createDisplaySection(Composite parent) { 632 Composite c = new Composite(parent, SWT.NONE); 633 c.setLayout(new GridLayout(2, false)); 634 c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 635 636 // Create: Display: __________________ 637 createLabel(c, "Display:"); 638 mSnapshotIndexCombo = new Combo(c, SWT.NONE | SWT.READ_ONLY); 639 mSnapshotIndexCombo.setItems(new String[] {"No heap snapshots available."}); 640 mSnapshotIndexCombo.setEnabled(false); 641 mSnapshotIndexCombo.addSelectionListener(new SelectionAdapter() { 642 @Override 643 public void widgetSelected(SelectionEvent arg0) { 644 displaySelectedSnapshot(); 645 } 646 }); 647 648 // Create: Memory Allocated (bytes): _________________ 649 createLabel(c, "Memory Allocated:"); 650 mMemoryAllocatedText = new Label(c, SWT.NONE); 651 GridData gd = new GridData(); 652 gd.widthHint = 100; 653 mMemoryAllocatedText.setLayoutData(gd); 654 655 // Create: Search Path: __________________ 656 createLabel(c, SYMBOL_SEARCH_PATH_LABEL_TEXT); 657 mSymbolSearchPathText = new Text(c, SWT.BORDER); 658 mSymbolSearchPathText.setMessage(SYMBOL_SEARCH_PATH_TEXT_MESSAGE); 659 mSymbolSearchPathText.setToolTipText(SYMBOL_SEARCH_PATH_TOOLTIP_TEXT); 660 mSymbolSearchPathText.addModifyListener(new ModifyListener() { 661 @Override 662 public void modifyText(ModifyEvent arg0) { 663 String path = mSymbolSearchPathText.getText(); 664 updateSearchPath(path); 665 mPrefStore.setValue(PREFS_SYMBOL_SEARCH_PATH, path); 666 } 667 }); 668 mSymbolSearchPathText.setText(mPrefStore.getString(PREFS_SYMBOL_SEARCH_PATH)); 669 mSymbolSearchPathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 670 } 671 updateSearchPath(String path)672 private void updateSearchPath(String path) { 673 Addr2Line.setSearchPath(path); 674 } 675 createLabel(Composite parent, String text)676 private void createLabel(Composite parent, String text) { 677 Label l = new Label(parent, SWT.NONE); 678 l.setText(text); 679 GridData gd = new GridData(); 680 gd.horizontalAlignment = SWT.RIGHT; 681 l.setLayoutData(gd); 682 } 683 684 /** 685 * Create the details section displaying the details table and the stack trace 686 * corresponding to the selection. 687 * 688 * The details is laid out like so: 689 * Details Toolbar 690 * Details Table 691 * ------------sash--- 692 * Stack Trace Label 693 * Stack Trace Text 694 * There is a sash in between the two sections, and we need to save/restore the sash 695 * preferences. Using FormLayout seems like the easiest solution here, but the layout 696 * code looks ugly as a result. 697 */ createDetailsSection(Composite parent)698 private void createDetailsSection(Composite parent) { 699 final Composite c = new Composite(parent, SWT.NONE); 700 c.setLayout(new FormLayout()); 701 c.setLayoutData(new GridData(GridData.FILL_BOTH)); 702 703 mDetailsToolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER); 704 initializeDetailsToolBar(mDetailsToolBar); 705 706 Tree detailsTree = new Tree(c, SWT.VIRTUAL | SWT.BORDER | SWT.MULTI); 707 initializeDetailsTree(detailsTree); 708 709 final Sash sash = new Sash(c, SWT.HORIZONTAL | SWT.BORDER); 710 711 Label stackTraceLabel = new Label(c, SWT.NONE); 712 stackTraceLabel.setText("Stack Trace:"); 713 714 Tree stackTraceTree = new Tree(c, SWT.BORDER | SWT.MULTI); 715 initializeStackTraceTree(stackTraceTree); 716 717 // layout the widgets created above 718 FormData data = new FormData(); 719 data.top = new FormAttachment(0, 0); 720 data.left = new FormAttachment(0, 0); 721 data.right = new FormAttachment(100, 0); 722 mDetailsToolBar.setLayoutData(data); 723 724 data = new FormData(); 725 data.top = new FormAttachment(mDetailsToolBar, 0); 726 data.bottom = new FormAttachment(sash, 0); 727 data.left = new FormAttachment(0, 0); 728 data.right = new FormAttachment(100, 0); 729 detailsTree.setLayoutData(data); 730 731 final FormData sashData = new FormData(); 732 sashData.top = new FormAttachment(mPrefStore.getInt(PREFS_SASH_HEIGHT_PERCENT), 0); 733 sashData.left = new FormAttachment(0, 0); 734 sashData.right = new FormAttachment(100, 0); 735 sash.setLayoutData(sashData); 736 737 data = new FormData(); 738 data.top = new FormAttachment(sash, 0); 739 data.left = new FormAttachment(0, 0); 740 data.right = new FormAttachment(100, 0); 741 stackTraceLabel.setLayoutData(data); 742 743 data = new FormData(); 744 data.top = new FormAttachment(stackTraceLabel, 0); 745 data.left = new FormAttachment(0, 0); 746 data.bottom = new FormAttachment(100, 0); 747 data.right = new FormAttachment(100, 0); 748 stackTraceTree.setLayoutData(data); 749 750 sash.addListener(SWT.Selection, new Listener() { 751 @Override 752 public void handleEvent(Event e) { 753 Rectangle sashRect = sash.getBounds(); 754 Rectangle panelRect = c.getClientArea(); 755 int sashPercent = sashRect.y * 100 / panelRect.height; 756 mPrefStore.setValue(PREFS_SASH_HEIGHT_PERCENT, sashPercent); 757 758 sashData.top = new FormAttachment(0, e.y); 759 c.layout(); 760 } 761 }); 762 } 763 initializeDetailsToolBar(ToolBar toolbar)764 private void initializeDetailsToolBar(ToolBar toolbar) { 765 mGroupByButton = new ToolItem(toolbar, SWT.CHECK); 766 mGroupByButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(GROUPBY_IMAGE, 767 toolbar.getDisplay())); 768 mGroupByButton.setToolTipText(TOOLTIP_GROUPBY); 769 mGroupByButton.setSelection(mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)); 770 mGroupByButton.addSelectionListener(new SelectionAdapter() { 771 @Override 772 public void widgetSelected(SelectionEvent arg0) { 773 updateDisplayGrouping(); 774 } 775 }); 776 777 mDiffsOnlyButton = new ToolItem(toolbar, SWT.CHECK); 778 mDiffsOnlyButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(DIFFS_ONLY_IMAGE, 779 toolbar.getDisplay())); 780 mDiffsOnlyButton.setToolTipText(TOOLTIP_DIFFS_ONLY); 781 mDiffsOnlyButton.setSelection(mPrefStore.getBoolean(PREFS_SHOW_DIFFS_ONLY)); 782 mDiffsOnlyButton.addSelectionListener(new SelectionAdapter() { 783 @Override 784 public void widgetSelected(SelectionEvent arg0) { 785 // simply refresh the display, as the display logic takes care of 786 // the current state of the diffs only checkbox. 787 int idx = mSnapshotIndexCombo.getSelectionIndex(); 788 displaySnapshot(idx); 789 } 790 }); 791 792 mShowZygoteAllocationsButton = new ToolItem(toolbar, SWT.CHECK); 793 mShowZygoteAllocationsButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( 794 ZYGOTE_IMAGE, toolbar.getDisplay())); 795 mShowZygoteAllocationsButton.setToolTipText(TOOLTIP_ZYGOTE_ALLOCATIONS); 796 mShowZygoteAllocationsButton.setSelection( 797 mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS)); 798 mShowZygoteAllocationsButton.addSelectionListener(new SelectionAdapter() { 799 @Override 800 public void widgetSelected(SelectionEvent arg0) { 801 updateDisplayForZygotes(); 802 } 803 }); 804 805 mExportHeapDataButton = new ToolItem(toolbar, SWT.PUSH); 806 mExportHeapDataButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( 807 EXPORT_DATA_IMAGE, toolbar.getDisplay())); 808 mExportHeapDataButton.setToolTipText(TOOLTIP_EXPORT_DATA); 809 mExportHeapDataButton.addSelectionListener(new SelectionAdapter() { 810 @Override 811 public void widgetSelected(SelectionEvent arg0) { 812 exportSnapshot(); 813 } 814 }); 815 } 816 817 /** Export currently displayed snapshot to a file */ exportSnapshot()818 private void exportSnapshot() { 819 int idx = mSnapshotIndexCombo.getSelectionIndex(); 820 String snapshotName = mSnapshotIndexCombo.getItem(idx); 821 822 FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), 823 SWT.SAVE); 824 825 fileDialog.setText("Save " + snapshotName); 826 fileDialog.setFileName("allocations.txt"); 827 828 final String fileName = fileDialog.open(); 829 if (fileName == null) { 830 return; 831 } 832 833 final NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(idx); 834 Thread t = new Thread(new Runnable() { 835 @Override 836 public void run() { 837 PrintWriter out; 838 try { 839 out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); 840 } catch (IOException e) { 841 displayErrorMessage(e.getMessage()); 842 return; 843 } 844 845 for (NativeAllocationInfo alloc : snapshot.getAllocations()) { 846 out.println(alloc.toString()); 847 } 848 out.close(); 849 } 850 851 private void displayErrorMessage(final String message) { 852 Display.getDefault().syncExec(new Runnable() { 853 @Override 854 public void run() { 855 MessageDialog.openError(Display.getDefault().getActiveShell(), 856 "Failed to export heap data", message); 857 } 858 }); 859 } 860 }); 861 t.setName("Saving Heap Data to File..."); 862 t.start(); 863 } 864 initializeDetailsTree(Tree tree)865 private void initializeDetailsTree(Tree tree) { 866 tree.setHeaderVisible(true); 867 tree.setLinesVisible(true); 868 869 List<String> properties = Arrays.asList(new String[] { 870 "Library", 871 "Total", 872 "Percentage", 873 "Count", 874 "Size", 875 "Method", 876 }); 877 878 List<String> sampleValues = Arrays.asList(new String[] { 879 "/path/in/device/to/system/library.so", 880 "123456789", 881 " 100%", 882 "123456789", 883 "123456789", 884 "PossiblyLongDemangledMethodName", 885 }); 886 887 // right align numeric values 888 List<Integer> swtFlags = Arrays.asList(new Integer[] { 889 SWT.LEFT, 890 SWT.RIGHT, 891 SWT.RIGHT, 892 SWT.RIGHT, 893 SWT.RIGHT, 894 SWT.LEFT, 895 }); 896 897 for (int i = 0; i < properties.size(); i++) { 898 String p = properties.get(i); 899 String v = sampleValues.get(i); 900 int flags = swtFlags.get(i); 901 TableHelper.createTreeColumn(tree, p, flags, v, getPref("details", p), mPrefStore); 902 } 903 904 mDetailsTreeViewer = new TreeViewer(tree); 905 906 mDetailsTreeViewer.setUseHashlookup(true); 907 908 boolean displayZygotes = mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS); 909 mContentProviderByAllocations = new NativeHeapProviderByAllocations(mDetailsTreeViewer, 910 displayZygotes); 911 mContentProviderByLibrary = new NativeHeapProviderByLibrary(mDetailsTreeViewer, 912 displayZygotes); 913 if (mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)) { 914 mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); 915 } else { 916 mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); 917 } 918 919 mDetailsTreeLabelProvider = new NativeHeapLabelProvider(); 920 mDetailsTreeViewer.setLabelProvider(mDetailsTreeLabelProvider); 921 922 mDetailsTreeViewer.setInput(null); 923 924 tree.addSelectionListener(new SelectionAdapter() { 925 @Override 926 public void widgetSelected(SelectionEvent event) { 927 displayStackTraceForSelection(); 928 } 929 }); 930 } 931 initializeStackTraceTree(Tree tree)932 private void initializeStackTraceTree(Tree tree) { 933 tree.setHeaderVisible(true); 934 tree.setLinesVisible(true); 935 936 List<String> properties = Arrays.asList(new String[] { 937 "Address", 938 "Library", 939 "Method", 940 "File", 941 "Line", 942 }); 943 944 List<String> sampleValues = Arrays.asList(new String[] { 945 "0x1234_5678", 946 "/path/in/device/to/system/library.so", 947 "PossiblyLongDemangledMethodName", 948 "/android/out/prefix/in/home/directory/to/path/in/device/to/system/library.so", 949 "2000", 950 }); 951 952 for (int i = 0; i < properties.size(); i++) { 953 String p = properties.get(i); 954 String v = sampleValues.get(i); 955 TableHelper.createTreeColumn(tree, p, SWT.LEFT, v, getPref("stack", p), mPrefStore); 956 } 957 958 mStackTraceTreeViewer = new TreeViewer(tree); 959 960 mStackTraceTreeViewer.setContentProvider(new NativeStackContentProvider()); 961 mStackTraceTreeViewer.setLabelProvider(new NativeStackLabelProvider()); 962 963 mStackTraceTreeViewer.setInput(null); 964 } 965 displayStackTraceForSelection()966 private void displayStackTraceForSelection() { 967 TreeItem []items = mDetailsTreeViewer.getTree().getSelection(); 968 if (items.length == 0) { 969 mStackTraceTreeViewer.setInput(null); 970 return; 971 } 972 973 Object data = items[0].getData(); 974 if (!(data instanceof NativeAllocationInfo)) { 975 mStackTraceTreeViewer.setInput(null); 976 return; 977 } 978 979 NativeAllocationInfo info = (NativeAllocationInfo) data; 980 if (info.isStackCallResolved()) { 981 mStackTraceTreeViewer.setInput(info.getResolvedStackCall()); 982 } else { 983 mStackTraceTreeViewer.setInput(info.getStackCallAddresses()); 984 } 985 } 986 getPref(String prefix, String s)987 private String getPref(String prefix, String s) { 988 return "nativeheap.tree." + prefix + "." + s; 989 } 990 991 @Override setFocus()992 public void setFocus() { 993 } 994 995 private ITableFocusListener mTableFocusListener; 996 997 @Override setTableFocusListener(ITableFocusListener listener)998 public void setTableFocusListener(ITableFocusListener listener) { 999 mTableFocusListener = listener; 1000 1001 final Tree heapSitesTree = mDetailsTreeViewer.getTree(); 1002 final IFocusedTableActivator heapSitesActivator = new IFocusedTableActivator() { 1003 @Override 1004 public void copy(Clipboard clipboard) { 1005 TreeItem[] items = heapSitesTree.getSelection(); 1006 copyToClipboard(items, clipboard); 1007 } 1008 1009 @Override 1010 public void selectAll() { 1011 heapSitesTree.selectAll(); 1012 } 1013 }; 1014 1015 heapSitesTree.addFocusListener(new FocusListener() { 1016 @Override 1017 public void focusLost(FocusEvent arg0) { 1018 mTableFocusListener.focusLost(heapSitesActivator); 1019 } 1020 1021 @Override 1022 public void focusGained(FocusEvent arg0) { 1023 mTableFocusListener.focusGained(heapSitesActivator); 1024 } 1025 }); 1026 1027 final Tree stackTraceTree = mStackTraceTreeViewer.getTree(); 1028 final IFocusedTableActivator stackTraceActivator = new IFocusedTableActivator() { 1029 @Override 1030 public void copy(Clipboard clipboard) { 1031 TreeItem[] items = stackTraceTree.getSelection(); 1032 copyToClipboard(items, clipboard); 1033 } 1034 1035 @Override 1036 public void selectAll() { 1037 stackTraceTree.selectAll(); 1038 } 1039 }; 1040 1041 stackTraceTree.addFocusListener(new FocusListener() { 1042 @Override 1043 public void focusLost(FocusEvent arg0) { 1044 mTableFocusListener.focusLost(stackTraceActivator); 1045 } 1046 1047 @Override 1048 public void focusGained(FocusEvent arg0) { 1049 mTableFocusListener.focusGained(stackTraceActivator); 1050 } 1051 }); 1052 } 1053 copyToClipboard(TreeItem[] items, Clipboard clipboard)1054 private void copyToClipboard(TreeItem[] items, Clipboard clipboard) { 1055 StringBuilder sb = new StringBuilder(); 1056 1057 for (TreeItem item : items) { 1058 Object data = item.getData(); 1059 if (data != null) { 1060 sb.append(data.toString()); 1061 sb.append('\n'); 1062 } 1063 } 1064 1065 String content = sb.toString(); 1066 if (content.length() > 0) { 1067 clipboard.setContents( 1068 new Object[] {sb.toString()}, 1069 new Transfer[] {TextTransfer.getInstance()} 1070 ); 1071 } 1072 } 1073 1074 private class SymbolResolverTask implements Runnable { 1075 private List<NativeAllocationInfo> mCallSites; 1076 private List<NativeLibraryMapInfo> mMappedLibraries; 1077 private Map<Long, NativeStackCallInfo> mResolvedSymbolCache; 1078 SymbolResolverTask(List<NativeAllocationInfo> callSites, List<NativeLibraryMapInfo> mappedLibraries)1079 public SymbolResolverTask(List<NativeAllocationInfo> callSites, 1080 List<NativeLibraryMapInfo> mappedLibraries) { 1081 mCallSites = callSites; 1082 mMappedLibraries = mappedLibraries; 1083 1084 mResolvedSymbolCache = new HashMap<Long, NativeStackCallInfo>(); 1085 } 1086 1087 @Override run()1088 public void run() { 1089 for (NativeAllocationInfo callSite : mCallSites) { 1090 if (callSite.isStackCallResolved()) { 1091 continue; 1092 } 1093 1094 List<Long> addresses = callSite.getStackCallAddresses(); 1095 List<NativeStackCallInfo> resolvedStackInfo = 1096 new ArrayList<NativeStackCallInfo>(addresses.size()); 1097 1098 for (Long address : addresses) { 1099 NativeStackCallInfo info = mResolvedSymbolCache.get(address); 1100 1101 if (info != null) { 1102 resolvedStackInfo.add(info); 1103 } else { 1104 info = resolveAddress(address); 1105 resolvedStackInfo.add(info); 1106 mResolvedSymbolCache.put(address, info); 1107 } 1108 } 1109 1110 callSite.setResolvedStackCall(resolvedStackInfo); 1111 } 1112 1113 Display.getDefault().asyncExec(new Runnable() { 1114 @Override 1115 public void run() { 1116 mDetailsTreeViewer.refresh(); 1117 mStackTraceTreeViewer.refresh(); 1118 } 1119 }); 1120 } 1121 resolveAddress(long addr)1122 private NativeStackCallInfo resolveAddress(long addr) { 1123 NativeLibraryMapInfo library = getLibraryFor(addr); 1124 1125 if (library != null) { 1126 Addr2Line process = Addr2Line.getProcess(library); 1127 if (process != null) { 1128 NativeStackCallInfo info = process.getAddress(addr); 1129 if (info != null) { 1130 return info; 1131 } 1132 } 1133 } 1134 1135 return new NativeStackCallInfo(addr, 1136 library != null ? library.getLibraryName() : null, 1137 Long.toHexString(addr), 1138 ""); 1139 } 1140 getLibraryFor(long addr)1141 private NativeLibraryMapInfo getLibraryFor(long addr) { 1142 for (NativeLibraryMapInfo info : mMappedLibraries) { 1143 if (info.isWithinLibrary(addr)) { 1144 return info; 1145 } 1146 } 1147 1148 Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); 1149 return null; 1150 } 1151 } 1152 } 1153