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