| /* |
| * 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.ClientData; |
| import com.android.ddmlib.Log; |
| import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; |
| import com.android.ddmlib.HeapSegment.HeapSegmentElement; |
| |
| import org.eclipse.jface.preference.IPreferenceStore; |
| 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.Font; |
| import org.eclipse.swt.graphics.FontData; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.ImageData; |
| import org.eclipse.swt.graphics.PaletteData; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.RGB; |
| 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.Group; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Table; |
| import org.eclipse.swt.widgets.TableColumn; |
| import org.eclipse.swt.widgets.TableItem; |
| import org.jfree.chart.ChartFactory; |
| import org.jfree.chart.JFreeChart; |
| import org.jfree.chart.axis.CategoryAxis; |
| import org.jfree.chart.axis.CategoryLabelPositions; |
| import org.jfree.chart.labels.CategoryToolTipGenerator; |
| import org.jfree.chart.plot.CategoryPlot; |
| import org.jfree.chart.plot.Plot; |
| import org.jfree.chart.plot.PlotOrientation; |
| import org.jfree.chart.renderer.category.CategoryItemRenderer; |
| import org.jfree.chart.title.TextTitle; |
| import org.jfree.data.category.CategoryDataset; |
| import org.jfree.data.category.DefaultCategoryDataset; |
| import org.jfree.experimental.chart.swt.ChartComposite; |
| import org.jfree.experimental.swt.SWTUtils; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.text.NumberFormat; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| |
| /** |
| * Base class for our information panels. |
| */ |
| public final class HeapPanel extends BaseHeapPanel { |
| private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$ |
| private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$ |
| private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$ |
| private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$ |
| private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$ |
| private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$ |
| private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$ |
| |
| /* args to setUpdateStatus() */ |
| private static final int NOT_SELECTED = 0; |
| private static final int NOT_ENABLED = 1; |
| private static final int ENABLED = 2; |
| |
| /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need |
| * Native+1 at least. We also need 2 more entries for free area and expansion area. */ |
| private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; |
| private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; |
| private static final PaletteData mMapPalette = createPalette(); |
| |
| private static final boolean DISPLAY_HEAP_BITMAP = false; |
| private static final boolean DISPLAY_HILBERT_BITMAP = false; |
| |
| private static final int PLACEHOLDER_HILBERT_SIZE = 200; |
| private static final int PLACEHOLDER_LINEAR_V_SIZE = 100; |
| private static final int PLACEHOLDER_LINEAR_H_SIZE = 300; |
| |
| private static final int[] ZOOMS = {100, 50, 25}; |
| |
| private static final NumberFormat sByteFormatter = NumberFormat.getInstance(); |
| private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance(); |
| private static final NumberFormat sCountFormatter = NumberFormat.getInstance(); |
| |
| static { |
| sByteFormatter.setMinimumFractionDigits(0); |
| sByteFormatter.setMaximumFractionDigits(1); |
| sLargeByteFormatter.setMinimumFractionDigits(3); |
| sLargeByteFormatter.setMaximumFractionDigits(3); |
| |
| sCountFormatter.setGroupingUsed(true); |
| } |
| |
| private Display mDisplay; |
| |
| private Composite mTop; // real top |
| private Label mUpdateStatus; |
| private Table mHeapSummary; |
| private Combo mDisplayMode; |
| |
| //private ScrolledComposite mScrolledComposite; |
| |
| private Composite mDisplayBase; // base of the displays. |
| private StackLayout mDisplayStack; |
| |
| private Composite mStatisticsBase; |
| private Table mStatisticsTable; |
| private JFreeChart mChart; |
| private ChartComposite mChartComposite; |
| private Button mGcButton; |
| private DefaultCategoryDataset mAllocCountDataSet; |
| |
| private Composite mLinearBase; |
| private Label mLinearHeapImage; |
| |
| private Composite mHilbertBase; |
| private Label mHilbertHeapImage; |
| private Group mLegend; |
| private Combo mZoom; |
| |
| /** Image used for the hilbert display. Since we recreate a new image every time, we |
| * keep this one around to dispose it. */ |
| private Image mHilbertImage; |
| private Image mLinearImage; |
| private Composite[] mLayout; |
| |
| /* |
| * Create color palette for map. Set up titles for legend. |
| */ |
| private static PaletteData createPalette() { |
| RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; |
| colors[0] |
| = new RGB(192, 192, 192); // non-heap pixels are gray |
| mMapLegend[0] |
| = "(heap expansion area)"; |
| |
| colors[1] |
| = new RGB(0, 0, 0); // free chunks are black |
| mMapLegend[1] |
| = "free"; |
| |
| colors[HeapSegmentElement.KIND_OBJECT + 2] |
| = new RGB(0, 0, 255); // objects are blue |
| mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] |
| = "data object"; |
| |
| colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] |
| = new RGB(0, 255, 0); // class objects are green |
| mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] |
| = "class object"; |
| |
| colors[HeapSegmentElement.KIND_ARRAY_1 + 2] |
| = new RGB(255, 0, 0); // byte/bool arrays are red |
| mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] |
| = "1-byte array (byte[], boolean[])"; |
| |
| colors[HeapSegmentElement.KIND_ARRAY_2 + 2] |
| = new RGB(255, 128, 0); // short/char arrays are orange |
| mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] |
| = "2-byte array (short[], char[])"; |
| |
| colors[HeapSegmentElement.KIND_ARRAY_4 + 2] |
| = new RGB(255, 255, 0); // obj/int/float arrays are yellow |
| mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] |
| = "4-byte array (object[], int[], float[])"; |
| |
| colors[HeapSegmentElement.KIND_ARRAY_8 + 2] |
| = new RGB(255, 128, 128); // long/double arrays are pink |
| mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] |
| = "8-byte array (long[], double[])"; |
| |
| colors[HeapSegmentElement.KIND_UNKNOWN + 2] |
| = new RGB(255, 0, 255); // unknown objects are cyan |
| mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] |
| = "unknown object"; |
| |
| colors[HeapSegmentElement.KIND_NATIVE + 2] |
| = new RGB(64, 64, 64); // native objects are dark gray |
| mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] |
| = "non-Java object"; |
| |
| return new PaletteData(colors); |
| } |
| |
| /** |
| * 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_INTEREST}, {@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_HEAP_MODE) == Client.CHANGE_HEAP_MODE || |
| (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) { |
| try { |
| mTop.getDisplay().asyncExec(new Runnable() { |
| public void run() { |
| clientSelected(); |
| } |
| }); |
| } catch (SWTException e) { |
| // display is disposed (app is quitting most likely), 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 (mTop.isDisposed()) |
| return; |
| |
| Client client = getCurrentClient(); |
| |
| Log.d("ddms", "HeapPanel: changed " + client); |
| |
| if (client != null) { |
| ClientData cd = client.getClientData(); |
| |
| if (client.isHeapUpdateEnabled()) { |
| mGcButton.setEnabled(true); |
| mDisplayMode.setEnabled(true); |
| setUpdateStatus(ENABLED); |
| } else { |
| setUpdateStatus(NOT_ENABLED); |
| mGcButton.setEnabled(false); |
| mDisplayMode.setEnabled(false); |
| } |
| |
| fillSummaryTable(cd); |
| |
| int mode = mDisplayMode.getSelectionIndex(); |
| if (mode == 0) { |
| fillDetailedTable(client, false /* forceRedraw */); |
| } else { |
| if (DISPLAY_HEAP_BITMAP) { |
| renderHeapData(cd, mode - 1, false /* forceRedraw */); |
| } |
| } |
| } else { |
| mGcButton.setEnabled(false); |
| mDisplayMode.setEnabled(false); |
| fillSummaryTable(null); |
| fillDetailedTable(null, true); |
| setUpdateStatus(NOT_SELECTED); |
| } |
| |
| // sizes of things change frequently, so redo layout |
| //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, |
| // SWT.DEFAULT)); |
| mDisplayBase.layout(); |
| //mScrolledComposite.redraw(); |
| } |
| |
| /** |
| * Create our control(s). |
| */ |
| @Override |
| protected Control createControl(Composite parent) { |
| mDisplay = parent.getDisplay(); |
| |
| GridLayout gl; |
| |
| mTop = new Composite(parent, SWT.NONE); |
| mTop.setLayout(new GridLayout(1, false)); |
| mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| |
| mUpdateStatus = new Label(mTop, SWT.NONE); |
| setUpdateStatus(NOT_SELECTED); |
| |
| Composite summarySection = new Composite(mTop, SWT.NONE); |
| summarySection.setLayout(gl = new GridLayout(2, false)); |
| gl.marginHeight = gl.marginWidth = 0; |
| |
| mHeapSummary = createSummaryTable(summarySection); |
| mGcButton = new Button(summarySection, SWT.PUSH); |
| mGcButton.setText("Cause GC"); |
| mGcButton.setEnabled(false); |
| mGcButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| Client client = getCurrentClient(); |
| if (client != null) { |
| client.executeGarbageCollector(); |
| } |
| } |
| }); |
| |
| Composite comboSection = new Composite(mTop, SWT.NONE); |
| gl = new GridLayout(2, false); |
| gl.marginHeight = gl.marginWidth = 0; |
| comboSection.setLayout(gl); |
| |
| Label displayLabel = new Label(comboSection, SWT.NONE); |
| displayLabel.setText("Display: "); |
| |
| mDisplayMode = new Combo(comboSection, SWT.READ_ONLY); |
| mDisplayMode.setEnabled(false); |
| mDisplayMode.add("Stats"); |
| if (DISPLAY_HEAP_BITMAP) { |
| mDisplayMode.add("Linear"); |
| if (DISPLAY_HILBERT_BITMAP) { |
| mDisplayMode.add("Hilbert"); |
| } |
| } |
| |
| // the base of the displays. |
| mDisplayBase = new Composite(mTop, SWT.NONE); |
| mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| mDisplayStack = new StackLayout(); |
| mDisplayBase.setLayout(mDisplayStack); |
| |
| // create the statistics display |
| mStatisticsBase = new Composite(mDisplayBase, SWT.NONE); |
| //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| mStatisticsBase.setLayout(gl = new GridLayout(1, false)); |
| gl.marginHeight = gl.marginWidth = 0; |
| mDisplayStack.topControl = mStatisticsBase; |
| |
| mStatisticsTable = createDetailedTable(mStatisticsBase); |
| mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| |
| createChart(); |
| |
| //create the linear composite |
| mLinearBase = new Composite(mDisplayBase, SWT.NONE); |
| //mLinearBase.setLayoutData(new GridData()); |
| gl = new GridLayout(1, false); |
| gl.marginHeight = gl.marginWidth = 0; |
| mLinearBase.setLayout(gl); |
| |
| { |
| mLinearHeapImage = new Label(mLinearBase, SWT.NONE); |
| mLinearHeapImage.setLayoutData(new GridData()); |
| mLinearHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, |
| PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, |
| mDisplay.getSystemColor(SWT.COLOR_BLUE))); |
| |
| // create a composite to contain the bottom part (legend) |
| Composite bottomSection = new Composite(mLinearBase, SWT.NONE); |
| gl = new GridLayout(1, false); |
| gl.marginHeight = gl.marginWidth = 0; |
| bottomSection.setLayout(gl); |
| |
| createLegend(bottomSection); |
| } |
| |
| /* |
| mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL); |
| mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| mScrolledComposite.setExpandHorizontal(true); |
| mScrolledComposite.setExpandVertical(true); |
| mScrolledComposite.setContent(mDisplayBase); |
| */ |
| |
| |
| // create the hilbert display. |
| mHilbertBase = new Composite(mDisplayBase, SWT.NONE); |
| //mHilbertBase.setLayoutData(new GridData()); |
| gl = new GridLayout(2, false); |
| gl.marginHeight = gl.marginWidth = 0; |
| mHilbertBase.setLayout(gl); |
| |
| if (DISPLAY_HILBERT_BITMAP) { |
| mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE); |
| mHilbertHeapImage.setLayoutData(new GridData()); |
| mHilbertHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, |
| PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, |
| mDisplay.getSystemColor(SWT.COLOR_BLUE))); |
| |
| // create a composite to contain the right part (legend + zoom) |
| Composite rightSection = new Composite(mHilbertBase, SWT.NONE); |
| gl = new GridLayout(1, false); |
| gl.marginHeight = gl.marginWidth = 0; |
| rightSection.setLayout(gl); |
| |
| Composite zoomComposite = new Composite(rightSection, SWT.NONE); |
| gl = new GridLayout(2, false); |
| zoomComposite.setLayout(gl); |
| |
| Label l = new Label(zoomComposite, SWT.NONE); |
| l.setText("Zoom:"); |
| mZoom = new Combo(zoomComposite, SWT.READ_ONLY); |
| for (int z : ZOOMS) { |
| mZoom.add(String.format("%1$d%%", z)); // $NON-NLS-1$ |
| } |
| |
| mZoom.select(0); |
| mZoom.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| setLegendText(mZoom.getSelectionIndex()); |
| Client client = getCurrentClient(); |
| if (client != null) { |
| renderHeapData(client.getClientData(), 1, true); |
| mTop.pack(); |
| } |
| } |
| }); |
| |
| createLegend(rightSection); |
| } |
| mHilbertBase.pack(); |
| |
| mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase }; |
| mDisplayMode.select(0); |
| mDisplayMode.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| int index = mDisplayMode.getSelectionIndex(); |
| Client client = getCurrentClient(); |
| |
| if (client != null) { |
| if (index == 0) { |
| fillDetailedTable(client, true /* forceRedraw */); |
| } else { |
| renderHeapData(client.getClientData(), index-1, true /* forceRedraw */); |
| } |
| } |
| |
| mDisplayStack.topControl = mLayout[index]; |
| //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, |
| // SWT.DEFAULT)); |
| mDisplayBase.layout(); |
| //mScrolledComposite.redraw(); |
| } |
| }); |
| |
| //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, |
| // SWT.DEFAULT)); |
| mDisplayBase.layout(); |
| //mScrolledComposite.redraw(); |
| |
| return mTop; |
| } |
| |
| /** |
| * Sets the focus to the proper control inside the panel. |
| */ |
| @Override |
| public void setFocus() { |
| mHeapSummary.setFocus(); |
| } |
| |
| |
| private Table createSummaryTable(Composite base) { |
| Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); |
| tab.setHeaderVisible(true); |
| tab.setLinesVisible(true); |
| |
| TableColumn col; |
| |
| col = new TableColumn(tab, SWT.RIGHT); |
| col.setText("ID"); |
| col.pack(); |
| |
| col = new TableColumn(tab, SWT.RIGHT); |
| col.setText("000.000WW"); // $NON-NLS-1$ |
| col.pack(); |
| col.setText("Heap Size"); |
| |
| col = new TableColumn(tab, SWT.RIGHT); |
| col.setText("000.000WW"); // $NON-NLS-1$ |
| col.pack(); |
| col.setText("Allocated"); |
| |
| col = new TableColumn(tab, SWT.RIGHT); |
| col.setText("000.000WW"); // $NON-NLS-1$ |
| col.pack(); |
| col.setText("Free"); |
| |
| col = new TableColumn(tab, SWT.RIGHT); |
| col.setText("000.00%"); // $NON-NLS-1$ |
| col.pack(); |
| col.setText("% Used"); |
| |
| col = new TableColumn(tab, SWT.RIGHT); |
| col.setText("000,000,000"); // $NON-NLS-1$ |
| col.pack(); |
| col.setText("# Objects"); |
| |
| return tab; |
| } |
| |
| private Table createDetailedTable(Composite base) { |
| IPreferenceStore store = DdmUiPreferences.getStore(); |
| |
| Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); |
| tab.setHeaderVisible(true); |
| tab.setLinesVisible(true); |
| |
| TableHelper.createTableColumn(tab, "Type", SWT.LEFT, |
| "4-byte array (object[], int[], float[])", //$NON-NLS-1$ |
| PREFS_STATS_COL_TYPE, store); |
| |
| TableHelper.createTableColumn(tab, "Count", SWT.RIGHT, |
| "00,000", //$NON-NLS-1$ |
| PREFS_STATS_COL_COUNT, store); |
| |
| TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT, |
| "000.000 WW", //$NON-NLS-1$ |
| PREFS_STATS_COL_SIZE, store); |
| |
| TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT, |
| "000.000 WW", //$NON-NLS-1$ |
| PREFS_STATS_COL_SMALLEST, store); |
| |
| TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT, |
| "000.000 WW", //$NON-NLS-1$ |
| PREFS_STATS_COL_LARGEST, store); |
| |
| TableHelper.createTableColumn(tab, "Median", SWT.RIGHT, |
| "000.000 WW", //$NON-NLS-1$ |
| PREFS_STATS_COL_MEDIAN, store); |
| |
| TableHelper.createTableColumn(tab, "Average", SWT.RIGHT, |
| "000.000 WW", //$NON-NLS-1$ |
| PREFS_STATS_COL_AVERAGE, store); |
| |
| tab.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| |
| Client client = getCurrentClient(); |
| if (client != null) { |
| int index = mStatisticsTable.getSelectionIndex(); |
| TableItem item = mStatisticsTable.getItem(index); |
| |
| if (item != null) { |
| Map<Integer, ArrayList<HeapSegmentElement>> heapMap = |
| client.getClientData().getVmHeapData().getProcessedHeapMap(); |
| |
| ArrayList<HeapSegmentElement> list = heapMap.get(item.getData()); |
| if (list != null) { |
| showChart(list); |
| } |
| } |
| } |
| |
| } |
| }); |
| |
| return tab; |
| } |
| |
| /** |
| * Creates the chart below the statistics table |
| */ |
| private void createChart() { |
| mAllocCountDataSet = new DefaultCategoryDataset(); |
| mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet, |
| PlotOrientation.VERTICAL, false, true, false); |
| |
| // get the font to make a proper title. We need to convert the swt font, |
| // into an awt font. |
| Font f = mStatisticsBase.getFont(); |
| FontData[] fData = f.getFontData(); |
| |
| // event though on Mac OS there could be more than one fontData, we'll only use |
| // the first one. |
| FontData firstFontData = fData[0]; |
| |
| java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(), |
| firstFontData, true /* ensureSameSize */); |
| |
| mChart.setTitle(new TextTitle("Allocation count per size", awtFont)); |
| |
| Plot plot = mChart.getPlot(); |
| if (plot instanceof CategoryPlot) { |
| // get the plot |
| CategoryPlot categoryPlot = (CategoryPlot)plot; |
| |
| // set the domain axis to draw labels that are displayed even with many values. |
| CategoryAxis domainAxis = categoryPlot.getDomainAxis(); |
| domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90); |
| |
| CategoryItemRenderer renderer = categoryPlot.getRenderer(); |
| renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() { |
| public String generateToolTip(CategoryDataset dataset, int row, int column) { |
| // get the key for the size of the allocation |
| ByteLong columnKey = (ByteLong)dataset.getColumnKey(column); |
| String rowKey = (String)dataset.getRowKey(row); |
| Number value = dataset.getValue(rowKey, columnKey); |
| |
| return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey, |
| columnKey.getValue()); |
| } |
| }); |
| } |
| mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart, |
| ChartComposite.DEFAULT_WIDTH, |
| ChartComposite.DEFAULT_HEIGHT, |
| ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, |
| ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, |
| 3000, // max draw width. We don't want it to zoom, so we put a big number |
| 3000, // max draw height. We don't want it to zoom, so we put a big number |
| true, // off-screen buffer |
| true, // properties |
| true, // save |
| true, // print |
| false, // zoom |
| true); // tooltips |
| |
| mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| } |
| |
| private static String prettyByteCount(long bytes) { |
| double fracBytes = bytes; |
| String units = " B"; |
| if (fracBytes < 1024) { |
| return sByteFormatter.format(fracBytes) + units; |
| } else { |
| fracBytes /= 1024; |
| units = " KB"; |
| } |
| if (fracBytes >= 1024) { |
| fracBytes /= 1024; |
| units = " MB"; |
| } |
| if (fracBytes >= 1024) { |
| fracBytes /= 1024; |
| units = " GB"; |
| } |
| |
| return sLargeByteFormatter.format(fracBytes) + units; |
| } |
| |
| private static String approximateByteCount(long bytes) { |
| double fracBytes = bytes; |
| String units = ""; |
| if (fracBytes >= 1024) { |
| fracBytes /= 1024; |
| units = "K"; |
| } |
| if (fracBytes >= 1024) { |
| fracBytes /= 1024; |
| units = "M"; |
| } |
| if (fracBytes >= 1024) { |
| fracBytes /= 1024; |
| units = "G"; |
| } |
| |
| return sByteFormatter.format(fracBytes) + units; |
| } |
| |
| private static String addCommasToNumber(long num) { |
| return sCountFormatter.format(num); |
| } |
| |
| private static String fractionalPercent(long num, long denom) { |
| double val = (double)num / (double)denom; |
| val *= 100; |
| |
| NumberFormat nf = NumberFormat.getInstance(); |
| nf.setMinimumFractionDigits(2); |
| nf.setMaximumFractionDigits(2); |
| return nf.format(val) + "%"; |
| } |
| |
| private void fillSummaryTable(ClientData cd) { |
| if (mHeapSummary.isDisposed()) { |
| return; |
| } |
| |
| mHeapSummary.setRedraw(false); |
| mHeapSummary.removeAll(); |
| |
| if (cd != null) { |
| synchronized (cd) { |
| Iterator<Integer> iter = cd.getVmHeapIds(); |
| |
| while (iter.hasNext()) { |
| Integer id = iter.next(); |
| Map<String, Long> heapInfo = cd.getVmHeapInfo(id); |
| if (heapInfo == null) { |
| continue; |
| } |
| long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES); |
| long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED); |
| long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED); |
| |
| TableItem item = new TableItem(mHeapSummary, SWT.NONE); |
| item.setText(0, id.toString()); |
| |
| item.setText(1, prettyByteCount(sizeInBytes)); |
| item.setText(2, prettyByteCount(bytesAllocated)); |
| item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated)); |
| item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes)); |
| item.setText(5, addCommasToNumber(objectsAllocated)); |
| } |
| } |
| } |
| |
| mHeapSummary.pack(); |
| mHeapSummary.setRedraw(true); |
| } |
| |
| private void fillDetailedTable(Client client, boolean forceRedraw) { |
| // first check if the client is invalid or heap updates are not enabled. |
| if (client == null || client.isHeapUpdateEnabled() == false) { |
| mStatisticsTable.removeAll(); |
| showChart(null); |
| return; |
| } |
| |
| ClientData cd = client.getClientData(); |
| |
| Map<Integer, ArrayList<HeapSegmentElement>> heapMap; |
| |
| // Atomically get and clear the heap data. |
| synchronized (cd) { |
| if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { |
| // no change, we return. |
| return; |
| } |
| |
| heapMap = cd.getVmHeapData().getProcessedHeapMap(); |
| } |
| |
| // we have new data, lets display it. |
| |
| // First, get the current selection, and its key. |
| int index = mStatisticsTable.getSelectionIndex(); |
| Integer selectedKey = null; |
| if (index != -1) { |
| selectedKey = (Integer)mStatisticsTable.getItem(index).getData(); |
| } |
| |
| // disable redraws and remove all from the table. |
| mStatisticsTable.setRedraw(false); |
| mStatisticsTable.removeAll(); |
| |
| if (heapMap != null) { |
| int selectedIndex = -1; |
| ArrayList<HeapSegmentElement> selectedList = null; |
| |
| // get the keys |
| Set<Integer> keys = heapMap.keySet(); |
| int iter = 0; // use a manual iter int because Set<?> doesn't have an index |
| // based accessor. |
| for (Integer key : keys) { |
| ArrayList<HeapSegmentElement> list = heapMap.get(key); |
| |
| // check if this is the key that is supposed to be selected |
| if (key.equals(selectedKey)) { |
| selectedIndex = iter; |
| selectedList = list; |
| } |
| iter++; |
| |
| TableItem item = new TableItem(mStatisticsTable, SWT.NONE); |
| item.setData(key); |
| |
| // get the type |
| item.setText(0, mMapLegend[key]); |
| |
| // set the count, smallest, largest |
| int count = list.size(); |
| item.setText(1, addCommasToNumber(count)); |
| |
| if (count > 0) { |
| item.setText(3, prettyByteCount(list.get(0).getLength())); |
| item.setText(4, prettyByteCount(list.get(count-1).getLength())); |
| |
| int median = count / 2; |
| HeapSegmentElement element = list.get(median); |
| long size = element.getLength(); |
| item.setText(5, prettyByteCount(size)); |
| |
| long totalSize = 0; |
| for (int i = 0 ; i < count; i++) { |
| element = list.get(i); |
| |
| size = element.getLength(); |
| totalSize += size; |
| } |
| |
| // set the average and total |
| item.setText(2, prettyByteCount(totalSize)); |
| item.setText(6, prettyByteCount(totalSize / count)); |
| } |
| } |
| |
| mStatisticsTable.setRedraw(true); |
| |
| if (selectedIndex != -1) { |
| mStatisticsTable.setSelection(selectedIndex); |
| showChart(selectedList); |
| } else { |
| showChart(null); |
| } |
| } else { |
| mStatisticsTable.setRedraw(true); |
| } |
| } |
| |
| private static class ByteLong implements Comparable<ByteLong> { |
| private long mValue; |
| |
| private ByteLong(long value) { |
| mValue = value; |
| } |
| |
| public long getValue() { |
| return mValue; |
| } |
| |
| @Override |
| public String toString() { |
| return approximateByteCount(mValue); |
| } |
| |
| public int compareTo(ByteLong other) { |
| if (mValue != other.mValue) { |
| return mValue < other.mValue ? -1 : 1; |
| } |
| return 0; |
| } |
| |
| } |
| |
| /** |
| * Fills the chart with the content of the list of {@link HeapSegmentElement}. |
| */ |
| private void showChart(ArrayList<HeapSegmentElement> list) { |
| mAllocCountDataSet.clear(); |
| |
| if (list != null) { |
| String rowKey = "Alloc Count"; |
| |
| long currentSize = -1; |
| int currentCount = 0; |
| for (HeapSegmentElement element : list) { |
| if (element.getLength() != currentSize) { |
| if (currentSize != -1) { |
| ByteLong columnKey = new ByteLong(currentSize); |
| mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); |
| } |
| |
| currentSize = element.getLength(); |
| currentCount = 1; |
| } else { |
| currentCount++; |
| } |
| } |
| |
| // add the last item |
| if (currentSize != -1) { |
| ByteLong columnKey = new ByteLong(currentSize); |
| mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); |
| } |
| } |
| } |
| |
| /* |
| * Add a color legend to the specified table. |
| */ |
| private void createLegend(Composite parent) { |
| mLegend = new Group(parent, SWT.NONE); |
| mLegend.setText(getLegendText(0)); |
| |
| mLegend.setLayout(new GridLayout(2, false)); |
| |
| RGB[] colors = mMapPalette.colors; |
| |
| for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) { |
| Image tmpImage = createColorRect(parent.getDisplay(), colors[i]); |
| |
| Label l = new Label(mLegend, SWT.NONE); |
| l.setImage(tmpImage); |
| |
| l = new Label(mLegend, SWT.NONE); |
| l.setText(mMapLegend[i]); |
| } |
| } |
| |
| private String getLegendText(int level) { |
| int bytes = 8 * (100 / ZOOMS[level]); |
| |
| return String.format("Key (1 pixel = %1$d bytes)", bytes); |
| } |
| |
| private void setLegendText(int level) { |
| mLegend.setText(getLegendText(level)); |
| |
| } |
| |
| /* |
| * Create a nice rectangle in the specified color. |
| */ |
| private Image createColorRect(Display display, RGB color) { |
| int width = 32; |
| int height = 16; |
| |
| Image img = new Image(display, width, height); |
| GC gc = new GC(img); |
| gc.setBackground(new Color(display, color)); |
| gc.fillRectangle(0, 0, width, height); |
| gc.dispose(); |
| return img; |
| } |
| |
| |
| /* |
| * Are updates enabled? |
| */ |
| private void setUpdateStatus(int status) { |
| switch (status) { |
| case NOT_SELECTED: |
| mUpdateStatus.setText("Select a client to see heap updates"); |
| break; |
| case NOT_ENABLED: |
| mUpdateStatus.setText("Heap updates are " + |
| "NOT ENABLED for this client"); |
| break; |
| case ENABLED: |
| mUpdateStatus.setText("Heap updates will happen after " + |
| "every GC for this client"); |
| break; |
| default: |
| throw new RuntimeException(); |
| } |
| |
| mUpdateStatus.pack(); |
| } |
| |
| |
| /** |
| * Return the closest power of two greater than or equal to value. |
| * |
| * @param value the return value will be >= value |
| * @return a power of two >= value. If value > 2^31, 2^31 is returned. |
| */ |
| //xxx use Integer.highestOneBit() or numberOfLeadingZeros(). |
| private int nextPow2(int value) { |
| for (int i = 31; i >= 0; --i) { |
| if ((value & (1<<i)) != 0) { |
| if (i < 31) { |
| return 1<<(i + 1); |
| } else { |
| return 1<<31; |
| } |
| } |
| } |
| return 0; |
| } |
| |
| private int zOrderData(ImageData id, byte pixData[]) { |
| int maxX = 0; |
| for (int i = 0; i < pixData.length; i++) { |
| /* Tread the pixData index as a z-order curve index and |
| * decompose into Cartesian coordinates. |
| */ |
| int x = (i & 1) | |
| ((i >>> 2) & 1) << 1 | |
| ((i >>> 4) & 1) << 2 | |
| ((i >>> 6) & 1) << 3 | |
| ((i >>> 8) & 1) << 4 | |
| ((i >>> 10) & 1) << 5 | |
| ((i >>> 12) & 1) << 6 | |
| ((i >>> 14) & 1) << 7 | |
| ((i >>> 16) & 1) << 8 | |
| ((i >>> 18) & 1) << 9 | |
| ((i >>> 20) & 1) << 10 | |
| ((i >>> 22) & 1) << 11 | |
| ((i >>> 24) & 1) << 12 | |
| ((i >>> 26) & 1) << 13 | |
| ((i >>> 28) & 1) << 14 | |
| ((i >>> 30) & 1) << 15; |
| int y = ((i >>> 1) & 1) << 0 | |
| ((i >>> 3) & 1) << 1 | |
| ((i >>> 5) & 1) << 2 | |
| ((i >>> 7) & 1) << 3 | |
| ((i >>> 9) & 1) << 4 | |
| ((i >>> 11) & 1) << 5 | |
| ((i >>> 13) & 1) << 6 | |
| ((i >>> 15) & 1) << 7 | |
| ((i >>> 17) & 1) << 8 | |
| ((i >>> 19) & 1) << 9 | |
| ((i >>> 21) & 1) << 10 | |
| ((i >>> 23) & 1) << 11 | |
| ((i >>> 25) & 1) << 12 | |
| ((i >>> 27) & 1) << 13 | |
| ((i >>> 29) & 1) << 14 | |
| ((i >>> 31) & 1) << 15; |
| try { |
| id.setPixel(x, y, pixData[i]); |
| if (x > maxX) { |
| maxX = x; |
| } |
| } catch (IllegalArgumentException ex) { |
| System.out.println("bad pixels: i " + i + |
| ", w " + id.width + |
| ", h " + id.height + |
| ", x " + x + |
| ", y " + y); |
| throw ex; |
| } |
| } |
| return maxX; |
| } |
| |
| private final static int HILBERT_DIR_N = 0; |
| private final static int HILBERT_DIR_S = 1; |
| private final static int HILBERT_DIR_E = 2; |
| private final static int HILBERT_DIR_W = 3; |
| |
| private void hilbertWalk(ImageData id, InputStream pixData, |
| int order, int x, int y, int dir) |
| throws IOException { |
| if (x >= id.width || y >= id.height) { |
| return; |
| } else if (order == 0) { |
| try { |
| int p = pixData.read(); |
| if (p >= 0) { |
| // flip along x=y axis; assume width == height |
| id.setPixel(y, x, p); |
| |
| /* Skanky; use an otherwise-unused ImageData field |
| * to keep track of the max x,y used. Note that x and y are inverted. |
| */ |
| if (y > id.x) { |
| id.x = y; |
| } |
| if (x > id.y) { |
| id.y = x; |
| } |
| } |
| //xxx just give up; don't bother walking the rest of the image |
| } catch (IllegalArgumentException ex) { |
| System.out.println("bad pixels: order " + order + |
| ", dir " + dir + |
| ", w " + id.width + |
| ", h " + id.height + |
| ", x " + x + |
| ", y " + y); |
| throw ex; |
| } |
| } else { |
| order--; |
| int delta = 1 << order; |
| int nextX = x + delta; |
| int nextY = y + delta; |
| |
| switch (dir) { |
| case HILBERT_DIR_E: |
| hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N); |
| hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E); |
| hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E); |
| hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S); |
| break; |
| case HILBERT_DIR_N: |
| hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E); |
| hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N); |
| hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N); |
| hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W); |
| break; |
| case HILBERT_DIR_S: |
| hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W); |
| hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S); |
| hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S); |
| hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E); |
| break; |
| case HILBERT_DIR_W: |
| hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S); |
| hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W); |
| hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W); |
| hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N); |
| break; |
| default: |
| throw new RuntimeException("Unexpected Hilbert direction " + |
| dir); |
| } |
| } |
| } |
| |
| private Point hilbertOrderData(ImageData id, byte pixData[]) { |
| |
| int order = 0; |
| for (int n = 1; n < id.width; n *= 2) { |
| order++; |
| } |
| /* Skanky; use an otherwise-unused ImageData field |
| * to keep track of maxX. |
| */ |
| Point p = new Point(0,0); |
| int oldIdX = id.x; |
| int oldIdY = id.y; |
| id.x = id.y = 0; |
| try { |
| hilbertWalk(id, new ByteArrayInputStream(pixData), |
| order, 0, 0, HILBERT_DIR_E); |
| p.x = id.x; |
| p.y = id.y; |
| } catch (IOException ex) { |
| System.err.println("Exception during hilbertWalk()"); |
| p.x = id.height; |
| p.y = id.width; |
| } |
| id.x = oldIdX; |
| id.y = oldIdY; |
| return p; |
| } |
| |
| private ImageData createHilbertHeapImage(byte pixData[]) { |
| int w, h; |
| |
| // Pick an image size that the largest of heaps will fit into. |
| w = (int)Math.sqrt((double)((16 * 1024 * 1024)/8)); |
| |
| // Space-filling curves require a power-of-2 width. |
| w = nextPow2(w); |
| h = w; |
| |
| // Create the heap image. |
| ImageData id = new ImageData(w, h, 8, mMapPalette); |
| |
| // Copy the data into the image |
| //int maxX = zOrderData(id, pixData); |
| Point maxP = hilbertOrderData(id, pixData); |
| |
| // update the max size to make it a round number once the zoom is applied |
| int factor = 100 / ZOOMS[mZoom.getSelectionIndex()]; |
| if (factor != 1) { |
| int tmp = maxP.x % factor; |
| if (tmp != 0) { |
| maxP.x += factor - tmp; |
| } |
| |
| tmp = maxP.y % factor; |
| if (tmp != 0) { |
| maxP.y += factor - tmp; |
| } |
| } |
| |
| if (maxP.y < id.height) { |
| // Crop the image down to the interesting part. |
| id = new ImageData(id.width, maxP.y, id.depth, id.palette, |
| id.scanlinePad, id.data); |
| } |
| |
| if (maxP.x < id.width) { |
| // crop the image again. A bit trickier this time. |
| ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette); |
| |
| int[] buffer = new int[maxP.x]; |
| for (int l = 0 ; l < id.height; l++) { |
| id.getPixels(0, l, maxP.x, buffer, 0); |
| croppedId.setPixels(0, l, maxP.x, buffer, 0); |
| } |
| |
| id = croppedId; |
| } |
| |
| // apply the zoom |
| if (factor != 1) { |
| id = id.scaledTo(id.width / factor, id.height / factor); |
| } |
| |
| return id; |
| } |
| |
| /** |
| * Convert the raw heap data to an image. We know we're running in |
| * the UI thread, so we can issue graphics commands directly. |
| * |
| * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html |
| * |
| * @param cd The client data |
| * @param mode The display mode. 0 = linear, 1 = hilbert. |
| * @param forceRedraw |
| */ |
| private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) { |
| Image image; |
| |
| byte[] pixData; |
| |
| // Atomically get and clear the heap data. |
| synchronized (cd) { |
| if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { |
| // no change, we return. |
| return; |
| } |
| |
| pixData = getSerializedData(); |
| } |
| |
| if (pixData != null) { |
| ImageData id; |
| if (mode == 1) { |
| id = createHilbertHeapImage(pixData); |
| } else { |
| id = createLinearHeapImage(pixData, 200, mMapPalette); |
| } |
| |
| image = new Image(mDisplay, id); |
| } else { |
| // Render a placeholder image. |
| int width, height; |
| if (mode == 1) { |
| width = height = PLACEHOLDER_HILBERT_SIZE; |
| } else { |
| width = PLACEHOLDER_LINEAR_H_SIZE; |
| height = PLACEHOLDER_LINEAR_V_SIZE; |
| } |
| image = new Image(mDisplay, width, height); |
| GC gc = new GC(image); |
| gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED)); |
| gc.drawLine(0, 0, width-1, height-1); |
| gc.dispose(); |
| gc = null; |
| } |
| |
| // set the new image |
| |
| if (mode == 1) { |
| if (mHilbertImage != null) { |
| mHilbertImage.dispose(); |
| } |
| |
| mHilbertImage = image; |
| mHilbertHeapImage.setImage(mHilbertImage); |
| mHilbertHeapImage.pack(true); |
| mHilbertBase.layout(); |
| mHilbertBase.pack(true); |
| } else { |
| if (mLinearImage != null) { |
| mLinearImage.dispose(); |
| } |
| |
| mLinearImage = image; |
| mLinearHeapImage.setImage(mLinearImage); |
| mLinearHeapImage.pack(true); |
| mLinearBase.layout(); |
| mLinearBase.pack(true); |
| } |
| } |
| |
| @Override |
| protected void setTableFocusListener() { |
| addTableToFocusListener(mHeapSummary); |
| } |
| } |
| |