aboutsummaryrefslogtreecommitdiffstats
path: root/ddms/libs
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2012-01-27 17:50:02 -0800
committerJeff Sharkey <jsharkey@android.com>2012-02-01 11:35:32 -0800
commit613f55001e7f39590d02227179f6f272ae8ab96f (patch)
treeed257d62a5a11b0194e254d9862bef8bf3fc4c31 /ddms/libs
parent98e9214b8b17ddcb49c6e9e0f4263ed7c987d311 (diff)
downloadsdk-613f55001e7f39590d02227179f6f272ae8ab96f.zip
sdk-613f55001e7f39590d02227179f6f272ae8ab96f.tar.gz
sdk-613f55001e7f39590d02227179f6f272ae8ab96f.tar.bz2
Show detailed network statistics from xt_qtaguid.
New panel that shows live network statistics by reading xt_qtaguid proc file across adb. Shows all tags for selected UID on a chart with 30 seconds of history. Each tag is presented as unique series, with the overall usage in grey. Table below the chart shows running totals of known tags. Includes options to pause, reset, and change polling frequency. Bug: 5194997 Change-Id: I72d43ab51aaf93ecfd6d45fcd452c7230cdee5b7
Diffstat (limited to 'ddms/libs')
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/net/NetworkPanel.java1053
1 files changed, 1053 insertions, 0 deletions
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/net/NetworkPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/net/NetworkPanel.java
new file mode 100644
index 0000000..9266f38
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/net/NetworkPanel.java
@@ -0,0 +1,1053 @@
+/*
+ * Copyright (C) 2012 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.net;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.TablePanel;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.RowLayout;
+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.Label;
+import org.eclipse.swt.widgets.Table;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.plot.DatasetRenderingOrder;
+import org.jfree.chart.plot.ValueMarker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2;
+import org.jfree.chart.renderer.xy.XYAreaRenderer;
+import org.jfree.data.DefaultKeyedValues2D;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimePeriod;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.AbstractIntervalXYDataset;
+import org.jfree.data.xy.TableXYDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.ui.RectangleAnchor;
+import org.jfree.ui.TextAnchor;
+
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.Iterator;
+
+/**
+ * Displays live network statistics for currently selected {@link Client}.
+ */
+public class NetworkPanel extends TablePanel {
+
+ // TODO: enable view of packets and bytes/packet
+ // TODO: add sash to resize chart and table
+ // TODO: let user edit tags to be meaningful
+ // TODO: hide panel when remote device is missing support
+ // TODO: add disposal listeners to clean up
+
+ /** Amount of historical data to display. */
+ private static final long HISTORY_MILLIS = 30 * 1000;
+
+ private final static String PREFS_NETWORK_COL_TITLE = "networkPanel.title";
+ private final static String PREFS_NETWORK_COL_RX_BYTES = "networkPanel.rxBytes";
+ private final static String PREFS_NETWORK_COL_RX_PACKETS = "networkPanel.rxPackets";
+ private final static String PREFS_NETWORK_COL_TX_BYTES = "networkPanel.txBytes";
+ private final static String PREFS_NETWORK_COL_TX_PACKETS = "networkPanel.txPackets";
+
+ /** Path to network statistics on remote device. */
+ private static final String PROC_XT_QTAGUID = "/proc/net/xt_qtaguid/stats";
+
+ private static final java.awt.Color TOTAL_COLOR = java.awt.Color.GRAY;
+
+ /** Colors used for tag series data. */
+ private static final java.awt.Color[] SERIES_COLORS = new java.awt.Color[] {
+ java.awt.Color.decode("0x2bc4c1"), // teal
+ java.awt.Color.decode("0xD50F25"), // red
+ java.awt.Color.decode("0x3369E8"), // blue
+ java.awt.Color.decode("0xEEB211"), // orange
+ java.awt.Color.decode("0x00bd2e"), // green
+ java.awt.Color.decode("0xae26ae"), // purple
+ };
+
+ private Display mDisplay;
+
+ private Composite mPanel;
+
+ /** Header panel with configuration options. */
+ private Composite mHeader;
+
+ private Label mSpeedLabel;
+ private Combo mSpeedCombo;
+ private long mSpeed;
+
+ private Button mPauseButton;
+ private Button mResetButton;
+
+ /** Chart of recent network activity. */
+ private JFreeChart mChart;
+ private ChartComposite mChartComposite;
+
+ private ValueAxis mDomainAxis;
+
+ /** Data for total traffic (tag 0x0). */
+ private TimeSeriesCollection mTotalCollection;
+ private TimeSeries mRxTotalSeries;
+ private TimeSeries mTxTotalSeries;
+
+ /** Data for detailed tagged traffic. */
+ private LiveTimeTableXYDataset mRxDetailDataset;
+ private LiveTimeTableXYDataset mTxDetailDataset;
+
+ private XYAreaRenderer mTotalRenderer;
+ private StackedXYAreaRenderer2 mRenderer;
+
+ /** Table showing summary of network activity. */
+ private Table mTable;
+ private TableViewer mTableViewer;
+
+ /** UID of currently selected {@link Client}. */
+ private int mActiveUid = -1;
+
+ /** List of traffic flows being actively tracked. */
+ private ArrayList<TrackedItem> mTrackedItems = new ArrayList<TrackedItem>();
+
+ /** Flag indicating that user has paused data collection. */
+ private volatile boolean mPaused = false;
+
+ private SampleThread mSampleThread;
+
+ private class SampleThread extends Thread {
+ @Override
+ public void run() {
+ while (!mPaused && mDisplay != null) {
+ performSample();
+
+ try {
+ Thread.sleep(mSpeed);
+ } catch (InterruptedException e) {
+ // ignored
+ }
+ }
+
+ mSampleThread = null;
+ }
+ }
+
+ /** Last snapshot taken by {@link #performSample()}. */
+ private NetworkSnapshot mLastSnapshot;
+
+ @Override
+ protected Control createControl(Composite parent) {
+ mDisplay = parent.getDisplay();
+
+ mPanel = new Composite(parent, SWT.NONE);
+
+ final FormLayout formLayout = new FormLayout();
+ mPanel.setLayout(formLayout);
+
+ createHeader();
+ createChart();
+ createTable();
+
+ return mPanel;
+ }
+
+ /**
+ * Create header panel with configuration options.
+ */
+ private void createHeader() {
+
+ mHeader = new Composite(mPanel, SWT.NONE);
+ final RowLayout layout = new RowLayout();
+ layout.center = true;
+ mHeader.setLayout(layout);
+
+ mSpeedLabel = new Label(mHeader, SWT.NONE);
+ mSpeedLabel.setText("Speed:");
+ mSpeedCombo = new Combo(mHeader, SWT.PUSH);
+ mSpeedCombo.add("Fast (100ms)");
+ mSpeedCombo.add("Medium (250ms)");
+ mSpeedCombo.add("Slow (500ms)");
+ mSpeedCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateSpeed();
+ }
+ });
+
+ mSpeedCombo.select(1);
+ updateSpeed();
+
+ mPauseButton = new Button(mHeader, SWT.PUSH);
+ mPauseButton.setText("Pause");
+ mPauseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mPaused = !mPaused;
+ updateRunning();
+ if (mPaused) {
+ mPauseButton.setText("Resume");
+ mHeader.pack();
+ } else {
+ mPauseButton.setText("Pause");
+ mHeader.pack();
+ }
+ }
+ });
+
+ mResetButton = new Button(mHeader, SWT.PUSH);
+ mResetButton.setText("Reset");
+ mResetButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ clearTrackedItems();
+ }
+ });
+
+ final FormData data = new FormData();
+ data.top = new FormAttachment(0);
+ data.left = new FormAttachment(0);
+ data.right = new FormAttachment(100);
+ mHeader.setLayoutData(data);
+ }
+
+ /**
+ * Create chart of recent network activity.
+ */
+ private void createChart() {
+
+ mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false);
+
+ // create backing datasets and series
+ mRxTotalSeries = new TimeSeries("RX total");
+ mTxTotalSeries = new TimeSeries("TX total");
+
+ mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
+ mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
+
+ mTotalCollection = new TimeSeriesCollection();
+ mTotalCollection.addSeries(mRxTotalSeries);
+ mTotalCollection.addSeries(mTxTotalSeries);
+
+ mRxDetailDataset = new LiveTimeTableXYDataset();
+ mTxDetailDataset = new LiveTimeTableXYDataset();
+
+ mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA);
+ mRenderer = new StackedXYAreaRenderer2();
+
+ final XYPlot xyPlot = mChart.getXYPlot();
+
+ xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
+
+ xyPlot.setDataset(0, mTotalCollection);
+ xyPlot.setDataset(1, mRxDetailDataset);
+ xyPlot.setDataset(2, mTxDetailDataset);
+ xyPlot.setRenderer(0, mTotalRenderer);
+ xyPlot.setRenderer(1, mRenderer);
+ xyPlot.setRenderer(2, mRenderer);
+
+ // we control domain axis manually when taking samples
+ mDomainAxis = xyPlot.getDomainAxis();
+ mDomainAxis.setAutoRange(false);
+
+ final NumberAxis axis = new NumberAxis();
+ axis.setNumberFormatOverride(new BytesFormat(true));
+ axis.setAutoRangeMinimumSize(50);
+ xyPlot.setRangeAxis(axis);
+ xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);
+
+ // draw thick line to separate RX versus TX traffic
+ xyPlot.addRangeMarker(
+ new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2)));
+
+ // label to indicate that positive axis is RX traffic
+ final ValueMarker rxMarker = new ValueMarker(0);
+ rxMarker.setStroke(new java.awt.BasicStroke(0));
+ rxMarker.setLabel("RX");
+ rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f));
+ rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
+ rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
+ rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT);
+ xyPlot.addRangeMarker(rxMarker);
+
+ // label to indicate that negative axis is TX traffic
+ final ValueMarker txMarker = new ValueMarker(0);
+ txMarker.setStroke(new java.awt.BasicStroke(0));
+ txMarker.setLabel("TX");
+ txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f));
+ txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
+ txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT);
+ txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
+ xyPlot.addRangeMarker(txMarker);
+
+ mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart,
+ ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true,
+ false, true);
+
+ final FormData data = new FormData();
+ data.top = new FormAttachment(mHeader);
+ data.left = new FormAttachment(0);
+ data.bottom = new FormAttachment(70);
+ data.right = new FormAttachment(100);
+ mChartComposite.setLayoutData(data);
+ }
+
+ /**
+ * Create table showing summary of network activity.
+ */
+ private void createTable() {
+ mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
+
+ final FormData data = new FormData();
+ data.top = new FormAttachment(mChartComposite);
+ data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER);
+ data.bottom = new FormAttachment(100);
+ mTable.setLayoutData(data);
+
+ mTable.setHeaderVisible(true);
+ mTable.setLinesVisible(true);
+
+ final IPreferenceStore store = null; //DdmUiPreferences.getStore();
+
+ TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null);
+ TableHelper.createTableColumn(
+ mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store);
+ TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_RX_BYTES, store);
+ TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_RX_PACKETS, store);
+ TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_TX_BYTES, store);
+ TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_TX_PACKETS, store);
+
+ mTableViewer = new TableViewer(mTable);
+ mTableViewer.setContentProvider(new ContentProvider());
+ mTableViewer.setLabelProvider(new LabelProvider());
+ }
+
+ /**
+ * Update {@link #mSpeed} to match {@link #mSpeedCombo} selection.
+ */
+ private void updateSpeed() {
+ switch (mSpeedCombo.getSelectionIndex()) {
+ case 0:
+ mSpeed = 100;
+ break;
+ case 1:
+ mSpeed = 250;
+ break;
+ case 2:
+ mSpeed = 500;
+ break;
+ }
+ }
+
+ @Override
+ public void setFocus() {
+ mPanel.setFocus();
+ }
+
+ private static java.awt.Color nextSeriesColor(int index) {
+ return SERIES_COLORS[index % SERIES_COLORS.length];
+ }
+
+ /**
+ * Find a {@link TrackedItem} that matches the requested UID and tag, or
+ * create one if none exists.
+ */
+ public TrackedItem findOrCreateTrackedItem(int uid, int tag) {
+ // try searching for existing item
+ for (TrackedItem item : mTrackedItems) {
+ if (item.uid == uid && item.tag == tag) {
+ return item;
+ }
+ }
+
+ // nothing found; create new item
+ final TrackedItem item = new TrackedItem(uid, tag);
+ if (item.isTotal()) {
+ item.color = TOTAL_COLOR;
+ item.label = "Total";
+ } else {
+ final int size = mTrackedItems.size();
+ item.color = nextSeriesColor(size);
+ item.label = "0x" + new Formatter().format("%08x", tag);
+ }
+
+ // create color chip to display as legend in table
+ item.colorImage = new Image(mDisplay, 20, 20);
+ final GC gc = new GC(item.colorImage);
+ gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color
+ .getRed(), item.color.getGreen(), item.color.getBlue()));
+ gc.fillRectangle(item.colorImage.getBounds());
+ gc.dispose();
+
+ mTrackedItems.add(item);
+ return item;
+ }
+
+ /**
+ * Clear all {@link TrackedItem} and chart history.
+ */
+ public void clearTrackedItems() {
+ mRxTotalSeries.clear();
+ mTxTotalSeries.clear();
+
+ mRxDetailDataset.clear();
+ mTxDetailDataset.clear();
+
+ mTrackedItems.clear();
+ mTableViewer.setInput(mTrackedItems);
+ }
+
+ /**
+ * Update the {@link #mRenderer} colors to match {@link TrackedItem#color}.
+ */
+ private void updateSeriesPaint() {
+ for (TrackedItem item : mTrackedItems) {
+ final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label);
+ if (seriesIndex >= 0) {
+ mRenderer.setSeriesPaint(seriesIndex, item.color);
+ mRenderer.setSeriesFillPaint(seriesIndex, item.color);
+ }
+ }
+
+ // series data is always the same color
+ final int count = mTotalCollection.getSeriesCount();
+ for (int i = 0; i < count; i++) {
+ mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR);
+ mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR);
+ }
+ }
+
+ /**
+ * Traffic flow being actively tracked, uniquely defined by UID and tag. Can
+ * record {@link NetworkSnapshot} deltas into {@link TimeSeries} for
+ * charting, and into summary statistics for {@link Table} display.
+ */
+ private class TrackedItem {
+ public final int uid;
+ public final int tag;
+
+ public java.awt.Color color;
+ public Image colorImage;
+
+ public String label;
+ public long rxBytes;
+ public long rxPackets;
+ public long txBytes;
+ public long txPackets;
+
+ public TrackedItem(int uid, int tag) {
+ this.uid = uid;
+ this.tag = tag;
+ }
+
+ public boolean isTotal() {
+ return tag == 0x0;
+ }
+
+ /**
+ * Record the given {@link NetworkSnapshot} delta, updating
+ * {@link TimeSeries} and summary statistics.
+ *
+ * @param time Timestamp when delta was observed.
+ * @param deltaMillis Time duration covered by delta, in milliseconds.
+ */
+ public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) {
+ final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis;
+ final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis;
+
+ // record values under correct series
+ if (isTotal()) {
+ mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond);
+ mTxTotalSeries.addOrUpdate(time, txBytesPerSecond);
+ } else {
+ mRxDetailDataset.addValue(rxBytesPerSecond, time, label);
+ mTxDetailDataset.addValue(-txBytesPerSecond, time, label);
+ }
+
+ rxBytes += delta.rxBytes;
+ rxPackets += delta.rxPackets;
+ txBytes += delta.txBytes;
+ txPackets += delta.txPackets;
+ }
+ }
+
+ @Override
+ public void deviceSelected() {
+ // ignored
+ }
+
+ /**
+ * Start {@link SampleThread} if not already running, and user hasn't paused
+ * playback.
+ */
+ public synchronized void updateRunning() {
+ if (!mPaused && mSampleThread == null) {
+ mSampleThread = new SampleThread();
+ mSampleThread.start();
+ }
+ }
+
+ @Override
+ public void clientSelected() {
+ final Client client = getCurrentClient();
+ final int pid = client.getClientData().getPid();
+
+ try {
+ // map PID to UID from device
+ final UidParser uidParser = new UidParser();
+ getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser);
+ mActiveUid = uidParser.uid;
+ } catch (TimeoutException e) {
+ e.printStackTrace();
+ } catch (AdbCommandRejectedException e) {
+ e.printStackTrace();
+ } catch (ShellCommandUnresponsiveException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ clearTrackedItems();
+ updateRunning();
+ }
+
+ @Override
+ public void clientChanged(Client client, int changeMask) {
+ // ignored
+ }
+
+ /**
+ * Take a snapshot from {@link #getCurrentDevice()}, recording any delta
+ * network traffic to {@link TrackedItem}.
+ */
+ public void performSample() {
+ try {
+ final NetworkSnapshot snapshot = new NetworkSnapshot(System.currentTimeMillis());
+ getCurrentDevice().executeShellCommand(
+ "cat " + PROC_XT_QTAGUID, new NetworkSnapshotParser(snapshot));
+
+ // use first snapshot as baseline
+ if (mLastSnapshot == null) {
+ mLastSnapshot = snapshot;
+ return;
+ }
+
+ final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot);
+ mLastSnapshot = snapshot;
+
+ // perform delta updates over on UI thread
+ if (mDisplay != null) {
+ mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp));
+ }
+
+ } catch (TimeoutException e) {
+ e.printStackTrace();
+ } catch (AdbCommandRejectedException e) {
+ e.printStackTrace();
+ } catch (ShellCommandUnresponsiveException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Task that updates UI with given {@link NetworkSnapshot} delta.
+ */
+ private class UpdateDeltaRunnable implements Runnable {
+ private final NetworkSnapshot mDelta;
+ private final long mEndTime;
+
+ public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) {
+ mDelta = delta;
+ mEndTime = endTime;
+ }
+
+ @Override
+ public void run() {
+ final Millisecond time = new Millisecond();
+ for (NetworkSnapshot.Entry entry : mDelta) {
+ if (mActiveUid != entry.uid) continue;
+
+ final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag);
+ item.recordDelta(time, mDelta.timestamp, entry);
+ }
+
+ // remove any historical detail data
+ final long beforeMillis = mEndTime - HISTORY_MILLIS;
+ mRxDetailDataset.removeBefore(beforeMillis);
+ mTxDetailDataset.removeBefore(beforeMillis);
+
+ // trigger refresh from bulk changes above
+ mRxDetailDataset.fireDatasetChanged();
+ mTxDetailDataset.fireDatasetChanged();
+
+ // update axis to show latest 30 second time period
+ mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime);
+
+ updateSeriesPaint();
+
+ // kick table viewer to update
+ mTableViewer.setInput(mTrackedItems);
+ }
+ }
+
+ /**
+ * Parser that extracts UID from remote {@code /proc/pid/status} file.
+ */
+ private static class UidParser extends MultiLineReceiver {
+ public int uid = -1;
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ if (line.startsWith("Uid:")) {
+ // we care about the "real" UID
+ final String[] cols = line.split("\t");
+ uid = Integer.parseInt(cols[1]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parser that populates {@link NetworkSnapshot} based on contents of remote
+ * {@link NetworkPanel#PROC_XT_QTAGUID} file.
+ */
+ private static class NetworkSnapshotParser extends MultiLineReceiver {
+ private final NetworkSnapshot mSnapshot;
+
+ public NetworkSnapshotParser(NetworkSnapshot snapshot) {
+ mSnapshot = snapshot;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ // ignore header line
+ if (line.startsWith("idx")) {
+ continue;
+ }
+
+ final String[] cols = line.split(" ");
+ if (cols.length < 9) continue;
+
+ // iface and set are currently ignored, which groups those
+ // entries together.
+ final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry();
+ entry.iface = null; //cols[1];
+ entry.uid = Integer.parseInt(cols[3]);
+ entry.set = -1; //Integer.parseInt(cols[4]);
+ entry.tag = (int) (Long.decode(cols[2]) >> 32);
+ entry.rxBytes = Long.parseLong(cols[5]);
+ entry.rxPackets = Long.parseLong(cols[6]);
+ entry.txBytes = Long.parseLong(cols[7]);
+ entry.txPackets = Long.parseLong(cols[8]);
+
+ mSnapshot.combine(entry);
+ }
+ }
+ }
+
+ /**
+ * Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time.
+ */
+ private static class NetworkSnapshot implements Iterable<NetworkSnapshot.Entry> {
+ private ArrayList<Entry> mStats = new ArrayList<Entry>();
+
+ public final long timestamp;
+
+ /** Single parsed statistics row. */
+ public static class Entry {
+ public String iface;
+ public int uid;
+ public int set;
+ public int tag;
+ public long rxBytes;
+ public long rxPackets;
+ public long txBytes;
+ public long txPackets;
+
+ public boolean isEmpty() {
+ return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0;
+ }
+ }
+
+ public NetworkSnapshot(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public void clear() {
+ mStats.clear();
+ }
+
+ /**
+ * Combine the given {@link Entry} with any existing {@link Entry}, or
+ * insert if none exists.
+ */
+ public void combine(Entry entry) {
+ final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag);
+ if (existing != null) {
+ existing.rxBytes += entry.rxBytes;
+ existing.rxPackets += entry.rxPackets;
+ existing.txBytes += entry.txBytes;
+ existing.txPackets += entry.txPackets;
+ } else {
+ mStats.add(entry);
+ }
+ }
+
+ @Override
+ public Iterator<Entry> iterator() {
+ return mStats.iterator();
+ }
+
+ public Entry findEntry(String iface, int uid, int set, int tag) {
+ for (Entry entry : mStats) {
+ if (entry.uid == uid && entry.set == set && entry.tag == tag
+ && equal(entry.iface, iface)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Subtract the two given {@link NetworkSnapshot} objects, returning the
+ * delta between them.
+ */
+ public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) {
+ final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp);
+
+ // for each row on left, subtract value from right side
+ for (Entry leftEntry : left) {
+ final Entry rightEntry = right.findEntry(
+ leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag);
+ if (rightEntry == null) continue;
+
+ final Entry resultEntry = new Entry();
+ resultEntry.iface = leftEntry.iface;
+ resultEntry.uid = leftEntry.uid;
+ resultEntry.set = leftEntry.set;
+ resultEntry.tag = leftEntry.tag;
+ resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes;
+ resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets;
+ resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes;
+ resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets;
+
+ result.combine(resultEntry);
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Provider of {@link #mTrackedItems}.
+ */
+ private class ContentProvider implements IStructuredContentProvider {
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return mTrackedItems.toArray();
+ }
+ }
+
+ /**
+ * Provider of labels for {@Link TrackedItem} values.
+ */
+ private static class LabelProvider implements ITableLabelProvider {
+ private final DecimalFormat mFormat = new DecimalFormat("#,###");
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (element instanceof TrackedItem) {
+ final TrackedItem item = (TrackedItem) element;
+ switch (columnIndex) {
+ case 0:
+ return item.colorImage;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof TrackedItem) {
+ final TrackedItem item = (TrackedItem) element;
+ switch (columnIndex) {
+ case 0:
+ return null;
+ case 1:
+ return item.label;
+ case 2:
+ return mFormat.format(item.rxBytes);
+ case 3:
+ return mFormat.format(item.rxPackets);
+ case 4:
+ return mFormat.format(item.txBytes);
+ case 5:
+ return mFormat.format(item.txPackets);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Format that displays simplified byte units for when given values are
+ * large enough.
+ */
+ private static class BytesFormat extends NumberFormat {
+ private final String[] mUnits;
+ private final DecimalFormat mFormat = new DecimalFormat("#.#");
+
+ public BytesFormat(boolean perSecond) {
+ if (perSecond) {
+ mUnits = new String[] { "B/s", "KB/s", "MB/s" };
+ } else {
+ mUnits = new String[] { "B", "KB", "MB" };
+ }
+ }
+
+ @Override
+ public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
+ double value = Math.abs(number);
+
+ int i = 0;
+ while (value > 1024 && i < mUnits.length - 1) {
+ value /= 1024;
+ i++;
+ }
+
+ toAppendTo.append(mFormat.format(value));
+ toAppendTo.append(mUnits[i]);
+
+ return toAppendTo;
+ }
+
+ @Override
+ public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+ return format((long) number, toAppendTo, pos);
+ }
+
+ @Override
+ public Number parse(String source, ParsePosition parsePosition) {
+ return null;
+ }
+ }
+
+ public static boolean equal(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ /**
+ * Build stub string of requested length, usually for measurement.
+ */
+ private static String buildSampleText(int length) {
+ final StringBuilder builder = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ builder.append("X");
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Dataset that contains live measurements. Exposes
+ * {@link #removeBefore(long)} to efficiently remove old data, and enables
+ * batched {@link #fireDatasetChanged()} events.
+ */
+ public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements
+ TableXYDataset {
+ private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true);
+
+ /**
+ * Caller is responsible for triggering {@link #fireDatasetChanged()}.
+ */
+ public void addValue(Number value, TimePeriod rowKey, String columnKey) {
+ mValues.addValue(value, rowKey, columnKey);
+ }
+
+ /**
+ * Caller is responsible for triggering {@link #fireDatasetChanged()}.
+ */
+ public void removeBefore(long beforeMillis) {
+ while(mValues.getRowCount() > 0) {
+ final TimePeriod period = (TimePeriod) mValues.getRowKey(0);
+ if (period.getEnd().getTime() < beforeMillis) {
+ mValues.removeRow(0);
+ } else {
+ break;
+ }
+ }
+ }
+
+ public int getColumnIndex(String key) {
+ return mValues.getColumnIndex(key);
+ }
+
+ public void clear() {
+ mValues.clear();
+ fireDatasetChanged();
+ }
+
+ @Override
+ public void fireDatasetChanged() {
+ super.fireDatasetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mValues.getRowCount();
+ }
+
+ @Override
+ public int getItemCount(int series) {
+ return mValues.getRowCount();
+ }
+
+ @Override
+ public int getSeriesCount() {
+ return mValues.getColumnCount();
+ }
+
+ @Override
+ public Comparable getSeriesKey(int series) {
+ return mValues.getColumnKey(series);
+ }
+
+ @Override
+ public double getXValue(int series, int item) {
+ final TimePeriod period = (TimePeriod) mValues.getRowKey(item);
+ return period.getStart().getTime();
+ }
+
+ @Override
+ public double getStartXValue(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public double getEndXValue(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getX(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getStartX(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getEndX(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getY(int series, int item) {
+ return mValues.getValue(item, series);
+ }
+
+ @Override
+ public Number getStartY(int series, int item) {
+ return getY(series, item);
+ }
+
+ @Override
+ public Number getEndY(int series, int item) {
+ return getY(series, item);
+ }
+ }
+}