//Copyright 2012 RobustNet Lab, University of Michigan. All Rights Reserved. package com.mobilyzer.measurements; import java.io.IOException; import java.io.InvalidClassException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Date; import java.util.Map; import java.util.Random; import com.mobilyzer.MeasurementDesc; import com.mobilyzer.MeasurementResult; import com.mobilyzer.MeasurementTask; import com.mobilyzer.MeasurementResult.TaskProgress; import com.mobilyzer.exceptions.MeasurementError; import com.mobilyzer.util.Logger; import com.mobilyzer.util.MLabNS; import com.mobilyzer.util.MeasurementJsonConvertor; import com.mobilyzer.util.PhoneUtils; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; /** * @author Haokun Luo * * TCP Throughput is a measurement task for cellular network throughput. * 1. Uplink: the mobile device continuously sends packets with consistent * packet size. We send packets for a fixed amount of time, and we sample * each throughput value at a smaller period. We use the median of all the * sampling result as the final measurement result. The result is calculated * at the server side, and send back to the device. * 2. Downlink: similar methodology as uplink. Only difference is that the * device is receiving packets from the server, and calculate the result * locally. */ public class TCPThroughputTask extends MeasurementTask { // default constant here public static final String DESCRIPTOR = "TCP Speed Test"; public static final int PORT_DOWNLINK = 6001; public static final int PORT_UPLINK = 6002; public static final int PORT_CONFIG = 6003; public static final String TYPE = "tcpthroughput"; // Timing related public final int BUFFER_SIZE = 5000; public static final long DURATION_IN_SEC = 15; public final int KSEC = 1000; public static final long SAMPLE_PERIOD_IN_SEC = 1; public static final long SLOW_START_PERIOD_IN_SEC = 5; public static final int TCP_TIMEOUT_IN_SEC = 30; // largest non-fragment packet size in LTE (uplink) public static final int THROUGHPUT_UP_PKT_SIZE_MAX = 1357; public static final int THROUGHPUT_UP_PKT_SIZE_MIN = 700; // Data related private final int KBYTE = 1024; private static final int DATA_LIMIT_MB_UP = 5; private static final int DATA_LIMIT_MB_DOWN = 10; private boolean DATA_LIMIT_ON = true; private boolean DATA_LIMIT_EXCEEDED = false; private static final String UPLINK_FINISH_MSG = "*"; private Context context = null; // helper variables private int accumulativeSize = 0; private Random randStr = new Random(); private ArrayList<Double> samplingResults = new ArrayList<Double>(); //start time of each sampling period private long startSampleTime = 0; private String serverVersion = ""; private long taskStartTime = 0; private double taskDuration = 0; //uplink accumulative data private int totalSendSize = 0; // downlink accumulative data private int totalRevSize = 0; private long duration; private TaskProgress taskProgress; private volatile boolean stopFlag; // class constructor public TCPThroughputTask(MeasurementDesc desc) { super(new TCPThroughputDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); this.taskProgress=TaskProgress.FAILED; this.stopFlag=false; this.duration=(long)(this.KSEC* ((TCPThroughputDesc)measurementDesc).duration_period_sec + ((TCPThroughputDesc)measurementDesc).slow_start_period_sec); Logger.i("Create new throughput task"); } protected TCPThroughputTask(Parcel in) { super(in); taskProgress = (TaskProgress)in.readSerializable(); stopFlag = in.readByte() != 0; duration = in.readLong(); } public static final Parcelable.Creator<TCPThroughputTask> CREATOR = new Parcelable.Creator<TCPThroughputTask>() { public TCPThroughputTask createFromParcel(Parcel in) { return new TCPThroughputTask(in); } public TCPThroughputTask[] newArray(int size) { return new TCPThroughputTask[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeSerializable(taskProgress); dest.writeByte((byte) (stopFlag ? 1 : 0)); dest.writeLong(duration); } /** * There are seven parameters specifically for this experiment: * 1. data_limit_mb_up: uplink cellular network data limit * 2. data_limit_mb_down: downlink cellular network data limit * 3. duration_period_sec : downlink maximum experiment duration period * 4. pkt_size_up_bytes: the size each packet in the uplink * 5. sample_period_sec : the small interval to calculate current throughput result * 6. slow_start_period_sec : waiting period to avoid TCP slow start * 7. tcp_timeout_sec: TCP connection timeout */ public static class TCPThroughputDesc extends MeasurementDesc { // declared parameters public double data_limit_mb_up = TCPThroughputTask.DATA_LIMIT_MB_UP; public double data_limit_mb_down = TCPThroughputTask.DATA_LIMIT_MB_DOWN; public boolean dir_up = false; public double duration_period_sec = TCPThroughputTask.DURATION_IN_SEC; public int pkt_size_up_bytes = TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MAX; public double sample_period_sec = TCPThroughputTask.SAMPLE_PERIOD_IN_SEC; public double slow_start_period_sec = TCPThroughputTask.SLOW_START_PERIOD_IN_SEC; public String target = null; public double tcp_timeout_sec = TCPThroughputTask.TCP_TIMEOUT_IN_SEC; public TCPThroughputDesc(String key, Date startTime, Date endTime, double intervalSec, long count, long priority, int contextIntervalSec, Map<String, String> params) throws InvalidParameterException { super(TCPThroughputTask.TYPE, key, startTime, endTime, intervalSec, count, priority, contextIntervalSec, params); initializeParams(params); if (this.target == null || this.target.length() == 0) { throw new InvalidParameterException("TCPThroughputTask null target"); } } protected TCPThroughputDesc(Parcel in) { super(in); data_limit_mb_up = in.readDouble(); data_limit_mb_down = in.readDouble(); dir_up = in.readByte() != 0; duration_period_sec = in.readDouble(); pkt_size_up_bytes = in.readInt(); sample_period_sec = in.readDouble(); slow_start_period_sec = in.readDouble(); target = in.readString(); tcp_timeout_sec = in.readDouble(); } public static final Parcelable.Creator<TCPThroughputDesc> CREATOR = new Parcelable.Creator<TCPThroughputDesc>() { public TCPThroughputDesc createFromParcel(Parcel in) { return new TCPThroughputDesc(in); } public TCPThroughputDesc[] newArray(int size) { return new TCPThroughputDesc[size]; } }; @Override public int describeContents() { return super.describeContents(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeDouble(data_limit_mb_up); dest.writeDouble(data_limit_mb_down); dest.writeByte((byte) (dir_up ? 1 : 0)); dest.writeDouble(duration_period_sec); dest.writeInt(pkt_size_up_bytes); dest.writeDouble(sample_period_sec); dest.writeDouble(slow_start_period_sec); dest.writeString(target); dest.writeDouble(tcp_timeout_sec); } @Override protected void initializeParams(Map<String, String> params) { if (params == null) { return; } if ( (target = params.get("target")) == null ) { target = MLabNS.TARGET; } try { String readVal = null; if ((readVal = params.get("data_limit_mb_down")) != null && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { this.data_limit_mb_down = Double.parseDouble(readVal); if (this.data_limit_mb_down > TCPThroughputTask.DATA_LIMIT_MB_DOWN) { this.data_limit_mb_down = TCPThroughputTask.DATA_LIMIT_MB_DOWN; } } if ((readVal = params.get("data_limit_mb_up")) != null && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { this.data_limit_mb_up = Double.parseDouble(readVal); if (this.data_limit_mb_up > TCPThroughputTask.DATA_LIMIT_MB_UP) { this.data_limit_mb_up = TCPThroughputTask.DATA_LIMIT_MB_UP; } } if ((readVal = params.get("duration_period_sec")) != null && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { this.duration_period_sec = Double.parseDouble(readVal); if (this.duration_period_sec > TCPThroughputTask.DURATION_IN_SEC) { this.duration_period_sec = TCPThroughputTask.DURATION_IN_SEC; } } if ((readVal = params.get("pkt_size_up_bytes")) != null && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { this.pkt_size_up_bytes = Integer.parseInt(readVal); if (this.pkt_size_up_bytes > TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MAX) { this.pkt_size_up_bytes = TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MAX; } if (this.pkt_size_up_bytes < TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MIN) { this.pkt_size_up_bytes = TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MIN; } } if ((readVal = params.get("sample_period_sec")) != null && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { this.sample_period_sec = Double.parseDouble(readVal); if (this.sample_period_sec > TCPThroughputTask.DURATION_IN_SEC/2) { this.sample_period_sec = TCPThroughputTask.DURATION_IN_SEC/2; } } if ((readVal = params.get("slow_start_period_sec")) != null && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { this.slow_start_period_sec = Double.parseDouble(readVal); if (this.slow_start_period_sec > TCPThroughputTask.DURATION_IN_SEC/2) { this.slow_start_period_sec = TCPThroughputTask.DURATION_IN_SEC/2; } } if ((readVal = params.get("tcp_timeout_sec")) != null && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { this.tcp_timeout_sec = Integer.parseInt(readVal)*1000; if (this.tcp_timeout_sec > TCPThroughputTask.TCP_TIMEOUT_IN_SEC) { this.tcp_timeout_sec = TCPThroughputTask.TCP_TIMEOUT_IN_SEC; } } } catch (NumberFormatException e) { throw new InvalidParameterException("TCP Throughput Task invalid parameters."); } String dir = null; if ((dir = params.get("dir_up")) != null && dir.length() > 0) { if (dir.compareTo("Up") == 0 || dir.compareTo("true") == 0) { this.dir_up = true; } } } @Override public String getType() { return TCPThroughputTask.TYPE; } /** * Find the median value from a TCPThroughput JSON result string (already sorted) * Suppose N is the number of results. If N is odd, we pick the result with index * (N-1)/2. If N is even, we take the mean value between index N/2 and N/2-1 * * @return -1 fail to create result * @return median value result */ public double calMedianSpeedFromTCPThroughputOutput(String outputInJSON) { if (outputInJSON == null || outputInJSON.equals("") || outputInJSON.equals("[]") || outputInJSON.charAt(0) != '[' || outputInJSON.charAt(outputInJSON.length()-1) != ']') { return -1; } String[] splitResult = outputInJSON.substring(1, outputInJSON.length()-1).split(","); int resultLen = splitResult.length; if (resultLen <= 0) return 0.0; double result = 0.0; if (resultLen % 2 == 0) { result = (Double.parseDouble(splitResult[resultLen / 2]) + Double.parseDouble(splitResult[resultLen / 2 - 1])) / 2; } else { result = Double.parseDouble(splitResult[(resultLen - 1) / 2]); } return result; } } /** * Make a deep cloning of the task */ @Override public MeasurementTask clone() { MeasurementDesc desc = this.measurementDesc; TCPThroughputDesc newDesc = new TCPThroughputDesc( desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters); return new TCPThroughputTask(newDesc); } @Override public String getType() { return TCPThroughputTask.TYPE; } @Override public String getDescriptor() { return TCPThroughputTask.DESCRIPTOR; } /** * This will be printed to the device log console. Make sure it's well * structured and human readable */ @Override public String toString() { TCPThroughputDesc desc = (TCPThroughputDesc) measurementDesc; String resp; if (desc.dir_up) { resp = "[TCP Uplink]\n"; } else { resp = "[TCP Downlink]\n"; } resp += " Target: " + desc.target + "\n Interval (sec): " + desc.intervalSec + "\n Next run: " + desc.startTime; return resp; } @SuppressWarnings("rawtypes") public static Class getDescClass() throws InvalidClassException { return TCPThroughputDesc.class; } @Override public MeasurementResult[] call() throws MeasurementError { this.taskProgress=TaskProgress.FAILED; TCPThroughputDesc desc = (TCPThroughputDesc)measurementDesc; // Apply MLabNS lookup to fetch FQDN if (!desc.target.equals(MLabNS.TARGET)) { Logger.i("Not using MLab server!"); throw new InvalidParameterException("Unknown target " + desc.target + " for TCPThroughput"); } try { ArrayList<String> mlabResult = MLabNS.Lookup(context, "mobiperf"); if (mlabResult.size() == 1) { desc.target = mlabResult.get(0); } else { throw new MeasurementError("Invalid MLabNS result"); } } catch (InvalidParameterException e) { throw new MeasurementError(e.getMessage()); } Logger.i("Setting target to: " + desc.target); PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); // reset the data limit if the phone is under Wifi if (phoneUtils.getNetwork().equals(phoneUtils.NETWORK_WIFI)) { Logger.i("Detect Wifi network"); this.DATA_LIMIT_ON = false; } Logger.i("Running TCPThroughput on " + desc.target); try { // fetch server information if (!acquireServerConfig()) { throw new MeasurementError("Fail to acquire server configuration"); } Logger.i("Server version is " + this.serverVersion); if (desc.dir_up == true) { uplink(); if(stopFlag){ throw new MeasurementError("Cancelled"); } Logger.i("Uplink measurement result is:"); } else { this.taskStartTime = System.currentTimeMillis(); downlink(); if(stopFlag){ throw new MeasurementError("Cancelled"); } Logger.i("Downlink measurement result is:"); } this.taskProgress=TaskProgress.COMPLETED; } catch (MeasurementError e) { throw e; } catch (IOException e) { Logger.e("Error close the socket for " + desc.type); throw new MeasurementError("Error close the socket for " + desc.type); } catch (InterruptedException e) { Logger.e("Interrupted captured"); throw new MeasurementError("Task gets interrrupted"); } MeasurementResult result = new MeasurementResult( phoneUtils.getDeviceInfo().deviceId, phoneUtils.getDeviceProperty(this.getKey()), TCPThroughputTask.TYPE, System.currentTimeMillis() * 1000, taskProgress, this.measurementDesc); // TODO (Haokun): add more results if necessary result.addResult("tcp_speed_results", this.samplingResults); result.addResult("data_limit_exceeded", this.DATA_LIMIT_EXCEEDED); result.addResult("duration", this.taskDuration); result.addResult("server_version", this.serverVersion); Logger.i(MeasurementJsonConvertor.toJsonString(result)); MeasurementResult[] mrArray= new MeasurementResult[1]; mrArray[0]=result; return mrArray; } /***************************************************************** * Core measurement functions definitions ***************************************************************** * acquire server configuration information * 1) m-lab slice version * * @return: true -- successful acquire data from M-Lab slice * @return: false -- failure to acquire data from M-Lab slice */ private boolean acquireServerConfig() throws MeasurementError, IOException, InterruptedException { Socket tcpSocket = null; InputStream iStream = null; boolean result = false; try { tcpSocket = new Socket(); buildUpSocket(tcpSocket, ((TCPThroughputDesc)measurementDesc).target, TCPThroughputTask.PORT_CONFIG); iStream = tcpSocket.getInputStream(); } catch (IOException e) { throw new MeasurementError("Error open uplink socket at " + ((TCPThroughputDesc)measurementDesc).target + " with port " + TCPThroughputTask.PORT_CONFIG); } try { // read from server side configuration byte [] resultMsg = new byte[this.BUFFER_SIZE]; int resultMsgLen = iStream.read(resultMsg, 0, resultMsg.length); if (resultMsgLen > 0) { // TODO (Haokun): Maybe switch to JSON for multiple acquired data // currently use one double number this.serverVersion = new String(resultMsg).substring(0, resultMsgLen); result = true; } } catch (IOException e) { throw new MeasurementError("Error to acquire configuration from " + ((TCPThroughputDesc)measurementDesc).target); } finally { iStream.close(); tcpSocket.close(); Logger.i("Close server Config socket"); } return result; } /* Uplink measurement task * @throws IOException * @throws InterruptedException */ private void uplink() throws MeasurementError, IOException, InterruptedException { Logger.i("Start uplink task on " + ((TCPThroughputDesc)measurementDesc).target); Socket tcpSocket = null; InputStream iStream = null; OutputStream oStream = null; try { tcpSocket = new Socket(); buildUpSocket(tcpSocket, ((TCPThroughputDesc)measurementDesc).target, TCPThroughputTask.PORT_UPLINK); oStream = tcpSocket.getOutputStream(); iStream = tcpSocket.getInputStream(); } catch (IOException e){ e.printStackTrace(); throw new MeasurementError("Error open uplink socket at " + ((TCPThroughputDesc)measurementDesc).target + " with port " + TCPThroughputTask.PORT_UPLINK); } long startTime = System.currentTimeMillis(); long endTime = startTime; int data_limit_byte_up = (int)(((TCPThroughputDesc)measurementDesc).data_limit_mb_up *this.KBYTE*this.KBYTE); byte[] uplinkBuffer = new byte[((TCPThroughputDesc)measurementDesc).pkt_size_up_bytes]; this.genRandomByteArray(uplinkBuffer); try { long totalDuration = (long)(this.KSEC* ((TCPThroughputDesc)measurementDesc).duration_period_sec + ((TCPThroughputDesc)measurementDesc).slow_start_period_sec); do { if(stopFlag){ throw new MeasurementError("Cancelled"); } oStream.write(uplinkBuffer, 0, uplinkBuffer.length); oStream.flush(); endTime = System.currentTimeMillis(); this.totalSendSize += ((TCPThroughputDesc)measurementDesc).pkt_size_up_bytes; if (this.DATA_LIMIT_ON && this.totalSendSize >= data_limit_byte_up) { Logger.i("Detect uplink exceeding limitation " + (double)((TCPThroughputDesc)measurementDesc).data_limit_mb_up + " MB"); this.DATA_LIMIT_EXCEEDED = true; break; } // propagate every quarter } while ((endTime - startTime) < totalDuration); // convert into seconds this.taskDuration = (double)(endTime - startTime) / 1000.0; Logger.i("Uplink total data comsumption is " + (double)this.totalSendSize/(1024*1024) + " MB"); // send last message with special content uplinkBuffer = TCPThroughputTask.UPLINK_FINISH_MSG.getBytes(); oStream.write(uplinkBuffer, 0, uplinkBuffer.length); oStream.flush(); // read from server side results byte [] resultMsg = new byte[this.BUFFER_SIZE]; int resultMsgLen = iStream.read(resultMsg, 0, resultMsg.length); if (resultMsgLen > 0) { String resultMsgStr = new String(resultMsg).substring(0, resultMsgLen); // Sample result string is "1111.11#2222.22#3333.33"; Logger.i("Uplink result from server is " + resultMsgStr); String [] tps_result_str = resultMsgStr.split("#"); double sampleResult; for (int i = 0; i < tps_result_str.length; i++) { sampleResult = Double.valueOf(tps_result_str[i]); this.samplingResults = this.insertWithOrder(this.samplingResults, sampleResult); } } Logger.i("Total number of sampling result is " + this.samplingResults.size()); } catch (OutOfMemoryError e) { throw new MeasurementError("Detect out of memory during Uplink task."); } catch (IOException e) { throw new MeasurementError("Error to send/receive data to " + ((TCPThroughputDesc)measurementDesc).target); } finally { iStream.close(); oStream.close(); tcpSocket.close(); Logger.i("Close uplink socket"); } } /** * Downlink measurement task */ private void downlink() throws MeasurementError, IOException { Logger.i("Start downlink task on " + ((TCPThroughputDesc)measurementDesc).target); Socket tcpSocket = null; InputStream iStream = null; try { tcpSocket = new Socket(); buildUpSocket(tcpSocket, ((TCPThroughputDesc)measurementDesc).target, TCPThroughputTask.PORT_DOWNLINK); iStream = tcpSocket.getInputStream(); } catch (IOException i) { Logger.e("Downlink socket opening error" + i.getCause().toString()); throw new MeasurementError("Error to open downlink socket at " + ((TCPThroughputDesc)measurementDesc).target + " with port " + TCPThroughputTask.PORT_DOWNLINK); } try { int read_bytes = 0; int data_limit_byte_down = (int)(this.KBYTE*this.KBYTE* ((TCPThroughputDesc)measurementDesc).data_limit_mb_down); byte[] buffer = new byte[this.BUFFER_SIZE]; long totalDuration = (long)(this.KSEC* ((TCPThroughputDesc)measurementDesc).duration_period_sec + ((TCPThroughputDesc)measurementDesc).slow_start_period_sec); do { if(stopFlag){ throw new MeasurementError("Cancelled"); } read_bytes = iStream.read(buffer, 0, buffer.length); updateSize(read_bytes); this.totalRevSize += read_bytes; if (this.DATA_LIMIT_ON && this.totalRevSize >= data_limit_byte_down) { Logger.i("Detect downlink data limitation exceed with " + ((TCPThroughputDesc)measurementDesc).data_limit_mb_down + " MB"); this.DATA_LIMIT_EXCEEDED = true; break; } } while (read_bytes >= 0); // convert milliseconds to seconds this.taskDuration = (System.currentTimeMillis() - (double) this.taskStartTime) / 1000.0; Logger.i("Total download data is " + (double)this.totalRevSize/(1024*1024) + " MB"); Logger.i("Total number of sampling result is " + this.samplingResults.size()); } catch (OutOfMemoryError e) { throw new MeasurementError("Detect out of memory at Downlink task."); } catch (IOException e) { throw new MeasurementError("Error to receive data from " + ((TCPThroughputDesc)measurementDesc).target); } finally { iStream.close(); tcpSocket.close(); Logger.i("Close downlink socket"); } } /***************************************************************** * Helper functions ***************************************************************** * update the total received packet size * @param time period increment */ private void updateSize(int delta) { double gtime = System.currentTimeMillis() - this.taskStartTime; //ignore slow start if (gtime<((TCPThroughputDesc)measurementDesc).slow_start_period_sec*this.KSEC) return; if (this.startSampleTime == 0) { this.startSampleTime = System.currentTimeMillis(); this.accumulativeSize = 0; } this.accumulativeSize += delta; double time = System.currentTimeMillis() - this.startSampleTime; if (time < ((TCPThroughputDesc)measurementDesc).sample_period_sec*this.KSEC) { return; } else { double throughput = (double)this.accumulativeSize * 8.0 / time; this.samplingResults = this.insertWithOrder(this.samplingResults, throughput); this.accumulativeSize = 0; this.startSampleTime = System.currentTimeMillis(); } } private void buildUpSocket(Socket tcpSocket, String hostname, int portNum) throws IOException { TCPThroughputDesc desc = (TCPThroughputDesc) measurementDesc; SocketAddress remoteAddr = new InetSocketAddress(hostname, portNum); tcpSocket.connect(remoteAddr, (int)desc.tcp_timeout_sec*this.KSEC); tcpSocket.setSoTimeout((int)desc.tcp_timeout_sec*this.KSEC); tcpSocket.setTcpNoDelay(true); } private void genRandomByteArray(byte[] byteArray) { for (int i = 0; i < byteArray.length; i++) { byteArray[i] = (byte)('a' + randStr.nextInt(26)); } } // insert element with ascending order, i.e. insertion sort private ArrayList<Double> insertWithOrder(ArrayList<Double> array, double item) { int i; for (i = 0; i < array.size(); i++ ) { if (item < array.get(i)) { break; } } array.add(i,item); return array; } @Override public long getDuration() { return this.duration; } @Override public void setDuration(long newDuration) { if(newDuration<0){ this.duration=0; }else{ this.duration=newDuration; } } @Override public boolean stop() { stopFlag=true; return true; } /** * Based on the measured total data sent and received, the same returned as * a measurement result */ @Override public long getDataConsumed() { return totalSendSize + totalRevSize; } }