• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
finish()167         public void finish() {
168             mFinish = true;
169             interrupt();
170         }
171 
172         @Override
run()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
createControl(Composite parent)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      */
createHeader()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      */
createChart()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      */
createTable()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      */
updateSpeed()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      */
updateRunning(boolean shouldRun)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
setFocus()414     public void setFocus() {
415         mPanel.setFocus();
416     }
417 
nextSeriesColor(int index)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      */
findOrCreateTrackedItem(int uid, int tag)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      */
clearTrackedItems()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      */
updateSeriesPaint()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 
TrackedItem(int uid, int tag)509         public TrackedItem(int uid, int tag) {
510             this.uid = uid;
511             this.tag = tag;
512         }
513 
isTotal()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          */
recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta)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
deviceSelected()546     public void deviceSelected() {
547         // treat as client selection to update enabled states
548         clientSelected();
549     }
550 
551     @Override
clientSelected()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
clientChanged(Client client, int changeMask)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      */
performSample()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 
UpdateDeltaRunnable(NetworkSnapshot delta, long endTime)649         public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) {
650             mDelta = delta;
651             mEndTime = endTime;
652         }
653 
654         @Override
run()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
isCancelled()692         public boolean isCancelled() {
693             return false;
694         }
695 
696         @Override
processNewLines(String[] lines)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 
NetworkSnapshotParser()715         public NetworkSnapshotParser() {
716             mSnapshot = new NetworkSnapshot(System.currentTimeMillis());
717         }
718 
isError()719         public boolean isError() {
720             return mSnapshot == null;
721         }
722 
getParsedSnapshot()723         public NetworkSnapshot getParsedSnapshot() {
724             return mSnapshot;
725         }
726 
727         @Override
isCancelled()728         public boolean isCancelled() {
729             return false;
730         }
731 
732         @Override
processNewLines(String[] lines)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 
isEmpty()784             public boolean isEmpty() {
785                 return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0;
786             }
787         }
788 
NetworkSnapshot(long timestamp)789         public NetworkSnapshot(long timestamp) {
790             this.timestamp = timestamp;
791         }
792 
clear()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          */
combine(Entry entry)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
iterator()814         public Iterator<Entry> iterator() {
815             return mStats.iterator();
816         }
817 
findEntry(String iface, int uid, int set, int tag)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          */
subtract(NetworkSnapshot left, NetworkSnapshot right)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
inputChanged(Viewer viewer, Object oldInput, Object newInput)863         public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
864             // pass
865         }
866 
867         @Override
dispose()868         public void dispose() {
869             // pass
870         }
871 
872         @Override
getElements(Object inputElement)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
getColumnImage(Object element, int columnIndex)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
getColumnText(Object element, int columnIndex)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
addListener(ILabelProviderListener listener)919         public void addListener(ILabelProviderListener listener) {
920             // pass
921         }
922 
923         @Override
dispose()924         public void dispose() {
925             // pass
926         }
927 
928         @Override
isLabelProperty(Object element, String property)929         public boolean isLabelProperty(Object element, String property) {
930             // pass
931             return false;
932         }
933 
934         @Override
removeListener(ILabelProviderListener listener)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 
BytesFormat(boolean perSecond)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
format(long number, StringBuffer toAppendTo, FieldPosition pos)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
format(double number, StringBuffer toAppendTo, FieldPosition pos)973         public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
974             return format((long) number, toAppendTo, pos);
975         }
976 
977         @Override
parse(String source, ParsePosition parsePosition)978         public Number parse(String source, ParsePosition parsePosition) {
979             return null;
980         }
981     }
982 
equal(Object a, Object b)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      */
buildSampleText(int length)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          */
addValue(Number value, TimePeriod rowKey, String columnKey)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          */
removeBefore(long beforeMillis)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 
getColumnIndex(String key)1028         public int getColumnIndex(String key) {
1029             return mValues.getColumnIndex(key);
1030         }
1031 
clear()1032         public void clear() {
1033             mValues.clear();
1034             fireDatasetChanged();
1035         }
1036 
1037         @Override
fireDatasetChanged()1038         public void fireDatasetChanged() {
1039             super.fireDatasetChanged();
1040         }
1041 
1042         @Override
getItemCount()1043         public int getItemCount() {
1044             return mValues.getRowCount();
1045         }
1046 
1047         @Override
getItemCount(int series)1048         public int getItemCount(int series) {
1049             return mValues.getRowCount();
1050         }
1051 
1052         @Override
getSeriesCount()1053         public int getSeriesCount() {
1054             return mValues.getColumnCount();
1055         }
1056 
1057         @Override
getSeriesKey(int series)1058         public Comparable getSeriesKey(int series) {
1059             return mValues.getColumnKey(series);
1060         }
1061 
1062         @Override
getXValue(int series, int item)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
getStartXValue(int series, int item)1069         public double getStartXValue(int series, int item) {
1070             return getXValue(series, item);
1071         }
1072 
1073         @Override
getEndXValue(int series, int item)1074         public double getEndXValue(int series, int item) {
1075             return getXValue(series, item);
1076         }
1077 
1078         @Override
getX(int series, int item)1079         public Number getX(int series, int item) {
1080             return getXValue(series, item);
1081         }
1082 
1083         @Override
getStartX(int series, int item)1084         public Number getStartX(int series, int item) {
1085             return getXValue(series, item);
1086         }
1087 
1088         @Override
getEndX(int series, int item)1089         public Number getEndX(int series, int item) {
1090             return getXValue(series, item);
1091         }
1092 
1093         @Override
getY(int series, int item)1094         public Number getY(int series, int item) {
1095             return mValues.getValue(item, series);
1096         }
1097 
1098         @Override
getStartY(int series, int item)1099         public Number getStartY(int series, int item) {
1100             return getY(series, item);
1101         }
1102 
1103         @Override
getEndY(int series, int item)1104         public Number getEndY(int series, int item) {
1105             return getY(series, item);
1106         }
1107     }
1108 }
1109