Java tutorial
/* * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. * * 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.mobiperf.measurements; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.InvalidClassException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.SharedPreferences; import android.net.TrafficStats; import android.util.StringBuilderPrinter; import com.mobiperf.Checkin; import com.mobiperf.Config; import com.mobiperf.Logger; import com.mobiperf.MeasurementDesc; import com.mobiperf.MeasurementError; import com.mobiperf.MeasurementResult; import com.mobiperf.MeasurementTask; import com.mobiperf.RRCTrafficControl; import com.mobiperf.util.MeasurementJsonConvertor; import com.mobiperf.util.PhoneUtils; /** * This class measures the round trip times of packets as you vary the packet timings between them, * with the purpose of inferring RRC state transitions. * * See "Characterizing Radio Resource Allocation for 3G Networks" by Feng et. al, IMC 2010 for a * full explanation of the methodology and goals. * * TODO (sanae): Pause the RRC tasks when a checkin is performed, rather than pausing the checkin * while the RRC task is performed. * * @author sanae@umich.edu (Sanae Rosen) * */ public class RRCTask extends MeasurementTask { // Type name for internal use public static final String TYPE = "rrc"; // Human readable name for the task public static final String DESCRIPTOR = "rrc"; public static String TAG = "MobiPerf_RRC_INFERENCE"; private boolean stop = false; private Context context; // Track data consumption for this task to avoid exceeding user's limit public static long data_consumed = 0; /** * Stores parameters for the RRC inference task * @author sanae@umich.edu (Sanae Rosen) * */ public static class RRCDesc extends MeasurementDesc { private static String HOST = "www.google.com"; // Default echo server name and port to measure the RTT to infer RRC state private static int PORT = 50000; private static String ECHO_HOST = "ep2.eecs.umich.edu"; // Perform RTT measurements every GRANULARITY ms public int GRANULARITY = 500; // MIN / MAX is the echo packet size int MIN = 0; int MAX = 1024; // Default total number of measurements int size = 31; // Echo server / port, and target to perform the upper-layer tasks public String echoHost = ECHO_HOST; public String target = HOST; int port = PORT; long testId; // unique value for this set of tests // Default threshold to repeat each RTT measurement because of background traffic int GIVEUP_THRESHHOLD = 15; // server controled variable boolean DNS = true; boolean TCP = true; boolean HTTP = true; boolean RRC = true; boolean SIZES = true; // Whether RRC result is visible to users public boolean RESULT_VISIBILITY = false; /* * For the upper-layer tests, a series of tests are made for different inter-packet intervals, * in order. "Times" indicates the inter-packet intervals, the other fields store the results. * All must be the same size. */ Integer[] times; // The times where the above tests were made, in units of GRANULARITY. int sizeGranularity = 200; // the spacing between sizes to test int[] httpTest; // The results of the HTTP test performed at each time int[] dnsTest; // likewise, for the DNS test int[] tcpTest; // likewise, for the TCP test // Whether or not to run the upper layer tests, i.e. the HTTP, TCP and DNS tests. // Disabling this flag will disable all upper layer tests. private boolean runUpperLayerTests = false; /* * Default times between packets for which the upper layer tests are performed. Later, these * times will be replaced by times from the model. These times are GRANULARITY milliseconds: if * GRANULARITY is 500, then a default time of 6 means measurements are taken 500 ms apart. */ Integer[] defaultTimesULTasks = new Integer[] { 0, 2, 4, 8, 12, 16, 22 }; public RRCDesc(String key, Date startTime, Date endTime, double intervalSec, long count, long priority, Map<String, String> params) { super(RRCTask.TYPE, key, startTime, endTime, intervalSec, count, priority, params); initializeParams(params); } public MeasurementResult getResults(MeasurementResult result) { if (HTTP) result.addResult("http", httpTest); if (TCP) result.addResult("tcp", tcpTest); if (DNS) result.addResult("dns", dnsTest); result.addResult("times", times); return result; } public void displayResults(StringBuilderPrinter printer) { String DEL = "\t", toprint = DEL + DEL; for (int i = 1; i <= times.length; i++) { toprint += DEL + " | state" + i; } toprint += " |"; int oneLineLen = toprint.length(); toprint += "\n"; // seperator for (int i = 0; i < oneLineLen; i++) { toprint += "-"; } toprint += "\n"; if (HTTP) { toprint += "HTTP (ms)" + DEL; for (int i = 0; i < httpTest.length; i++) { toprint += DEL + " | " + Integer.toString(httpTest[i]); } toprint += " |\n"; for (int i = 0; i < oneLineLen; i++) { toprint += "-"; } toprint += "\n"; } if (DNS) { toprint += "DNS (ms)" + DEL; for (int i = 0; i < dnsTest.length; i++) { toprint += DEL + " | " + Integer.toString(dnsTest[i]); } toprint += " |\n"; for (int i = 0; i < oneLineLen; i++) { toprint += "-"; } toprint += "\n"; } if (TCP) { toprint += "TCP (ms)" + DEL; for (int i = 0; i < tcpTest.length; i++) { toprint += DEL + " | " + Integer.toString(tcpTest[i]); } toprint += " |\n"; for (int i = 0; i < oneLineLen; i++) { toprint += "-"; } toprint += "\n"; } toprint += "Timers (s)"; for (int i = 0; i < times.length; i++) { double curTime = (double) times[i] * (double) GRANULARITY / 1000.0; toprint += DEL + " | " + String.format("%.2f", curTime); } toprint += " |\n"; printer.println(toprint); } @Override public String getType() { return RRCTask.TYPE; } /** * Given the parameters fetched from the server, sets up the parameters as needed for the upper * layer tests, i.e. the application layer tests. */ @Override protected void initializeParams(Map<String, String> params) { // In this case, we fall back to the default values defined above. if (params == null) { return; } // The parameters for the echo server this.echoHost = params.get("echo_host"); this.target = params.get("target"); if (this.echoHost == null) { this.echoHost = ECHO_HOST; } if (this.target == null) { this.target = HOST; } Logger.d("param: echo_host " + this.echoHost); Logger.d("param: target " + this.target); try { String val = null; // Size of the small packet if ((val = params.get("min")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.MIN = Integer.parseInt(val); } Logger.d("param: Min " + this.MIN); // Size of the large packet if ((val = params.get("max")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.MAX = Integer.parseInt(val); } Logger.d("param: MAX " + this.MAX); // Echo server port if ((val = params.get("port")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.port = Integer.parseInt(val); } Logger.d("param: port " + this.port); // Number of tests to run from the RRC test if ((val = params.get("size")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.size = Integer.parseInt(val); } Logger.d("param: size " + this.size); // When testing size dependence, increase the if ((val = params.get("size_granularity")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.sizeGranularity = Integer.parseInt(val); } Logger.d("param: size_granularity " + this.sizeGranularity); // Whether or not to run the DNS test if ((val = params.get("dns")) != null && val.length() > 0) { this.DNS = Boolean.parseBoolean(val); } Logger.d("param: DNS " + this.DNS); // Whether or not to run the HTTP test if ((val = params.get("http")) != null && val.length() > 0) { this.HTTP = Boolean.parseBoolean(val); } Logger.d("param: HTTP " + this.HTTP); // Whether or not to run the TCP test if ((val = params.get("tcp")) != null && val.length() > 0) { this.TCP = Boolean.parseBoolean(val); } Logger.d(params.get("rrc")); Logger.d("param: TCP " + this.TCP); // Whether or not to run the RRC inference task if ((val = params.get("rrc")) != null && val.length() > 0) { this.RRC = Boolean.parseBoolean(val); } Logger.d("param: RRC " + this.RRC); if ((val = params.get("measure_sizes")) != null && val.length() > 0) { this.SIZES = Boolean.parseBoolean(val); } Logger.d("param: SIZES " + this.SIZES); // Whether the RRC result is visible to users if ((val = params.get("result_visibility")) != null && val.length() > 0) { this.RESULT_VISIBILITY = Boolean.parseBoolean(val); } Logger.d("param: visibility " + this.RESULT_VISIBILITY); // How many times to retry a test when interrupted by background traffic if ((val = params.get("giveup_threshhold")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.GIVEUP_THRESHHOLD = Integer.parseInt(val); } Logger.d("param: GIVEUP_THRESHHOLD " + this.GIVEUP_THRESHHOLD); // Default assumed timers for the upper layer tests (HTTP, DNS, TCP), // in units of GRANULARITY. These are set via a comma-separated list // of numbers. if ((val = params.get("default_extra_test_timers")) != null && val.length() > 0) { String[] timesString = val.split("\\s*,\\s*"); List<String> stringList = new ArrayList<String>(Arrays.asList(timesString)); List<Integer> intList = new ArrayList<Integer>(); Iterator<String> iterator = stringList.iterator(); while (iterator.hasNext()) { intList.add(Integer.parseInt(iterator.next())); } times = (Integer[]) intList.toArray(new Integer[intList.size()]); } if (times == null) { times = defaultTimesULTasks; } } catch (NumberFormatException e) { throw new InvalidParameterException(" RRCTask cannot be created due to invalid params"); } if (size == 0) { // 31 tests, by default. From 0s to 15s inclusive, in half-second intervals. size = 31; } } /** * For the arrays holding the results for the upper layer tests, we need to initialize them to * be the same size as the number of tests we run. -1 means uninitialized. * * @param size Number of results to store */ public void initializeExtraTaskResults(int size) { httpTest = new int[size]; dnsTest = new int[size]; tcpTest = new int[size]; for (int i = 0; i < size; i++) { httpTest[i] = -1; dnsTest[i] = -1; tcpTest[i] = -1; } runUpperLayerTests = true; } /** * Given an interpacket interval and a round trip time from an HTTP test, store that value. * * @param index Index into measurement result array, corresponding to an interpacket interval * @param rtt Time for HTTP test to complete, in milliseconds * @throws MeasurementError */ public void setHttp(int index, int rtt) throws MeasurementError { if (!runUpperLayerTests) { throw new MeasurementError("Data class not initialized"); } httpTest[index] = rtt; } /** * Given an interpacket interval and a round trip time from a TCP test, store that value. * * @param index Index into measurement result array, corresponding to an interpacket interval * @param rtt Time for the TCP test to complete, in milliseconds * @throws MeasurementError */ public void setTcp(int index, int rtt) throws MeasurementError { if (!runUpperLayerTests) { throw new MeasurementError("Data class not initialized"); } tcpTest[index] = rtt; } /** * Given an interpacket interval and a round trip time from a DNS test, store that value. * * @param index Index into measurement result array, corresponding to an interpacket interval * @param rtt Time for the DNS test to complete, in milliseconds * @throws MeasurementError */ public void setDns(int index, int rtt) throws MeasurementError { if (!runUpperLayerTests) { throw new MeasurementError("Data class not initialized"); } dnsTest[index] = rtt; } } /** * Stores data from the tests on the effect of packet size on RRC state related delays * * @author sanae@umich.edu (Sanae Rosen) */ public static class RrcSizeTestData { int time; // Interval between packets int size; // Size of packets sent long result; // Round trip time, in milliseconds long testId; // A unique id associated with a set of measurements /** * * @param time The inter-packet interval * @param size The packet size, in bytes * @param result The round trip time for that packet size and inter-packet interval * @param testId The unique id for this set of tests */ public RrcSizeTestData(int time, int size, long result, long testId) { this.time = time; this.size = size; this.result = result; this.testId = testId; } public JSONObject toJSON(String networktype, String phoneId) throws JSONException { JSONObject entry = new JSONObject(); entry.put("network_type", networktype); entry.put("phone_id", phoneId); entry.put("test_id", testId); entry.put("time_delay", time); entry.put("size", size); entry.put("result", result); return entry; } } /** * Store the results of our RRC test. * * Data is stored as a list of results indexed by the test number. Tests are performed in order * with increasing inter-packet intervals and results and data about the tests are stored here. * * @author sanae@umich.edu (Sanae Rosen) * */ public static class RRCTestData { // Round-trip times, in ms int[] rttsSmall; int[] rttsLarge; // Packets lost for each test int[] packetsLostSmall; int[] packetsLostLarge; // Signal strengths at time of each test int[] signalStrengthSmall; int[] signalStrengthLarge; // Error Counts from each test int[] errorCountSmall; int[] errorCountLarge; ArrayList<RrcSizeTestData> packetSizes; // Unique incrementing value that identifies this set of tests. long testId; /** * @param size Number of tests to run (each corresponding to an interpacket interval, * increasing in increments of 500 ms) * @param testId Unique ID for this set ot tests */ public RRCTestData(int size, long testId) { size = size + 1; rttsSmall = new int[size]; rttsLarge = new int[size]; packetsLostSmall = new int[size]; packetsLostLarge = new int[size]; signalStrengthSmall = new int[size]; signalStrengthLarge = new int[size]; errorCountLarge = new int[size]; errorCountSmall = new int[size]; this.testId = testId; packetSizes = new ArrayList<RrcSizeTestData>(); // Set default values for (int i = 0; i < rttsSmall.length; i++) { // 7000 is the cutoff for timeouts. // This makes the model-building script treat no data and timeouts the same. rttsSmall[i] = 7000; rttsLarge[i] = 7000; packetsLostSmall[i] = -1; packetsLostLarge[i] = -1; signalStrengthSmall[i] = -1; signalStrengthLarge[i] = -1; errorCountSmall[i] = -1; errorCountLarge[i] = -1; } } public long testId() { return testId; } public String[] toJSON(String networktype, String phoneId) { String[] returnval = new String[rttsSmall.length]; try { for (int i = 0; i < rttsSmall.length; i++) { JSONObject subtest = new JSONObject(); subtest.put("rtt_low", rttsSmall[i]); subtest.put("rtt_high", rttsLarge[i]); subtest.put("lost_low", packetsLostSmall[i]); subtest.put("lost_high", packetsLostLarge[i]); subtest.put("signal_low", signalStrengthSmall[i]); subtest.put("signal_high", signalStrengthLarge[i]); subtest.put("error_low", errorCountSmall[i]); subtest.put("error_high", errorCountLarge[i]); subtest.put("network_type", networktype); subtest.put("time_delay", i); subtest.put("test_id", testId); subtest.put("phone_id", phoneId); returnval[i] = subtest.toString(); Logger.w("Test ID for rrc inference test was " + this.testId); } } catch (JSONException e) { Logger.e("Error converting RRC data to JSON"); } return returnval; } /** * Erase all data corresponding to a set of results with a particular interpacket interval * @param index Index into the array of results. */ public void deleteItem(int index) { rttsSmall[index] = -1; rttsLarge[index] = -1; packetsLostSmall[index] = -1; packetsLostLarge[index] = -1; signalStrengthSmall[index] = -1; signalStrengthLarge[index] = -1; errorCountSmall[index] = -1; errorCountLarge[index] = -1; } /** * Save the data resulting from a given test. * * @param index Index into the array of results. * @param rttMax Round trip time for large packets (generally 1KB) * @param rttMin Round time time for small packets (generally empty) * @param numPacketsLostMax Number of packets lost for the set of large packets, out of 10 * @param numPacketsLostMin Likewise, for the small packets * @param errorHigh Error count for the set of large packets * @param errorLow Likewise, for the small packets * @param signalHigh The signal strength when sending the large packets * @param signalLow Likewise, for the small packets */ public void updateAll(int index, int rttMax, int rttMin, int numPacketsLostMax, int numPacketsLostMin, int errorHigh, int errorLow, int signalHigh, int signalLow) { this.rttsLarge[index] = (int) rttMax; this.rttsSmall[index] = (int) rttMin; this.packetsLostLarge[index] = numPacketsLostMax; this.packetsLostSmall[index] = numPacketsLostMin; this.errorCountLarge[index] = errorHigh; this.errorCountSmall[index] = errorLow; this.signalStrengthLarge[index] = signalHigh; this.signalStrengthSmall[index] = signalLow; } public void setRrcSizeTestData(int index, int size, long result, long testId) throws MeasurementError { packetSizes.add(new RrcSizeTestData(index, size, result, testId)); } public String[] sizeDataToJSON(String networkType, String phoneId) { String[] returnval = new String[packetSizes.size()]; try { for (int i = 0; i < packetSizes.size(); i++) { returnval[i] = packetSizes.get(i).toJSON(networkType, phoneId).toString(); } } catch (JSONException e) { Logger.e("Error converting RRC data to JSON"); } return returnval; } } /** * Class for tracking if there has been interfering traffic. * * We need to know how many packets are expected. We can then use the global packet counters to * see if more packets are sent than expected. * * @author sanae@umich.edu (Sanae Rosen) * */ public static class PacketMonitor { private long[] packetsFirst; private long[] packetsLast; boolean bySize = false; /** * Initialize immediately before use. Values are time-sensitive. */ PacketMonitor() { readCurrentPacketValues(); } void setBySize() { bySize = true; } void readCurrentPacketValues() { packetsFirst = getPacketsSent(); } /** * Call this to determine if packets have been sent since initializing. * * @param expectedRcv Number of packets expected to be received by the device since * initialization * @param expectedSent Number of packets expected to be sent by the device since * initialization * @return Whether or not there is interfering traffic */ boolean isTrafficInterfering(int expectedRcv, int expectedSent) { packetsLast = getPacketsSent(); long rcvPackets = (packetsLast[0] - packetsFirst[0]); long sentPackets = (packetsLast[1] - packetsFirst[1]); if (rcvPackets <= expectedRcv && sentPackets <= expectedSent) { Logger.d("No competing traffic, continue"); return false; } Logger.d("Competing traffic, retry"); return true; } /** * Determine how many packets, so far, have been sent (the contents of /proc/net/dev/). This is * a global value. We use this to determine if any other app anywhere on the phone may have sent * interfering traffic that might have changed the RRC state without our knowledge. * * @return Two values: number of bytes or packets received at index 0, followed by the number * sent at index 1. */ public long[] getPacketsSent() { long[] retval = { -1, -1 }; if (bySize) { retval[0] = TrafficStats.getMobileRxBytes(); retval[1] = TrafficStats.getMobileTxBytes(); } else { retval[0] = TrafficStats.getMobileRxPackets(); retval[1] = TrafficStats.getMobileTxPackets(); } return retval; } /** * The total traffic sent and received since getPacketsSent was run. * * @return The total packets (or data) sent and received, as a sum */ public long getPacketsSentDiff() { packetsLast = getPacketsSent(); long rcvPackets = (packetsLast[0] - packetsFirst[0]); long sentPackets = (packetsLast[1] - packetsFirst[1]); return rcvPackets + sentPackets; } } @SuppressWarnings("rawtypes") public static Class getDescClass() throws InvalidClassException { return RRCDesc.class; } public RRCTask(MeasurementDesc desc, Context parent) { super(new RRCDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.parameters), parent); context = parent; } @Override public MeasurementTask clone() { MeasurementDesc desc = this.measurementDesc; RRCDesc newDesc = new RRCDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.parameters); return new RRCTask(newDesc, parent); } @Override public MeasurementResult call() throws MeasurementError { RRCDesc desc = runInferenceTests(); return constructResultStandard(desc); } @Override public String getDescriptor() { return DESCRIPTOR; } @Override public String getType() { return RRCTask.TYPE; } @Override public void stop() { stop = true; } /** * Helper function to construct MeasurementResults to submit to the server * * @param desc The data collected for the RRC test to be converted into something understandable * by the server. * @return A data structure that can be sent to the server. Contains only the results of the * TCP, DNS and HTTP tests: the others are sent via a separate mechanism as they are stored * separately. */ private MeasurementResult constructResultStandard(RRCDesc desc) { PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); boolean success = true; MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, phoneUtils.getDeviceProperty(), RRCTask.TYPE, System.currentTimeMillis() * 1000, success, this.measurementDesc); if (desc.runUpperLayerTests) { result = desc.getResults(result); } Logger.i(MeasurementJsonConvertor.toJsonString(result)); return result; } /** * The core RRC inference functionality is in this function. The steps involved can be summarized * as follows: * <ol> * <li> Fetch the last model generated by the server, if it exists.</li> * <li> Check we are on a cellular network, otherwise abort. </li> * <li> Inform all other tasks that they should delay network traffic until later. </li> * <li> For every upper layer test, if upper layer tests and that test specifically are enabled, * run that test.</li> * </ol> * @return A data structure containing all the results and metadata surrounding the RRC inference tests * performed. * @throws MeasurementError */ private RRCDesc runInferenceTests() throws MeasurementError { data_consumed = 0; Checkin checkin = new Checkin(context); // Fetch the existing model from the server, if it exists RRCDesc desc = (RRCDesc) measurementDesc; desc.testId = getTestId(context); Logger.w("Test ID set to " + desc.testId); PhoneUtils utils = PhoneUtils.getPhoneUtils(); desc.initializeExtraTaskResults(desc.times.length); // Check to make sure we are on a valid (i.e. cellular) network if (utils.getNetwork() == "UNKNOWN" || utils.getNetwork() == "WIRELESS") { Logger.d("Returning: network is" + utils.getNetwork() + " rssi " + utils.getCurrentRssi()); return desc; } try { /* * Suspend all other tasks performed by the app as they can interfere. Although we have a * built-in check where we abort if traffic in the background interferes, in the past people * have scheduled other tests to be every 5 minutes, which can cause the RRC task to never * successfully complete without having to abort. */ RRCTrafficControl.PauseTraffic(); RRCTestData data = new RRCTestData(desc.size, desc.testId); // If the RRC task is enabled if (desc.RRC) { // Set up the connection to the echo server Logger.d("Active inference: about to begin"); Logger.d(desc.echoHost + ":" + desc.port); InetAddress serverAddr = InetAddress.getByName(desc.echoHost); // Perform the RRC timer and latency inference task Logger.d("Demotion inference: about to begin"); desc = inferDemotion(serverAddr, desc, data, utils); Logger.d("About to save data"); this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, 40); try { Logger.w("RRC: update the model on the GAE datastore"); checkin.uploadRrcInferenceData(data); Logger.d("Saving data complete"); } catch (IOException e) { e.printStackTrace(); Logger.e("Data not saved: " + e.getMessage()); } } // Check if the upper layer tasks are enabled if (desc.runUpperLayerTests) { if (desc.DNS) { Logger.w("Start DNS task"); // Test the dependence of DNS latency on the RRC state, using // the previously constructed model if available. runDnsTest(desc.times, desc); } this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, 60); if (desc.TCP) { Logger.w("Start TCP task"); // Test the dependence of TCP latency on the RRC state. runTCPHandshakeTest(desc.times, desc); } this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, 80); if (desc.HTTP) { Logger.w("Start HTTP task"); // Test the dependence of HTTP latency on the RRC state. runHTTPTest(desc.times, desc); } if (desc.SIZES) { Logger.w("Start size dependence task"); runSizeThresholdTest(desc.times, desc, data, desc.testId); checkin.uploadRrcInferenceSizeData(data); } } this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, 100); } catch (UnknownHostException e) { e.printStackTrace(); } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } finally { RRCTrafficControl.UnPauseTraffic(); } return desc; } /** * Determines the packet size dependence of the RRC task. * * Given a specified list of inter-packet intervals, perform the RRC inference measurement for * packets of sizes incremented by the size granularity specified. * * @param times Inter-packet intervals to test * @param desc Parameters for the RRC inference task * @param data Stores results of the RRC inference task * @param testId A unique ID identifying this set of tests */ private void runSizeThresholdTest(final Integer[] times, RRCDesc desc, RRCTestData data, long testId) { InetAddress serverAddr; try { serverAddr = InetAddress.getByName(desc.echoHost); } catch (UnknownHostException e) { Logger.e("Invalid or unreachable echo host. Test aborted."); e.printStackTrace(); return; } for (int i = 0; i < times.length; i++) { for (int j = desc.sizeGranularity; j <= 1024; j += desc.sizeGranularity) { try { long result = inferDemotionPacketSize(serverAddr, times[i], desc, j); data.setRrcSizeTestData(times[i], j, result, testId); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (MeasurementError e) { e.printStackTrace(); } } } } /** * Due to problems with how we detect interfering traffic, this test does not always work and * results should be treated with caution. I am keeping this test around, though, as we can still * get data on how much HTTP handshakes vary in how long they take. * * The "times" are the inter-packet intervals at which to run the test. Ideally these should be * based on the model constructed by the server, a default assumed value is used in their absence. * * Based on the time it takes to load a response from the page. * * This test is not currently as accurate as the other tests, for reasons described below, and has * thus been disabled. * * @param times List of inter-packet intervals, in half-second increments, at which to run the * tests * @param desc Stores parameters for the RRC inference tests in general */ private void runHTTPTest(final Integer[] times, RRCDesc desc) { /* * Length of time it takes to request and read in a page. */ Logger.d("Active inference HTTP test: about to begin"); if (times.length != desc.httpTest.length) { desc.httpTest = new int[times.length]; } long startTime = 0; long endTime = 0; try { for (int i = 0; i < times.length; i++) { // We try until we reach a threshhold or until there is no // competing traffic. for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { // Sometimes the network can change in the middle of a test checkIfWifi(); if (stop) { return; } /* * We keep track of the packets sent at the beginning and end of the test so we can detect * if there is competing traffic anywhere on the phone. */ PacketMonitor packetMonitor; /* * We also keep track of the data consumed in order to remain within * the data limit */ PacketMonitor datamonitor = new PacketMonitor(); datamonitor.setBySize(); datamonitor.readCurrentPacketValues(); // Initiate the desired RRC state by sending a large enough packet // to go to DCH and waiting for the specified amount of time try { // Wait for 1 second. Give time for any extraneous data sending to complete waitTime(1, false); InetAddress serverAddr; serverAddr = InetAddress.getByName(desc.echoHost); sendPacket(serverAddr, desc.MAX, desc); packetMonitor = new PacketMonitor(); waitTime(times[i] * desc.GRANULARITY, true); } catch (InterruptedException e1) { e1.printStackTrace(); continue; } catch (UnknownHostException e) { e.printStackTrace(); continue; } catch (IOException e) { e.printStackTrace(); continue; } startTime = System.currentTimeMillis(); // Somewhat approximte: we can pick up packets sent by our request. // Our request seems to never send more than 24 packets when there is no interference. boolean success = !packetMonitor.isTrafficInterfering(3, 70); HttpClient client = new DefaultHttpClient(); HttpGet request = new HttpGet(); request.setURI(new URI("http://" + desc.target + "?dummy=" + i + "" + j)); HttpResponse response = client.execute(request); endTime = System.currentTimeMillis(); BufferedReader in = null; in = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); StringBuffer sb = new StringBuffer(""); String line = ""; while ((line = in.readLine()) != null) { sb.append(line + "\n"); } in.close(); // This may overestimate the data consumed, but there's no good way // to tell what was us and what was another app data_consumed += datamonitor.getPacketsSentDiff(); if (success) { break; } startTime = 0; endTime = 0; } long rtt = endTime - startTime; try { desc.setHttp(i, (int) rtt); } catch (MeasurementError e) { e.printStackTrace(); } Logger.d("Time for Http" + rtt); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (URISyntaxException e) { e.printStackTrace(); } } /** * The "times" are the inter-packet intervals at which to run the test. Ideally these should be * based on the model constructed by the server, a default assumed value is used in their absence. * * <ol> * <li> Send a packet to initiate the RRC state desired. </li> * <li>Create a randomly generated host name (to ensure that the host name is not cached). I * found on some devices that even when you clear the cache manually, the data remains in * the cache.</li> * <li> Time how long it took to look it up.</li> * <li> Count the total packets sent, globally on the phone. If more packets were sent than </li> * expected, abort and try again. </li> * <li> Otherwise, save the data for that test and move to the next inter-packet interval.</li> * </ol> * * Test is similar to the approach taken in DnsLookUpTask.java. * * @param times List of inter-packet intervals, in half-second increments, at which to run the * test * @param desc Stores parameters for the RRC inference tests in general * @throws MeasurementError */ public void runDnsTest(final Integer[] times, RRCDesc desc) throws MeasurementError { Logger.d("Active inference DNS test: about to begin"); if (times.length != desc.dnsTest.length) { desc.dnsTest = new int[times.length]; } long dataConsumedThisTask = 0; long startTime = 0; long endTime = 0; // For each inter-packet interval... for (int i = 0; i < times.length; i++) { // On a failure, try again until a threshold is reached. for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { // Sometimes the network can change in the middle of a test checkIfWifi(); if (stop) { return; } /* * We keep track of the packets sent at the beginning and end of the test so we can detect * if there is competing traffic anywhere on the phone. */ PacketMonitor packetMonitor = new PacketMonitor(); // Initiate the desired RRC state by sending a large enough packet // to go to DCH and waiting for the specified amount of time try { InetAddress serverAddr; serverAddr = InetAddress.getByName(desc.echoHost); sendPacket(serverAddr, desc.MAX, desc); waitTime(times[i] * desc.GRANULARITY, true); } catch (InterruptedException e1) { e1.printStackTrace(); continue; } catch (UnknownHostException e) { e.printStackTrace(); continue; } catch (IOException e) { e.printStackTrace(); continue; } // Create a random URL, to avoid the caching problem UUID uuid = UUID.randomUUID(); String host = uuid.toString() + ".com"; // Start measuring the time to complete the task startTime = System.currentTimeMillis(); try { @SuppressWarnings("unused") InetAddress serverAddr = InetAddress.getByName(host); } catch (UnknownHostException e) { // we do this on purpose! Since it's a fake URL the lookup will fail } dataConsumedThisTask += DnsLookupTask.AVG_DATA_USAGE_BYTE; // When we fail to find the URL, we stop timing endTime = System.currentTimeMillis(); // Check how many packets were sent again. If the expected number // of packets were sent, we can finish and go to the next task. // Otherwise, we have to try again. if (!packetMonitor.isTrafficInterfering(5, 5)) { break; } startTime = 0; endTime = 0; } // If we broke out of the try-again loop, the last set of results are // valid and we can save them. long rtt = endTime - startTime; try { desc.setDns(i, (int) rtt); } catch (MeasurementError e) { e.printStackTrace(); } Logger.d("Time for DNS" + rtt); } incrementData(dataConsumedThisTask); } /** * Time how long it takes to do a TCP 3-way handshake, starting from the induced RRC state. * * <ol> * <li> Send a packet to initiate the RRC state desired. </li> * <li> Open a TCP connection to the echo host server. </li> * <li> Time how long it took to look it up. </li> * <li> Count the total packets sent, globally on the phone. If more packets were sent than * expected, abort and try again. </li> * <li> Otherwise, save the data for that test and move to the next inter-packet interval. * </ol> * * @param times List of inter-packet intervals, in half-second increments, at which to run the * test * @param desc Stores parameters for the RRC inference tests in general */ public void runTCPHandshakeTest(final Integer[] times, RRCDesc desc) { Logger.d("Active inference TCP test: about to begin"); if (times.length != desc.tcpTest.length) { desc.tcpTest = new int[times.length]; } long startTime = 0; long endTime = 0; long dataConsumedThisTask = 0; try { // For each inter-packet interval... for (int i = 0; i < times.length; i++) { // On a failure, try again until a threshhold is reached. for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { checkIfWifi(); if (stop) { return; } PacketMonitor packetMonitor = new PacketMonitor(); // Induce DCH then wait for specified time InetAddress serverAddr; serverAddr = InetAddress.getByName(desc.echoHost); sendPacket(serverAddr, desc.MAX, desc); waitTime(times[i] * 500, true); // begin test. We test the time to do a 3-way handshake only. startTime = System.currentTimeMillis(); serverAddr = InetAddress.getByName(desc.target); // three-way handshake done when socket created Socket socket = new Socket(serverAddr, 80); endTime = System.currentTimeMillis(); // Not exact, but also a smallish task... dataConsumedThisTask += DnsLookupTask.AVG_DATA_USAGE_BYTE; // Check how many packets were sent again. If the expected number // of packets were sent, we can finish and go to the next task. // Otherwise, we have to try again. if (!packetMonitor.isTrafficInterfering(5, 4)) { socket.close(); break; } startTime = 0; endTime = 0; socket.close(); } long rtt = endTime - startTime; try { desc.setTcp(i, (int) rtt); } catch (MeasurementError e) { e.printStackTrace(); } Logger.d("Time for TCP" + rtt); } } catch (InterruptedException e1) { e1.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } incrementData(dataConsumedThisTask); } /** * * For all time intervals specified, go through and perform the RRC inference test. * * The way this test works, at a high level, is: * <ol> * <li> Send a large packet to induce DCH (or the equivalent) </li> * <li> Wait for an amount of time, t </li> * <li> Send a large packet and measure the round-trip time </li> * <li> Wait for another amount of time, t </li> * <li> Send a small packet and measure the round-trip time 6. Repeat for all specified values * of t. These start at 0 and increase by GRANULARITY, "size" times. </li> * </ol> * * The size of "small" and "large" packets are defined in the parameters. We observe the total * packets sent to make sure there is no interfering traffic. * * Packets are UDP packets. * * From this, we can infer the timers associated with RRC states. By sending a large packet, we * induce the highest power state. Waiting a number of seconds afterwards allows us to demote to * the next state. Sending a packet and observing the RTT allows us to infer if a state promotion * had to take place. * * FACH is characterized by different state promotion times for large and small packets. * * @param serverAddr Address of the echo server * @param desc Stores the parameters for the RRC tests * @param data Stores the results of the RRC tests * @param utils For fetching the signal strength when the test is performed * @return The parameters for the RRC tests * @throws InterruptedException * @throws IOException */ private RRCDesc inferDemotion(InetAddress serverAddr, RRCDesc desc, RRCTestData data, PhoneUtils utils) throws InterruptedException, IOException { Logger.d("Demotion basic test"); for (int i = 0; i <= desc.size; i++) { this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, (int) (100 * i / (desc.size))); checkIfWifi(); if (stop) { return desc; } inferDemotionHelper(serverAddr, i, data, desc, utils); Logger.d("Finished demotion test with length" + i); // Note that we scale from 0-90 to save some stuff for upper layer tests. // If we wanted to really do this properly we could scale // according to how long each task should take. this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, (int) (90 * i / desc.size)); } return desc; } @Override public String toString() { RRCDesc desc = (RRCDesc) measurementDesc; return "[RRC]\n Echo Server: " + desc.echoHost + "\n Target: " + desc.target + "\n Interval (sec): " + desc.intervalSec + "\n Next run: " + desc.startTime; } /********************************************************************* * UTILITIES * *********************************************************************/ /** * * Sleep for the amount of time indicated. * * @param timeToSleep Time for which we pause the current thread * @param useMs Toggles between units of milliseconds for the first parameter (true) and * seconds(false). * @throws InterruptedException */ public static void waitTime(int timeToSleep, boolean useMs) throws InterruptedException { Logger.d("Wait for n ms: " + timeToSleep); if (!useMs) { timeToSleep = timeToSleep * 1000; } Thread.sleep(timeToSleep); } /** * Sends a bunch of UDP packets of the size indicated and wait for the response. * * Counts how long it takes for all the packets to return. PAckets are currently not labelled: the * total time is the time for the first packet to leave until the last packet arrives. AFter 7000 * ms it is assumed packets are lost and the socket times out. In that case, the number of packets * lost is recorded. * * @param serverAddr server to which to send the packets * @param size size of the packets * @param num number of packets to send * @param packetSize size of the packets sent * @param port port to send the packets to * @return first value: the amount of time to send all packets and get a response. second value: * number of packets lost, on a timeout. * @throws IOException */ public static long[] sendMultiPackets(InetAddress serverAddr, int size, int num, int packetSize, int port) throws IOException { long startTime = 0; long endTime = 0; byte[] buf = new byte[size]; byte[] rcvBuf = new byte[packetSize]; long[] retval = { -1, -1 }; long numLost = 0; int i = 0; long dataConsumedThisTask = 0; DatagramSocket socket = new DatagramSocket(); DatagramPacket packetRcv = new DatagramPacket(rcvBuf, rcvBuf.length); DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddr, port); // number * (packet sent + packet received) dataConsumedThisTask += num * (size + packetSize); try { socket.setSoTimeout(7000); startTime = System.currentTimeMillis(); Logger.d("Sending packet, waiting for response "); for (i = 0; i < num; i++) { socket.send(packet); } for (i = 0; i < num; i++) { socket.receive(packetRcv); if (i == 0) { endTime = System.currentTimeMillis(); } } } catch (SocketTimeoutException e) { Logger.d("Timed out"); numLost += (num - i); socket.close(); } Logger.d("Sending complete: " + endTime); retval[0] = endTime - startTime; retval[1] = numLost; incrementData(dataConsumedThisTask); return retval; } /** * Helper function that sends a single packet and receives an empty packet back. * @param serverAddr Echo server to calculate round trip * @param size size of packet to send in bytes * @param desc Holds parameters for the RRC inference task * @return The round trip time for the packet * @throws IOException */ private static long sendPacket(InetAddress serverAddr, int size, RRCDesc desc) throws IOException { return sendPacket(serverAddr, size, desc.MIN, desc.port); } /** * Send a single packet of the size indicated and wait for a response. * * After 7000 ms, time out and return a value of -1 (meaning no response). Otherwise, return the * time from when the packet was sent to when a response was returned by the echo server. * * @param serverAddr Echo server to calculate round trip * @param size size of packet to send in bytes * @param rcvSize size of packets sent from the echo server * @param port where the echo server is listening * @return The round trip time for the packet * @throws IOException */ public static long sendPacket(InetAddress serverAddr, int size, int rcvSize, int port) throws IOException { long startTime = 0; byte[] buf = new byte[size]; byte[] rcvBuf = new byte[rcvSize]; long dataConsumedThisTask = 0; DatagramSocket socket = new DatagramSocket(); DatagramPacket packetRcv = new DatagramPacket(rcvBuf, rcvBuf.length); dataConsumedThisTask += (size + rcvSize); DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddr, port); try { socket.setSoTimeout(7000); startTime = System.currentTimeMillis(); Logger.d("Sending packet, waiting for response "); socket.send(packet); socket.receive(packetRcv); } catch (SocketTimeoutException e) { Logger.d("Timed out, trying again"); socket.close(); return -1; } long endTime = System.currentTimeMillis(); Logger.d("Sending complete: " + endTime); incrementData(dataConsumedThisTask); return endTime - startTime; } /** * Performs a single RRC inference test to account for packet sizes. * * Sends a packet, waits for the specified length of time, then sends a cluster of packets of * the specified size. * * @param serverAddr Echo server to calculate round trip * @param wait Time to wait between packets, in milliseconds. * @param desc Holds parameters for the RRC inference task * @param size Size, in bytes, of the packet to send. * @return The amount of time to send all packets and get a response. * @throws IOException * @throws InterruptedException */ public static long inferDemotionPacketSize(InetAddress serverAddr, int wait, RRCDesc desc, int size) throws IOException, InterruptedException { long retval = -1; for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { Logger.d("Active inference: determine packet size, size " + size + " interval " + wait); // Induce the highest power state sendPacket(serverAddr, desc.MAX, desc.MIN, desc.port); // WAit for the specified amount of time waitTime(wait * desc.GRANULARITY, true); // Send the specified packet size long[] rtts = sendMultiPackets(serverAddr, size, 1, desc.MIN, desc.port); long rttPacket = rtts[0]; PacketMonitor packetMonitor = new PacketMonitor(); if (!packetMonitor.isTrafficInterfering(3, 3)) { retval = rttPacket; break; } } return retval; } private long[] inferDemotionHelper(InetAddress serverAddr, int wait, RRCTestData data, RRCDesc desc, PhoneUtils utils) throws IOException, InterruptedException { return inferDemotionHelper(serverAddr, wait, data, desc, wait, utils); } /** * One component of the RRC inference task. * * <ol> * <li> Induce the highest-power RRC state by sending a large packet. </li> * <li> Wait the indicated number of seconds. </li> * <li> Send a series of 10 large packets at once. Measure: a) Time for all packets to be * echoed back b) number of packets lost, if any c) associated signal strength d) error rate * is currently not implemented. </li> * <li> Check if the expected number of packets were sent while performing a test. If too many * packets were sent, abort. </li> * </ol> * * @param serverAddr Echo server to calculate round trip * @param wait Time in milliseconds to pause between packets sent * @param data Stores the results of the RRC inference tests * @param desc Stores parameters for the RRC inference tests * @param index Index of the current test, corresponds to the inter-packet time in intervals of * half milliseconds * @param utils Used to retrieve the phone's RSSI at the time of collecting the data * @return first value: the amount of time to send all small packets and get a response. * Second value: time to send all large packets and get a response. * @throws IOException * @throws InterruptedException */ public static long[] inferDemotionHelper(InetAddress serverAddr, int wait, RRCTestData data, RRCDesc desc, int index, PhoneUtils utils) throws IOException, InterruptedException { /** * Once we generalize the RRC state inference problem, this is what we will use (since in * general, RRC state can differ between large and small packets). Gives the RTT for a large * packet and a small packet for a given time after inducing DCH state. Granularity currently * half-seconds but can easily be increased. * * Measures packets sent before and after to make sure no extra packets were sent, and retries * on a failure. Also checks that there was no timeout and retries on a failure. */ long rttLargePacket = -1; long rttSmallPacket = -1; int packetsLostSmall = 0; int packetsLostLarge = 0; int errorCountLarge = 0; int errorCountSmall = 0; int signalStrengthLarge = 0; int signalStrengthSmall = 0; for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { Logger.d("Active inference: about to begin helper"); PacketMonitor packetMonitor = new PacketMonitor(); // Induce the highest power state sendPacket(serverAddr, desc.MAX, desc.MIN, desc.port); // WAit for the specified amount of time waitTime(wait * desc.GRANULARITY, true); // Send a bunch of large packets, all at once, and take measurements on the result signalStrengthLarge = utils.getCurrentRssi(); long[] retval = sendMultiPackets(serverAddr, desc.MAX, 10, desc.MIN, desc.port); packetsLostSmall = (int) retval[1]; rttLargePacket = retval[0]; // wait for the specified amount of time waitTime(wait * desc.GRANULARITY, true); // Send a bunch of small packets, all at once, and take measurements on the result signalStrengthSmall = utils.getCurrentRssi(); retval = sendMultiPackets(serverAddr, desc.MIN, 10, desc.MIN, desc.port); packetsLostLarge = (int) retval[1]; rttSmallPacket = retval[0]; if (!packetMonitor.isTrafficInterfering(21, 21)) { break; } Logger.d("Try again."); rttLargePacket = -1; rttSmallPacket = -1; packetsLostSmall = 0; packetsLostLarge = 0; errorCountLarge = 0; errorCountSmall = 0; signalStrengthLarge = 0; signalStrengthSmall = 0; } Logger.d("3G demotion, lower bound: rtts are:" + rttLargePacket + " " + rttSmallPacket + " " + packetsLostSmall + " " + packetsLostLarge); long[] retval = { rttLargePacket, rttSmallPacket }; data.updateAll(index, (int) rttLargePacket, (int) rttSmallPacket, packetsLostSmall, packetsLostLarge, errorCountLarge, errorCountSmall, signalStrengthLarge, signalStrengthSmall); return retval; } /** * Keep a global counter that labels each test with a unique, increasing integer. * * @param context Any context instance, needed to fetch the last test id from permanent storage * @return The unique (for this device) test ID generated. */ public static synchronized int getTestId(Context context) { SharedPreferences prefs = context.getSharedPreferences("test_ids", Context.MODE_PRIVATE); int testid = prefs.getInt("test_id", 0) + 1; SharedPreferences.Editor editor = prefs.edit(); editor.putInt("test_id", testid); editor.commit(); return testid; } /** * If on wifi, suspend the task until we go back to the cellular network. Use exponential back-off * to calculate the wait time, with a limit of 500 s. Once the limit is reached, unpause other * tasks. Repause traffic if we are good to resume again. * */ public void checkIfWifi() { PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); int timeToWait = 10; while (true) { if (phoneUtils.getNetwork() != PhoneUtils.NETWORK_WIFI) { RRCTrafficControl.PauseTraffic(); return; } if (stop) { return; } if (timeToWait < 60) { RRCTrafficControl.UnPauseTraffic(); } Logger.d("RRCTask: on Wifi, try again later: " + phoneUtils.getNetwork()); if (timeToWait < 500) { // 500s, or a bit over 8 minutes. timeToWait = timeToWait * 2; } else { // if it's taking a while, stop pausing traffic } try { waitTime(timeToWait, false); } catch (InterruptedException e) { e.printStackTrace(); } } } private synchronized static void incrementData(long data_increment) { data_consumed += data_increment; } /** * For RRC inference, we calculate this precisely based on the number and size * of packets sent. For the TCP handshake and DNS tasks, we use small, fixed * values based on the average data consumption measured for those tasks. * * For the HTTP task, we count <i>all</i> data sent during the task time towards * the budget. This will tend to overestimate the data used, but due to * retransmissions, etc, it is impossible to get a remotely accurate estimate * otherwise, I found. */ @Override public long getDataConsumed() { return data_consumed; } }