| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.tools.idea.monitor.network; |
| |
| import com.android.ddmlib.*; |
| import com.android.tools.chartlib.TimelineData; |
| import com.android.tools.idea.monitor.DeviceSampler; |
| import org.jetbrains.annotations.NotNull; |
| |
| import java.io.IOException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.logging.Logger; |
| |
| public class NetworkSampler extends DeviceSampler { |
| |
| public static final String NETWORK_STATS_FILE = "/proc/net/xt_qtaguid/stats"; |
| private static final int MAX_TIMEOUT_SECOND = 1; |
| private static final Logger LOG = Logger.getLogger(NetworkSampler.class.getName()); |
| private static final String LINE_SPLIT_REGEX = "[ \t\r\n\f]"; |
| |
| // Rx (received) and tx (transmitted) bytes in system file are accumulated, but what needed for monitoring is the real-time traffic flow. |
| // Stores the last read bytes variable to calculate the real time data. |
| private long myLastRxBytes; |
| private long myLastTxBytes; |
| private int myUid; |
| // The first sampling values are not current traffic, because they can include previous network traffic values. |
| private boolean myIsFirstSample; |
| |
| public NetworkSampler(@NotNull TimelineData timelineData, int frequencyMs) { |
| super(timelineData, frequencyMs); |
| } |
| |
| @NotNull |
| @Override |
| public String getDescription() { |
| return "Network data usage sampler that approximates the bytes received and transmitted"; |
| } |
| |
| @NotNull |
| @Override |
| public String getName() { |
| return "Network Sampler"; |
| } |
| |
| @Override |
| public void start() { |
| super.start(); |
| myUid = -1; |
| myLastRxBytes = 0L; |
| myLastTxBytes = 0L; |
| myIsFirstSample = true; |
| } |
| |
| public boolean canReadNetworkStatistics() { |
| IDevice device = myClient != null ? myClient.getDevice() : null; |
| if (device == null) { |
| return false; |
| } |
| |
| CollectingOutputReceiver receiver = new CollectingOutputReceiver(); |
| try { |
| device.executeShellCommand("ls " + NETWORK_STATS_FILE, receiver); |
| } |
| catch (TimeoutException timeoutException) { |
| LOG.warning(String.format("TimeoutException %1$s in ls %2$s", timeoutException.getMessage(), NETWORK_STATS_FILE)); |
| } |
| catch (AdbCommandRejectedException rejectedException) { |
| LOG.warning( |
| String.format("AdbCommandRejectedException %1$s in ls %2$s", rejectedException.getMessage(), NETWORK_STATS_FILE)); |
| } |
| catch (ShellCommandUnresponsiveException unresponsiveException) { |
| LOG.warning(String.format("ShellCommandUnresponsiveException %1$s in ls %2$s", unresponsiveException.getMessage(), NETWORK_STATS_FILE)); |
| } |
| catch (IOException ioException) { |
| LOG.warning(String.format("IOException %1$s in ls %2$s", ioException.getMessage(), NETWORK_STATS_FILE)); |
| } |
| return !receiver.getOutput().contains("No such file"); |
| } |
| |
| /** |
| * Samples the network usage belonging to the connected device and application. |
| */ |
| @Override |
| protected void sample(boolean forced) throws InterruptedException { |
| IDevice device = myClient != null ? myClient.getDevice() : null; |
| if (device == null) { |
| return; |
| } |
| |
| if (myUid < 0) { |
| int pid = myClient.getClientData().getPid(); |
| myUid = getUidFromPid(pid, device); |
| if (myUid < 0) { |
| return; |
| } |
| } |
| |
| NetworkStatsReceiver receiver = new NetworkStatsReceiver(myUid); |
| // Command that gets network metrics, the output includes the connected app and other apps' stats. |
| String command = "cat " + NETWORK_STATS_FILE + " | grep " + receiver.getUid(); |
| int myDataType = TYPE_DATA; |
| try { |
| device.executeShellCommand(command, receiver, MAX_TIMEOUT_SECOND, TimeUnit.SECONDS); |
| } |
| catch (TimeoutException timeoutException) { |
| myDataType = TYPE_TIMEOUT; |
| } |
| catch (AdbCommandRejectedException commandRejectedException) { |
| myDataType = TYPE_UNREACHABLE; |
| } |
| catch (ShellCommandUnresponsiveException commandUnresponsiveException) { |
| myDataType = TYPE_UNREACHABLE; |
| } |
| catch (IOException ioException) { |
| myDataType = TYPE_UNREACHABLE; |
| } |
| if (receiver.isFileMissing()) { |
| return; |
| } |
| |
| long rxBytesIncreased = 0L; |
| long rxBytesInTotal = receiver.getRxBytes(); |
| if (rxBytesInTotal > myLastRxBytes) { |
| rxBytesIncreased = rxBytesInTotal - myLastRxBytes; |
| myLastRxBytes = rxBytesInTotal; |
| } |
| long txBytesIncreased = 0L; |
| long txBytesInTotal = receiver.getTxBytes(); |
| if (txBytesInTotal > myLastTxBytes) { |
| txBytesIncreased = txBytesInTotal - myLastTxBytes; |
| myLastTxBytes = txBytesInTotal; |
| } |
| if (myIsFirstSample) { |
| myIsFirstSample = false; |
| } |
| else { |
| myData.add(System.currentTimeMillis(), myDataType, rxBytesIncreased / 1024.f, txBytesIncreased / 1024.f); |
| } |
| } |
| |
| private static int getUidFromPid(int pid, IDevice device) { |
| UidReceiver uidReceiver = new UidReceiver(); |
| try { |
| device.executeShellCommand("cat /proc/" + pid + "/status", uidReceiver, MAX_TIMEOUT_SECOND, TimeUnit.SECONDS); |
| } |
| catch (TimeoutException timeoutException) { |
| LOG.warning(String.format("TimeoutException to get uid from pid %d", pid)); |
| } |
| catch (AdbCommandRejectedException commandRejectedException) { |
| LOG.warning(String.format("AdbCommandRejectedException to get uid from pid %d", pid)); |
| } |
| catch (ShellCommandUnresponsiveException commandUnresponsiveException) { |
| LOG.warning(String.format("ShellCommandUnresponsiveException to get uid from pid %d", pid)); |
| } |
| catch (IOException ioException) { |
| LOG.warning(String.format("IOException to get uid from pid %d", pid)); |
| } |
| return uidReceiver.getUid(); |
| } |
| |
| /** |
| * Object to receive the shell get pid status command output and get the uid. |
| */ |
| private static class UidReceiver extends MultiLineReceiver { |
| |
| private static final int INDEX_OF_EFFECTIVE_USER_ID = 1; |
| |
| private int myUid; |
| |
| public UidReceiver() { |
| myUid = -1; |
| } |
| |
| public int getUid() { |
| return myUid; |
| } |
| |
| @Override |
| public void processNewLines(String[] lines) { |
| for (String line : lines) { |
| if (line.startsWith("Uid:")) { |
| String[] values = line.split(LINE_SPLIT_REGEX); |
| if (values.length <= INDEX_OF_EFFECTIVE_USER_ID) { |
| |
| LOG.warning(String.format("NumberFormatException %1$s \n length %2$d", line, values.length)); |
| return; |
| } |
| try { |
| myUid = Integer.parseInt(values[INDEX_OF_EFFECTIVE_USER_ID]); |
| } |
| catch (NumberFormatException e) { |
| LOG.warning(String.format("NumberFormatException %1$s in %2$s", e.getMessage(), line)); |
| } |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public boolean isCancelled() { |
| return false; |
| } |
| } |
| |
| /** |
| * Object to receive the network stats adb command output and parse the table-formatted output. |
| */ |
| private static class NetworkStatsReceiver extends MultiLineReceiver { |
| |
| private static int INDEX_OF_UID = 3; |
| private static int INDEX_OF_RX_BYTES = 5; |
| private static int INDEX_OF_TX_BYTES = 7; |
| |
| private final int myUid; |
| private long myRxBytes; |
| private long myTxBytes; |
| private boolean myIsFileMissing; |
| |
| public NetworkStatsReceiver(int uid) { |
| this.myUid = uid; |
| this.myRxBytes = 0L; |
| this.myTxBytes = 0L; |
| this.myIsFileMissing = false; |
| } |
| |
| public int getUid() { |
| return myUid; |
| } |
| |
| public long getRxBytes() { |
| return myRxBytes; |
| } |
| |
| public long getTxBytes() { |
| return myTxBytes; |
| } |
| |
| public boolean isFileMissing() { |
| return myIsFileMissing; |
| } |
| |
| /** |
| * Processes the stats line to sum up all network stats belonging to the uid. |
| */ |
| @Override |
| public void processNewLines(String[] lines) { |
| for (String line : lines) { |
| // Line starting with idx is the header. |
| if (line.startsWith("idx")) { |
| continue; |
| } |
| if (line.contains("No such file")) { |
| myIsFileMissing = true; |
| return; |
| } |
| String[] values = line.split(LINE_SPLIT_REGEX); |
| if (values.length < INDEX_OF_TX_BYTES) { |
| continue; |
| } |
| |
| // Gets the network usages belonging to the uid. |
| try { |
| int lineUid = Integer.parseInt(values[INDEX_OF_UID]); |
| if (myUid == lineUid) { |
| int tempRxBytes = Integer.parseInt(values[INDEX_OF_RX_BYTES]); |
| int tempTxBytes = Integer.parseInt(values[INDEX_OF_TX_BYTES]); |
| if (tempRxBytes < 0 || tempTxBytes < 0) { |
| LOG.warning(String.format("Negative rxBytes %1$d and/or txBytes %2$d in %3$s", tempRxBytes, tempTxBytes, line)); |
| continue; |
| } |
| myRxBytes += tempRxBytes; |
| myTxBytes += tempTxBytes; |
| } |
| } |
| catch (NumberFormatException e) { |
| LOG.warning(String.format("Expected int value, instead got uid %1$s, rxBytes %2$s, txBytes %3$s", values[INDEX_OF_UID], |
| values[INDEX_OF_RX_BYTES], values[INDEX_OF_TX_BYTES])); |
| } |
| } |
| } |
| |
| @Override |
| public boolean isCancelled() { |
| return false; |
| } |
| } |
| } |