Home | History | Annotate | Download | only in net
      1 /*
      2  * Copyright (C) 2012 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.net;
     18 
     19 import com.android.ddmlib.AdbCommandRejectedException;
     20 import com.android.ddmlib.Client;
     21 import com.android.ddmlib.IDevice;
     22 import com.android.ddmlib.MultiLineReceiver;
     23 import com.android.ddmlib.ShellCommandUnresponsiveException;
     24 import com.android.ddmlib.TimeoutException;
     25 import com.android.ddmuilib.DdmUiPreferences;
     26 import com.android.ddmuilib.TableHelper;
     27 import com.android.ddmuilib.TablePanel;
     28 
     29 import org.eclipse.core.runtime.IStatus;
     30 import org.eclipse.core.runtime.Status;
     31 import org.eclipse.jface.dialogs.ErrorDialog;
     32 import org.eclipse.jface.preference.IPreferenceStore;
     33 import org.eclipse.jface.viewers.ILabelProviderListener;
     34 import org.eclipse.jface.viewers.IStructuredContentProvider;
     35 import org.eclipse.jface.viewers.ITableLabelProvider;
     36 import org.eclipse.jface.viewers.TableViewer;
     37 import org.eclipse.jface.viewers.Viewer;
     38 import org.eclipse.swt.SWT;
     39 import org.eclipse.swt.events.SelectionAdapter;
     40 import org.eclipse.swt.events.SelectionEvent;
     41 import org.eclipse.swt.graphics.GC;
     42 import org.eclipse.swt.graphics.Image;
     43 import org.eclipse.swt.layout.FormAttachment;
     44 import org.eclipse.swt.layout.FormData;
     45 import org.eclipse.swt.layout.FormLayout;
     46 import org.eclipse.swt.layout.RowLayout;
     47 import org.eclipse.swt.widgets.Button;
     48 import org.eclipse.swt.widgets.Combo;
     49 import org.eclipse.swt.widgets.Composite;
     50 import org.eclipse.swt.widgets.Control;
     51 import org.eclipse.swt.widgets.Display;
     52 import org.eclipse.swt.widgets.Label;
     53 import org.eclipse.swt.widgets.Table;
     54 import org.jfree.chart.ChartFactory;
     55 import org.jfree.chart.JFreeChart;
     56 import org.jfree.chart.axis.AxisLocation;
     57 import org.jfree.chart.axis.NumberAxis;
     58 import org.jfree.chart.axis.ValueAxis;
     59 import org.jfree.chart.plot.DatasetRenderingOrder;
     60 import org.jfree.chart.plot.ValueMarker;
     61 import org.jfree.chart.plot.XYPlot;
     62 import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2;
     63 import org.jfree.chart.renderer.xy.XYAreaRenderer;
     64 import org.jfree.data.DefaultKeyedValues2D;
     65 import org.jfree.data.time.Millisecond;
     66 import org.jfree.data.time.TimePeriod;
     67 import org.jfree.data.time.TimeSeries;
     68 import org.jfree.data.time.TimeSeriesCollection;
     69 import org.jfree.data.xy.AbstractIntervalXYDataset;
     70 import org.jfree.data.xy.TableXYDataset;
     71 import org.jfree.experimental.chart.swt.ChartComposite;
     72 import org.jfree.ui.RectangleAnchor;
     73 import org.jfree.ui.TextAnchor;
     74 
     75 import java.io.IOException;
     76 import java.text.DecimalFormat;
     77 import java.text.FieldPosition;
     78 import java.text.NumberFormat;
     79 import java.text.ParsePosition;
     80 import java.util.ArrayList;
     81 import java.util.Date;
     82 import java.util.Formatter;
     83 import java.util.Iterator;
     84 
     85 /**
     86  * Displays live network statistics for currently selected {@link Client}.
     87  */
     88 public class NetworkPanel extends TablePanel {
     89 
     90     // TODO: enable view of packets and bytes/packet
     91     // TODO: add sash to resize chart and table
     92     // TODO: let user edit tags to be meaningful
     93 
     94     /** Amount of historical data to display. */
     95     private static final long HISTORY_MILLIS = 30 * 1000;
     96 
     97     private final static String PREFS_NETWORK_COL_TITLE = "networkPanel.title";
     98     private final static String PREFS_NETWORK_COL_RX_BYTES = "networkPanel.rxBytes";
     99     private final static String PREFS_NETWORK_COL_RX_PACKETS = "networkPanel.rxPackets";
    100     private final static String PREFS_NETWORK_COL_TX_BYTES = "networkPanel.txBytes";
    101     private final static String PREFS_NETWORK_COL_TX_PACKETS = "networkPanel.txPackets";
    102 
    103     /** Path to network statistics on remote device. */
    104     private static final String PROC_XT_QTAGUID = "/proc/net/xt_qtaguid/stats";
    105 
    106     private static final java.awt.Color TOTAL_COLOR = java.awt.Color.GRAY;
    107 
    108     /** Colors used for tag series data. */
    109     private static final java.awt.Color[] SERIES_COLORS = new java.awt.Color[] {
    110         java.awt.Color.decode("0x2bc4c1"), // teal
    111         java.awt.Color.decode("0xD50F25"), // red
    112         java.awt.Color.decode("0x3369E8"), // blue
    113         java.awt.Color.decode("0xEEB211"), // orange
    114         java.awt.Color.decode("0x00bd2e"), // green
    115         java.awt.Color.decode("0xae26ae"), // purple
    116     };
    117 
    118     private Display mDisplay;
    119 
    120     private Composite mPanel;
    121 
    122     /** Header panel with configuration options. */
    123     private Composite mHeader;
    124 
    125     private Label mSpeedLabel;
    126     private Combo mSpeedCombo;
    127 
    128     /** Current sleep between each sample, from {@link #mSpeedCombo}. */
    129     private long mSpeedMillis;
    130 
    131     private Button mRunningButton;
    132     private Button mResetButton;
    133 
    134     /** Chart of recent network activity. */
    135     private JFreeChart mChart;
    136     private ChartComposite mChartComposite;
    137 
    138     private ValueAxis mDomainAxis;
    139 
    140     /** Data for total traffic (tag 0x0).  */
    141     private TimeSeriesCollection mTotalCollection;
    142     private TimeSeries mRxTotalSeries;
    143     private TimeSeries mTxTotalSeries;
    144 
    145     /** Data for detailed tagged traffic. */
    146     private LiveTimeTableXYDataset mRxDetailDataset;
    147     private LiveTimeTableXYDataset mTxDetailDataset;
    148 
    149     private XYAreaRenderer mTotalRenderer;
    150     private StackedXYAreaRenderer2 mRenderer;
    151 
    152     /** Table showing summary of network activity. */
    153     private Table mTable;
    154     private TableViewer mTableViewer;
    155 
    156     /** UID of currently selected {@link Client}. */
    157     private int mActiveUid = -1;
    158 
    159     /** List of traffic flows being actively tracked. */
    160     private ArrayList<TrackedItem> mTrackedItems = new ArrayList<TrackedItem>();
    161 
    162     private SampleThread mSampleThread;
    163 
    164     private class SampleThread extends Thread {
    165         private volatile boolean mFinish;
    166 
    167         public void finish() {
    168             mFinish = true;
    169             interrupt();
    170         }
    171 
    172         @Override
    173         public void run() {
    174             while (!mFinish && !mDisplay.isDisposed()) {
    175                 performSample();
    176 
    177                 try {
    178                     Thread.sleep(mSpeedMillis);
    179                 } catch (InterruptedException e) {
    180                     // ignored
    181                 }
    182             }
    183         }
    184     }
    185 
    186     /** Last snapshot taken by {@link #performSample()}. */
    187     private NetworkSnapshot mLastSnapshot;
    188 
    189     @Override
    190     protected Control createControl(Composite parent) {
    191         mDisplay = parent.getDisplay();
    192 
    193         mPanel = new Composite(parent, SWT.NONE);
    194 
    195         final FormLayout formLayout = new FormLayout();
    196         mPanel.setLayout(formLayout);
    197 
    198         createHeader();
    199         createChart();
    200         createTable();
    201 
    202         return mPanel;
    203     }
    204 
    205     /**
    206      * Create header panel with configuration options.
    207      */
    208     private void createHeader() {
    209 
    210         mHeader = new Composite(mPanel, SWT.NONE);
    211         final RowLayout layout = new RowLayout();
    212         layout.center = true;
    213         mHeader.setLayout(layout);
    214 
    215         mSpeedLabel = new Label(mHeader, SWT.NONE);
    216         mSpeedLabel.setText("Speed:");
    217         mSpeedCombo = new Combo(mHeader, SWT.PUSH);
    218         mSpeedCombo.add("Fast (100ms)");
    219         mSpeedCombo.add("Medium (250ms)");
    220         mSpeedCombo.add("Slow (500ms)");
    221         mSpeedCombo.addSelectionListener(new SelectionAdapter() {
    222             @Override
    223             public void widgetSelected(SelectionEvent e) {
    224                 updateSpeed();
    225             }
    226         });
    227 
    228         mSpeedCombo.select(1);
    229         updateSpeed();
    230 
    231         mRunningButton = new Button(mHeader, SWT.PUSH);
    232         mRunningButton.setText("Start");
    233         mRunningButton.setEnabled(false);
    234         mRunningButton.addSelectionListener(new SelectionAdapter() {
    235             @Override
    236             public void widgetSelected(SelectionEvent e) {
    237                 final boolean alreadyRunning = mSampleThread != null;
    238                 updateRunning(!alreadyRunning);
    239             }
    240         });
    241 
    242         mResetButton = new Button(mHeader, SWT.PUSH);
    243         mResetButton.setText("Reset");
    244         mResetButton.addSelectionListener(new SelectionAdapter() {
    245             @Override
    246             public void widgetSelected(SelectionEvent e) {
    247                 clearTrackedItems();
    248             }
    249         });
    250 
    251         final FormData data = new FormData();
    252         data.top = new FormAttachment(0);
    253         data.left = new FormAttachment(0);
    254         data.right = new FormAttachment(100);
    255         mHeader.setLayoutData(data);
    256     }
    257 
    258     /**
    259      * Create chart of recent network activity.
    260      */
    261     private void createChart() {
    262 
    263         mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false);
    264 
    265         // create backing datasets and series
    266         mRxTotalSeries = new TimeSeries("RX total");
    267         mTxTotalSeries = new TimeSeries("TX total");
    268 
    269         mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
    270         mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
    271 
    272         mTotalCollection = new TimeSeriesCollection();
    273         mTotalCollection.addSeries(mRxTotalSeries);
    274         mTotalCollection.addSeries(mTxTotalSeries);
    275 
    276         mRxDetailDataset = new LiveTimeTableXYDataset();
    277         mTxDetailDataset = new LiveTimeTableXYDataset();
    278 
    279         mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA);
    280         mRenderer = new StackedXYAreaRenderer2();
    281 
    282         final XYPlot xyPlot = mChart.getXYPlot();
    283 
    284         xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
    285 
    286         xyPlot.setDataset(0, mTotalCollection);
    287         xyPlot.setDataset(1, mRxDetailDataset);
    288         xyPlot.setDataset(2, mTxDetailDataset);
    289         xyPlot.setRenderer(0, mTotalRenderer);
    290         xyPlot.setRenderer(1, mRenderer);
    291         xyPlot.setRenderer(2, mRenderer);
    292 
    293         // we control domain axis manually when taking samples
    294         mDomainAxis = xyPlot.getDomainAxis();
    295         mDomainAxis.setAutoRange(false);
    296 
    297         final NumberAxis axis = new NumberAxis();
    298         axis.setNumberFormatOverride(new BytesFormat(true));
    299         axis.setAutoRangeMinimumSize(50);
    300         xyPlot.setRangeAxis(axis);
    301         xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);
    302 
    303         // draw thick line to separate RX versus TX traffic
    304         xyPlot.addRangeMarker(
    305                 new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2)));
    306 
    307         // label to indicate that positive axis is RX traffic
    308         final ValueMarker rxMarker = new ValueMarker(0);
    309         rxMarker.setStroke(new java.awt.BasicStroke(0));
    310         rxMarker.setLabel("RX");
    311         rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f));
    312         rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
    313         rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
    314         rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT);
    315         xyPlot.addRangeMarker(rxMarker);
    316 
    317         // label to indicate that negative axis is TX traffic
    318         final ValueMarker txMarker = new ValueMarker(0);
    319         txMarker.setStroke(new java.awt.BasicStroke(0));
    320         txMarker.setLabel("TX");
    321         txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f));
    322         txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
    323         txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT);
    324         txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
    325         xyPlot.addRangeMarker(txMarker);
    326 
    327         mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart,
    328                 ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT,
    329                 ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
    330                 ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true,
    331                 false, true);
    332 
    333         final FormData data = new FormData();
    334         data.top = new FormAttachment(mHeader);
    335         data.left = new FormAttachment(0);
    336         data.bottom = new FormAttachment(70);
    337         data.right = new FormAttachment(100);
    338         mChartComposite.setLayoutData(data);
    339     }
    340 
    341     /**
    342      * Create table showing summary of network activity.
    343      */
    344     private void createTable() {
    345         mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
    346 
    347         final FormData data = new FormData();
    348         data.top = new FormAttachment(mChartComposite);
    349         data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER);
    350         data.bottom = new FormAttachment(100);
    351         mTable.setLayoutData(data);
    352 
    353         mTable.setHeaderVisible(true);
    354         mTable.setLinesVisible(true);
    355 
    356         final IPreferenceStore store = DdmUiPreferences.getStore();
    357 
    358         TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null);
    359         TableHelper.createTableColumn(
    360                 mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store);
    361         TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12),
    362                 PREFS_NETWORK_COL_RX_BYTES, store);
    363         TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12),
    364                 PREFS_NETWORK_COL_RX_PACKETS, store);
    365         TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12),
    366                 PREFS_NETWORK_COL_TX_BYTES, store);
    367         TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12),
    368                 PREFS_NETWORK_COL_TX_PACKETS, store);
    369 
    370         mTableViewer = new TableViewer(mTable);
    371         mTableViewer.setContentProvider(new ContentProvider());
    372         mTableViewer.setLabelProvider(new LabelProvider());
    373     }
    374 
    375     /**
    376      * Update {@link #mSpeedMillis} to match {@link #mSpeedCombo} selection.
    377      */
    378     private void updateSpeed() {
    379         switch (mSpeedCombo.getSelectionIndex()) {
    380             case 0:
    381                 mSpeedMillis = 100;
    382                 break;
    383             case 1:
    384                 mSpeedMillis = 250;
    385                 break;
    386             case 2:
    387                 mSpeedMillis = 500;
    388                 break;
    389         }
    390     }
    391 
    392     /**
    393      * Update if {@link SampleThread} should be actively running. Will create
    394      * new thread or finish existing thread to match requested state.
    395      */
    396     private void updateRunning(boolean shouldRun) {
    397         final boolean alreadyRunning = mSampleThread != null;
    398         if (alreadyRunning && !shouldRun) {
    399             mSampleThread.finish();
    400             mSampleThread = null;
    401 
    402             mRunningButton.setText("Start");
    403             mHeader.pack();
    404         } else if (!alreadyRunning && shouldRun) {
    405             mSampleThread = new SampleThread();
    406             mSampleThread.start();
    407 
    408             mRunningButton.setText("Stop");
    409             mHeader.pack();
    410         }
    411     }
    412 
    413     @Override
    414     public void setFocus() {
    415         mPanel.setFocus();
    416     }
    417 
    418     private static java.awt.Color nextSeriesColor(int index) {
    419         return SERIES_COLORS[index % SERIES_COLORS.length];
    420     }
    421 
    422     /**
    423      * Find a {@link TrackedItem} that matches the requested UID and tag, or
    424      * create one if none exists.
    425      */
    426     public TrackedItem findOrCreateTrackedItem(int uid, int tag) {
    427         // try searching for existing item
    428         for (TrackedItem item : mTrackedItems) {
    429             if (item.uid == uid && item.tag == tag) {
    430                 return item;
    431             }
    432         }
    433 
    434         // nothing found; create new item
    435         final TrackedItem item = new TrackedItem(uid, tag);
    436         if (item.isTotal()) {
    437             item.color = TOTAL_COLOR;
    438             item.label = "Total";
    439         } else {
    440             final int size = mTrackedItems.size();
    441             item.color = nextSeriesColor(size);
    442             item.label = "0x" + new Formatter().format("%08x", tag);
    443         }
    444 
    445         // create color chip to display as legend in table
    446         item.colorImage = new Image(mDisplay, 20, 20);
    447         final GC gc = new GC(item.colorImage);
    448         gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color
    449                 .getRed(), item.color.getGreen(), item.color.getBlue()));
    450         gc.fillRectangle(item.colorImage.getBounds());
    451         gc.dispose();
    452 
    453         mTrackedItems.add(item);
    454         return item;
    455     }
    456 
    457     /**
    458      * Clear all {@link TrackedItem} and chart history.
    459      */
    460     public void clearTrackedItems() {
    461         mRxTotalSeries.clear();
    462         mTxTotalSeries.clear();
    463 
    464         mRxDetailDataset.clear();
    465         mTxDetailDataset.clear();
    466 
    467         mTrackedItems.clear();
    468         mTableViewer.setInput(mTrackedItems);
    469     }
    470 
    471     /**
    472      * Update the {@link #mRenderer} colors to match {@link TrackedItem#color}.
    473      */
    474     private void updateSeriesPaint() {
    475         for (TrackedItem item : mTrackedItems) {
    476             final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label);
    477             if (seriesIndex >= 0) {
    478                 mRenderer.setSeriesPaint(seriesIndex, item.color);
    479                 mRenderer.setSeriesFillPaint(seriesIndex, item.color);
    480             }
    481         }
    482 
    483         // series data is always the same color
    484         final int count = mTotalCollection.getSeriesCount();
    485         for (int i = 0; i < count; i++) {
    486             mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR);
    487             mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR);
    488         }
    489     }
    490 
    491     /**
    492      * Traffic flow being actively tracked, uniquely defined by UID and tag. Can
    493      * record {@link NetworkSnapshot} deltas into {@link TimeSeries} for
    494      * charting, and into summary statistics for {@link Table} display.
    495      */
    496     private class TrackedItem {
    497         public final int uid;
    498         public final int tag;
    499 
    500         public java.awt.Color color;
    501         public Image colorImage;
    502 
    503         public String label;
    504         public long rxBytes;
    505         public long rxPackets;
    506         public long txBytes;
    507         public long txPackets;
    508 
    509         public TrackedItem(int uid, int tag) {
    510             this.uid = uid;
    511             this.tag = tag;
    512         }
    513 
    514         public boolean isTotal() {
    515             return tag == 0x0;
    516         }
    517 
    518         /**
    519          * Record the given {@link NetworkSnapshot} delta, updating
    520          * {@link TimeSeries} and summary statistics.
    521          *
    522          * @param time Timestamp when delta was observed.
    523          * @param deltaMillis Time duration covered by delta, in milliseconds.
    524          */
    525         public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) {
    526             final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis;
    527             final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis;
    528 
    529             // record values under correct series
    530             if (isTotal()) {
    531                 mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond);
    532                 mTxTotalSeries.addOrUpdate(time, -txBytesPerSecond);
    533             } else {
    534                 mRxDetailDataset.addValue(rxBytesPerSecond, time, label);
    535                 mTxDetailDataset.addValue(-txBytesPerSecond, time, label);
    536             }
    537 
    538             rxBytes += delta.rxBytes;
    539             rxPackets += delta.rxPackets;
    540             txBytes += delta.txBytes;
    541             txPackets += delta.txPackets;
    542         }
    543     }
    544 
    545     @Override
    546     public void deviceSelected() {
    547         // treat as client selection to update enabled states
    548         clientSelected();
    549     }
    550 
    551     @Override
    552     public void clientSelected() {
    553         mActiveUid = -1;
    554 
    555         final Client client = getCurrentClient();
    556         if (client != null) {
    557             final int pid = client.getClientData().getPid();
    558             try {
    559                 // map PID to UID from device
    560                 final UidParser uidParser = new UidParser();
    561                 getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser);
    562                 mActiveUid = uidParser.uid;
    563             } catch (TimeoutException e) {
    564                 e.printStackTrace();
    565             } catch (AdbCommandRejectedException e) {
    566                 e.printStackTrace();
    567             } catch (ShellCommandUnresponsiveException e) {
    568                 e.printStackTrace();
    569             } catch (IOException e) {
    570                 e.printStackTrace();
    571             }
    572         }
    573 
    574         clearTrackedItems();
    575         updateRunning(false);
    576 
    577         final boolean validUid = mActiveUid != -1;
    578         mRunningButton.setEnabled(validUid);
    579     }
    580 
    581     @Override
    582     public void clientChanged(Client client, int changeMask) {
    583         // ignored
    584     }
    585 
    586     /**
    587      * Take a snapshot from {@link #getCurrentDevice()}, recording any delta
    588      * network traffic to {@link TrackedItem}.
    589      */
    590     public void performSample() {
    591         final IDevice device = getCurrentDevice();
    592         if (device == null) return;
    593 
    594         try {
    595             final NetworkSnapshotParser parser = new NetworkSnapshotParser();
    596             device.executeShellCommand("cat " + PROC_XT_QTAGUID, parser);
    597 
    598             if (parser.isError()) {
    599                 mDisplay.asyncExec(new Runnable() {
    600                     @Override
    601                     public void run() {
    602                         updateRunning(false);
    603 
    604                         final String title = "Problem reading stats";
    605                         final String message = "Problem reading xt_qtaguid network "
    606                                 + "statistics from selected device.";
    607                         Status status = new Status(IStatus.ERROR, "NetworkPanel", 0, message, null);
    608                         ErrorDialog.openError(mPanel.getShell(), title, title, status);
    609                     }
    610                 });
    611 
    612                 return;
    613             }
    614 
    615             final NetworkSnapshot snapshot = parser.getParsedSnapshot();
    616 
    617             // use first snapshot as baseline
    618             if (mLastSnapshot == null) {
    619                 mLastSnapshot = snapshot;
    620                 return;
    621             }
    622 
    623             final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot);
    624             mLastSnapshot = snapshot;
    625 
    626             // perform delta updates over on UI thread
    627             if (!mDisplay.isDisposed()) {
    628                 mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp));
    629             }
    630 
    631         } catch (TimeoutException e) {
    632             e.printStackTrace();
    633         } catch (AdbCommandRejectedException e) {
    634             e.printStackTrace();
    635         } catch (ShellCommandUnresponsiveException e) {
    636             e.printStackTrace();
    637         } catch (IOException e) {
    638             e.printStackTrace();
    639         }
    640     }
    641 
    642     /**
    643      * Task that updates UI with given {@link NetworkSnapshot} delta.
    644      */
    645     private class UpdateDeltaRunnable implements Runnable {
    646         private final NetworkSnapshot mDelta;
    647         private final long mEndTime;
    648 
    649         public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) {
    650             mDelta = delta;
    651             mEndTime = endTime;
    652         }
    653 
    654         @Override
    655         public void run() {
    656             if (mDisplay.isDisposed()) return;
    657 
    658             final Millisecond time = new Millisecond(new Date(mEndTime));
    659             for (NetworkSnapshot.Entry entry : mDelta) {
    660                 if (mActiveUid != entry.uid) continue;
    661 
    662                 final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag);
    663                 item.recordDelta(time, mDelta.timestamp, entry);
    664             }
    665 
    666             // remove any historical detail data
    667             final long beforeMillis = mEndTime - HISTORY_MILLIS;
    668             mRxDetailDataset.removeBefore(beforeMillis);
    669             mTxDetailDataset.removeBefore(beforeMillis);
    670 
    671             // trigger refresh from bulk changes above
    672             mRxDetailDataset.fireDatasetChanged();
    673             mTxDetailDataset.fireDatasetChanged();
    674 
    675             // update axis to show latest 30 second time period
    676             mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime);
    677 
    678             updateSeriesPaint();
    679 
    680             // kick table viewer to update
    681             mTableViewer.setInput(mTrackedItems);
    682         }
    683     }
    684 
    685     /**
    686      * Parser that extracts UID from remote {@code /proc/pid/status} file.
    687      */
    688     private static class UidParser extends MultiLineReceiver {
    689         public int uid = -1;
    690 
    691         @Override
    692         public boolean isCancelled() {
    693             return false;
    694         }
    695 
    696         @Override
    697         public void processNewLines(String[] lines) {
    698             for (String line : lines) {
    699                 if (line.startsWith("Uid:")) {
    700                     // we care about the "real" UID
    701                     final String[] cols = line.split("\t");
    702                     uid = Integer.parseInt(cols[1]);
    703                 }
    704             }
    705         }
    706     }
    707 
    708     /**
    709      * Parser that populates {@link NetworkSnapshot} based on contents of remote
    710      * {@link NetworkPanel#PROC_XT_QTAGUID} file.
    711      */
    712     private static class NetworkSnapshotParser extends MultiLineReceiver {
    713         private NetworkSnapshot mSnapshot;
    714 
    715         public NetworkSnapshotParser() {
    716             mSnapshot = new NetworkSnapshot(System.currentTimeMillis());
    717         }
    718 
    719         public boolean isError() {
    720             return mSnapshot == null;
    721         }
    722 
    723         public NetworkSnapshot getParsedSnapshot() {
    724             return mSnapshot;
    725         }
    726 
    727         @Override
    728         public boolean isCancelled() {
    729             return false;
    730         }
    731 
    732         @Override
    733         public void processNewLines(String[] lines) {
    734             for (String line : lines) {
    735                 if (line.endsWith("No such file or directory")) {
    736                     mSnapshot = null;
    737                     return;
    738                 }
    739 
    740                 // ignore header line
    741                 if (line.startsWith("idx")) {
    742                     continue;
    743                 }
    744 
    745                 final String[] cols = line.split(" ");
    746                 if (cols.length < 9) continue;
    747 
    748                 // iface and set are currently ignored, which groups those
    749                 // entries together.
    750                 final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry();
    751                 entry.iface = null; //cols[1];
    752                 entry.uid = Integer.parseInt(cols[3]);
    753                 entry.set = -1; //Integer.parseInt(cols[4]);
    754                 entry.tag = (int) (Long.decode(cols[2]) >> 32);
    755                 entry.rxBytes = Long.parseLong(cols[5]);
    756                 entry.rxPackets = Long.parseLong(cols[6]);
    757                 entry.txBytes = Long.parseLong(cols[7]);
    758                 entry.txPackets = Long.parseLong(cols[8]);
    759 
    760                 mSnapshot.combine(entry);
    761             }
    762         }
    763     }
    764 
    765     /**
    766      * Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time.
    767      */
    768     private static class NetworkSnapshot implements Iterable<NetworkSnapshot.Entry> {
    769         private ArrayList<Entry> mStats = new ArrayList<Entry>();
    770 
    771         public final long timestamp;
    772 
    773         /** Single parsed statistics row. */
    774         public static class Entry {
    775             public String iface;
    776             public int uid;
    777             public int set;
    778             public int tag;
    779             public long rxBytes;
    780             public long rxPackets;
    781             public long txBytes;
    782             public long txPackets;
    783 
    784             public boolean isEmpty() {
    785                 return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0;
    786             }
    787         }
    788 
    789         public NetworkSnapshot(long timestamp) {
    790             this.timestamp = timestamp;
    791         }
    792 
    793         public void clear() {
    794             mStats.clear();
    795         }
    796 
    797         /**
    798          * Combine the given {@link Entry} with any existing {@link Entry}, or
    799          * insert if none exists.
    800          */
    801         public void combine(Entry entry) {
    802             final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag);
    803             if (existing != null) {
    804                 existing.rxBytes += entry.rxBytes;
    805                 existing.rxPackets += entry.rxPackets;
    806                 existing.txBytes += entry.txBytes;
    807                 existing.txPackets += entry.txPackets;
    808             } else {
    809                 mStats.add(entry);
    810             }
    811         }
    812 
    813         @Override
    814         public Iterator<Entry> iterator() {
    815             return mStats.iterator();
    816         }
    817 
    818         public Entry findEntry(String iface, int uid, int set, int tag) {
    819             for (Entry entry : mStats) {
    820                 if (entry.uid == uid && entry.set == set && entry.tag == tag
    821                         && equal(entry.iface, iface)) {
    822                     return entry;
    823                 }
    824             }
    825             return null;
    826         }
    827 
    828         /**
    829          * Subtract the two given {@link NetworkSnapshot} objects, returning the
    830          * delta between them.
    831          */
    832         public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) {
    833             final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp);
    834 
    835             // for each row on left, subtract value from right side
    836             for (Entry leftEntry : left) {
    837                 final Entry rightEntry = right.findEntry(
    838                         leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag);
    839                 if (rightEntry == null) continue;
    840 
    841                 final Entry resultEntry = new Entry();
    842                 resultEntry.iface = leftEntry.iface;
    843                 resultEntry.uid = leftEntry.uid;
    844                 resultEntry.set = leftEntry.set;
    845                 resultEntry.tag = leftEntry.tag;
    846                 resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes;
    847                 resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets;
    848                 resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes;
    849                 resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets;
    850 
    851                 result.combine(resultEntry);
    852             }
    853 
    854             return result;
    855         }
    856     }
    857 
    858     /**
    859      * Provider of {@link #mTrackedItems}.
    860      */
    861     private class ContentProvider implements IStructuredContentProvider {
    862         @Override
    863         public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
    864             // pass
    865         }
    866 
    867         @Override
    868         public void dispose() {
    869             // pass
    870         }
    871 
    872         @Override
    873         public Object[] getElements(Object inputElement) {
    874             return mTrackedItems.toArray();
    875         }
    876     }
    877 
    878     /**
    879      * Provider of labels for {@Link TrackedItem} values.
    880      */
    881     private static class LabelProvider implements ITableLabelProvider {
    882         private final DecimalFormat mFormat = new DecimalFormat("#,###");
    883 
    884         @Override
    885         public Image getColumnImage(Object element, int columnIndex) {
    886             if (element instanceof TrackedItem) {
    887                 final TrackedItem item = (TrackedItem) element;
    888                 switch (columnIndex) {
    889                     case 0:
    890                         return item.colorImage;
    891                 }
    892             }
    893             return null;
    894         }
    895 
    896         @Override
    897         public String getColumnText(Object element, int columnIndex) {
    898             if (element instanceof TrackedItem) {
    899                 final TrackedItem item = (TrackedItem) element;
    900                 switch (columnIndex) {
    901                     case 0:
    902                         return null;
    903                     case 1:
    904                         return item.label;
    905                     case 2:
    906                         return mFormat.format(item.rxBytes);
    907                     case 3:
    908                         return mFormat.format(item.rxPackets);
    909                     case 4:
    910                         return mFormat.format(item.txBytes);
    911                     case 5:
    912                         return mFormat.format(item.txPackets);
    913                 }
    914             }
    915             return null;
    916         }
    917 
    918         @Override
    919         public void addListener(ILabelProviderListener listener) {
    920             // pass
    921         }
    922 
    923         @Override
    924         public void dispose() {
    925             // pass
    926         }
    927 
    928         @Override
    929         public boolean isLabelProperty(Object element, String property) {
    930             // pass
    931             return false;
    932         }
    933 
    934         @Override
    935         public void removeListener(ILabelProviderListener listener) {
    936             // pass
    937         }
    938     }
    939 
    940     /**
    941      * Format that displays simplified byte units for when given values are
    942      * large enough.
    943      */
    944     private static class BytesFormat extends NumberFormat {
    945         private final String[] mUnits;
    946         private final DecimalFormat mFormat = new DecimalFormat("#.#");
    947 
    948         public BytesFormat(boolean perSecond) {
    949             if (perSecond) {
    950                 mUnits = new String[] { "B/s", "KB/s", "MB/s" };
    951             } else {
    952                 mUnits = new String[] { "B", "KB", "MB" };
    953             }
    954         }
    955 
    956         @Override
    957         public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
    958             double value = Math.abs(number);
    959 
    960             int i = 0;
    961             while (value > 1024 && i < mUnits.length - 1) {
    962                 value /= 1024;
    963                 i++;
    964             }
    965 
    966             toAppendTo.append(mFormat.format(value));
    967             toAppendTo.append(mUnits[i]);
    968 
    969             return toAppendTo;
    970         }
    971 
    972         @Override
    973         public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
    974             return format((long) number, toAppendTo, pos);
    975         }
    976 
    977         @Override
    978         public Number parse(String source, ParsePosition parsePosition) {
    979             return null;
    980         }
    981     }
    982 
    983     public static boolean equal(Object a, Object b) {
    984         return a == b || (a != null && a.equals(b));
    985     }
    986 
    987     /**
    988      * Build stub string of requested length, usually for measurement.
    989      */
    990     private static String buildSampleText(int length) {
    991         final StringBuilder builder = new StringBuilder(length);
    992         for (int i = 0; i < length; i++) {
    993             builder.append("X");
    994         }
    995         return builder.toString();
    996     }
    997 
    998     /**
    999      * Dataset that contains live measurements. Exposes
   1000      * {@link #removeBefore(long)} to efficiently remove old data, and enables
   1001      * batched {@link #fireDatasetChanged()} events.
   1002      */
   1003     public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements
   1004             TableXYDataset {
   1005         private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true);
   1006 
   1007         /**
   1008          * Caller is responsible for triggering {@link #fireDatasetChanged()}.
   1009          */
   1010         public void addValue(Number value, TimePeriod rowKey, String columnKey) {
   1011             mValues.addValue(value, rowKey, columnKey);
   1012         }
   1013 
   1014         /**
   1015          * Caller is responsible for triggering {@link #fireDatasetChanged()}.
   1016          */
   1017         public void removeBefore(long beforeMillis) {
   1018             while(mValues.getRowCount() > 0) {
   1019                 final TimePeriod period = (TimePeriod) mValues.getRowKey(0);
   1020                 if (period.getEnd().getTime() < beforeMillis) {
   1021                     mValues.removeRow(0);
   1022                 } else {
   1023                     break;
   1024                 }
   1025             }
   1026         }
   1027 
   1028         public int getColumnIndex(String key) {
   1029             return mValues.getColumnIndex(key);
   1030         }
   1031 
   1032         public void clear() {
   1033             mValues.clear();
   1034             fireDatasetChanged();
   1035         }
   1036 
   1037         @Override
   1038         public void fireDatasetChanged() {
   1039             super.fireDatasetChanged();
   1040         }
   1041 
   1042         @Override
   1043         public int getItemCount() {
   1044             return mValues.getRowCount();
   1045         }
   1046 
   1047         @Override
   1048         public int getItemCount(int series) {
   1049             return mValues.getRowCount();
   1050         }
   1051 
   1052         @Override
   1053         public int getSeriesCount() {
   1054             return mValues.getColumnCount();
   1055         }
   1056 
   1057         @Override
   1058         public Comparable getSeriesKey(int series) {
   1059             return mValues.getColumnKey(series);
   1060         }
   1061 
   1062         @Override
   1063         public double getXValue(int series, int item) {
   1064             final TimePeriod period = (TimePeriod) mValues.getRowKey(item);
   1065             return period.getStart().getTime();
   1066         }
   1067 
   1068         @Override
   1069         public double getStartXValue(int series, int item) {
   1070             return getXValue(series, item);
   1071         }
   1072 
   1073         @Override
   1074         public double getEndXValue(int series, int item) {
   1075             return getXValue(series, item);
   1076         }
   1077 
   1078         @Override
   1079         public Number getX(int series, int item) {
   1080             return getXValue(series, item);
   1081         }
   1082 
   1083         @Override
   1084         public Number getStartX(int series, int item) {
   1085             return getXValue(series, item);
   1086         }
   1087 
   1088         @Override
   1089         public Number getEndX(int series, int item) {
   1090             return getXValue(series, item);
   1091         }
   1092 
   1093         @Override
   1094         public Number getY(int series, int item) {
   1095             return mValues.getValue(item, series);
   1096         }
   1097 
   1098         @Override
   1099         public Number getStartY(int series, int item) {
   1100             return getY(series, item);
   1101         }
   1102 
   1103         @Override
   1104         public Number getEndY(int series, int item) {
   1105             return getY(series, item);
   1106         }
   1107     }
   1108 }
   1109