/* Copyright 2012 Google Inc.
*
* 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 android.content.Context;
import android.util.Log;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpConnectionParams;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import com.mobiperf.util.MeasurementJsonConvertor;
import com.mobiperf.util.PhoneUtils;
import com.mobiperf.util.Util;
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.R;
import com.mobiperf.SpeedometerApp;
/**
* A callable that executes a ping task using one of three methods
*/
public class PingTask extends MeasurementTask {
// Type name for internal use
public static final String TYPE = "ping";
// Human readable name for the task
public static final String DESCRIPTOR = "ping";
/* Default payload size of the ICMP packet, plus the 8-byte ICMP header resulting in a total of
* 64-byte ICMP packet */
public static final int DEFAULT_PING_PACKET_SIZE = 56;
public static final int DEFAULT_PING_TIMEOUT = 10;
private Process pingProc = null;
private String PING_METHOD_CMD = "ping_cmd";
private String PING_METHOD_JAVA = "java_ping";
private String PING_METHOD_HTTP = "http";
private String targetIp = null;
// Track data consumption for this task to avoid exceeding user's limit
private long dataConsumed;
/**
* Encode ping specific parameters, along with common parameters inherited from MeasurmentDesc
* @author wenjiezeng@google.com (Steve Zeng)
*
*/
public static class PingDesc extends MeasurementDesc {
public String pingExe = null;
// Host address either in the numeric form or domain names
public String target = null;
// The payload size in bytes of the ICMP packet
public int packetSizeByte = PingTask.DEFAULT_PING_PACKET_SIZE;
public int pingTimeoutSec = PingTask.DEFAULT_PING_TIMEOUT;
public PingDesc(String key, Date startTime,
Date endTime, double intervalSec, long count, long priority,
Map<String, String> params) throws InvalidParameterException {
super(PingTask.TYPE, key, startTime, endTime, intervalSec, count,
priority, params);
initializeParams(params);
if (this.target == null || this.target.length() == 0) {
throw new InvalidParameterException("PingTask cannot be created due "
+ " to null target string");
}
}
@Override
protected void initializeParams(Map<String, String> params) {
if (params == null) {
return;
}
this.target = params.get("target");
try {
String val = null;
if ((val = params.get("packet_size_byte")) != null && val.length() > 0 &&
Integer.parseInt(val) > 0) {
this.packetSizeByte = Integer.parseInt(val);
}
if ((val = params.get("ping_timeout_sec")) != null && val.length() > 0 &&
Integer.parseInt(val) > 0) {
this.pingTimeoutSec = Integer.parseInt(val);
}
} catch (NumberFormatException e) {
throw new InvalidParameterException("PingTask cannot be created due to invalid params");
}
}
@Override
public String getType() {
return PingTask.TYPE;
}
}
@SuppressWarnings("rawtypes")
public static Class getDescClass() throws InvalidClassException {
return PingDesc.class;
}
public PingTask(MeasurementDesc desc, Context parent) {
super(new PingDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec,
desc.count, desc.priority, desc.parameters), parent);
dataConsumed = 0;
}
/**
* Returns a copy of the PingTask
*/
@Override
public MeasurementTask clone() {
MeasurementDesc desc = this.measurementDesc;
PingDesc newDesc = new PingDesc(desc.key, desc.startTime, desc.endTime,
desc.intervalSec, desc.count, desc.priority, desc.parameters);
return new PingTask(newDesc, parent);
}
/* We will use three methods to ping the requested resource in the order of PING_COMMAND,
* JAVA_ICMP_PING, and HTTP_PING. If all fails, then we declare the resource unreachable */
@Override
public MeasurementResult call() throws MeasurementError {
PingDesc desc = (PingDesc) measurementDesc;
int ipByteLength;
try {
InetAddress addr = InetAddress.getByName(desc.target);
// Get the address length
ipByteLength = addr.getAddress().length;
Logger.i("IP address length is " + ipByteLength);
// All ping methods ping against targetIp rather than desc.target
targetIp = addr.getHostAddress();
Logger.i("IP is " + targetIp);
} catch (UnknownHostException e) {
throw new MeasurementError("Unknown host " + desc.target);
}
try {
Logger.i("running ping command");
// Prevents the phone from going to low-power mode where WiFi turns off
return executePingCmdTask(ipByteLength);
} catch (MeasurementError e) {
try {
Logger.i("running java ping");
return executeJavaPingTask();
} catch (MeasurementError ee) {
Logger.i("running http ping");
return executeHttpPingTask();
}
}
}
@Override
public String getType() {
return PingTask.TYPE;
}
@Override
public String getDescriptor() {
return DESCRIPTOR;
}
@Override
public int getProgress() {
return this.progress;
}
private MeasurementResult constructResult(ArrayList<Double> rrtVals, double packetLoss,
int packetsSent, String pingMethod) {
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
double mdev, avg, filteredAvg;
double total = 0;
boolean success = true;
if (rrtVals.size() == 0) {
return null;
}
for (double rrt : rrtVals) {
if (rrt < min) {
min = rrt;
}
if (rrt > max) {
max = rrt;
}
total += rrt;
}
avg = total / rrtVals.size();
mdev = Util.getStandardDeviation(rrtVals, avg);
filteredAvg = filterPingResults(rrtVals, avg);
PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId,
phoneUtils.getDeviceProperty(), PingTask.TYPE, System.currentTimeMillis() * 1000,
success, this.measurementDesc);
result.addResult("target_ip", targetIp);
result.addResult("mean_rtt_ms", avg);
result.addResult("min_rtt_ms", min);
result.addResult("max_rtt_ms", max);
result.addResult("stddev_rtt_ms", mdev);
if (filteredAvg != avg) {
result.addResult("filtered_mean_rtt_ms", filteredAvg);
}
result.addResult("packet_loss", packetLoss);
result.addResult("packets_sent", packetsSent);
result.addResult("ping_method", pingMethod);
Logger.i(MeasurementJsonConvertor.toJsonString(result));
return result;
}
private void cleanUp(Process proc) {
try {
if (proc != null) {
proc.destroy();
}
} catch (Exception e) {
Logger.w("Unable to kill ping process" + e.getMessage());
}
}
/* Compute the average of the filtered rtts.
* The first several ping results are usually extremely large as the device needs to activate
* the wireless interface and resolve domain names. Such distorted measurements are filtered out
*
*/
private double filterPingResults(final ArrayList<Double> rrts, double avg) {
double rrtAvg = avg;
// Our # of results should be less than the # of times we ping
try {
ArrayList<Double> filteredResults =
Util.applyInnerBandFilter(rrts, Double.MIN_VALUE, rrtAvg * Config.PING_FILTER_THRES);
// Now we compute the average again based on the filtered results
if (filteredResults != null && filteredResults.size() > 0) {
rrtAvg = Util.getSum(filteredResults) / filteredResults.size();
}
} catch (InvalidParameterException e) {
Log.wtf(SpeedometerApp.TAG, "This should never happen because rrts is never empty");
}
return rrtAvg;
}
// Runs when SystemState is IDLE
private MeasurementResult executePingCmdTask(int ipByteLen) throws MeasurementError {
Logger.i("Starting executePingCmdTask");
PingDesc pingTask = (PingDesc) this.measurementDesc;
String errorMsg = "";
MeasurementResult measurementResult = null;
// TODO(Wenjie): Add a exhaustive list of ping locations for different Android phones
pingTask.pingExe = Util.pingExecutableBasedOnIPType(ipByteLen, parent);
Logger.i("Ping executable is " + pingTask.pingExe);
if (pingTask.pingExe == null) {
Logger.e("Ping executable not found");
throw new MeasurementError("Ping executable not found");
}
try {
String command = Util.constructCommand(pingTask.pingExe, "-i",
Config.DEFAULT_INTERVAL_BETWEEN_ICMP_PACKET_SEC,
"-s", pingTask.packetSizeByte, "-w", pingTask.pingTimeoutSec, "-c",
Config.PING_COUNT_PER_MEASUREMENT, targetIp);
Logger.i("Running: " + command);
pingProc = Runtime.getRuntime().exec(command);
dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2;
// Grab the output of the process that runs the ping command
InputStream is = pingProc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line = null;
int lineCnt = 0;
ArrayList<Double> rrts = new ArrayList<Double>();
ArrayList<Integer> receivedIcmpSeq = new ArrayList<Integer>();
double packetLoss = Double.MIN_VALUE;
int packetsSent = Config.PING_COUNT_PER_MEASUREMENT;
// Process each line of the ping output and store the rrt in array rrts.
while ((line = br.readLine()) != null) {
// Ping prints a number of 'param=value' pairs, among which we only need the
// 'time=rrt_val' pair
String[] extractedValues = Util.extractInfoFromPingOutput(line);
if (extractedValues != null) {
int curIcmpSeq = Integer.parseInt(extractedValues[0]);
double rrtVal = Double.parseDouble(extractedValues[1]);
// ICMP responses from the system ping command could be duplicate and out of order
if (!receivedIcmpSeq.contains(curIcmpSeq)) {
rrts.add(rrtVal);
receivedIcmpSeq.add(curIcmpSeq);
}
}
this.progress = 100 * ++lineCnt / Config.PING_COUNT_PER_MEASUREMENT;
this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, progress);
broadcastProgressForUser(progress);
// Get the number of sent/received pings from the ping command output
int[] packetLossInfo = Util.extractPacketLossInfoFromPingOutput(line);
if (packetLossInfo != null) {
packetsSent = packetLossInfo[0];
int packetsReceived = packetLossInfo[1];
packetLoss = 1 - ((double) packetsReceived / (double) packetsSent);
}
Logger.i(line);
}
// Use the output from the ping command to compute packet loss. If that's not
// available, use an estimation.
if (packetLoss == Double.MIN_VALUE) {
packetLoss = 1 - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT);
}
measurementResult = constructResult(rrts, packetLoss, packetsSent, PING_METHOD_CMD);
} catch (IOException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (SecurityException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (NumberFormatException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (InvalidParameterException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} finally {
// All associated streams with the process will be closed upon destroy()
cleanUp(pingProc);
}
if (measurementResult == null) {
Logger.e("Error running ping: " + errorMsg);
throw new MeasurementError(errorMsg);
}
return measurementResult;
}
// Runs when the ping command fails
private MeasurementResult executeJavaPingTask() throws MeasurementError {
PingDesc pingTask = (PingDesc) this.measurementDesc;
long pingStartTime = 0;
long pingEndTime = 0;
ArrayList<Double> rrts = new ArrayList<Double>();
String errorMsg = "";
MeasurementResult result = null;
try {
int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec /
Config.PING_COUNT_PER_MEASUREMENT);
int successfulPingCnt = 0;
long totalPingDelay = 0;
for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) {
pingStartTime = System.currentTimeMillis();
boolean status = InetAddress.getByName(targetIp).isReachable(timeOut);
pingEndTime = System.currentTimeMillis();
long rrtVal = pingEndTime - pingStartTime;
if (status) {
totalPingDelay += rrtVal;
rrts.add((double) rrtVal);
}
this.progress = 100 * i / Config.PING_COUNT_PER_MEASUREMENT;
broadcastProgressForUser(progress);
}
Logger.i("java ping succeeds");
double packetLoss = 1 - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT);
dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2;
result = constructResult(rrts, packetLoss, Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_JAVA);
} catch (IllegalArgumentException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (IOException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
}
if (result != null) {
return result;
} else {
Logger.i("java ping fails");
throw new MeasurementError(errorMsg);
}
}
/**
* Use the HTTP Head method to emulate ping. The measurement from this method can be
* substantially (2x) greater than the first two methods and inaccurate. This is because,
* depending on the implementing of the destination web server, either a quick HTTP
* response is replied or some actual heavy lifting will be done in preparing the response
* */
private MeasurementResult executeHttpPingTask() throws MeasurementError {
long pingStartTime = 0;
long pingEndTime = 0;
ArrayList<Double> rrts = new ArrayList<Double>();
PingDesc pingTask = (PingDesc) this.measurementDesc;
String errorMsg = "";
MeasurementResult result = null;
try {
long totalPingDelay = 0;
URL url = new URL("http://"+ pingTask.target);
int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec /
Config.PING_COUNT_PER_MEASUREMENT);
for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) {
pingStartTime = System.currentTimeMillis();
HttpURLConnection httpClient = (HttpURLConnection) url.openConnection();
httpClient.setRequestProperty("Connection", "close");
httpClient.setRequestMethod("HEAD");
httpClient.setReadTimeout(timeOut);
httpClient.setConnectTimeout(timeOut);
httpClient.connect();
pingEndTime = System.currentTimeMillis();
httpClient.disconnect();
rrts.add((double) (pingEndTime - pingStartTime));
this.progress = 100 * i / Config.PING_COUNT_PER_MEASUREMENT;
broadcastProgressForUser(progress);
}
Logger.i("HTTP get ping succeeds");
Logger.i("RTT is " + rrts.toString());
double packetLoss = 1 - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT);
result = constructResult(rrts, packetLoss, Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_HTTP);
dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2;
} catch (MalformedURLException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (IOException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
}
if (result != null) {
return result;
} else {
Logger.i("HTTP get ping fails");
throw new MeasurementError(errorMsg);
}
}
@Override
public String toString() {
PingDesc desc = (PingDesc) measurementDesc;
return "[Ping]\n Target: " + desc.target + "\n Interval (sec): " + desc.intervalSec
+ "\n Next run: " + desc.startTime;
}
@Override
public void stop() {
cleanUp(pingProc);
}
/**
* Data sent so far by this task.
*
* We count packets sent directly to calculate the data sent
*/
@Override
public long getDataConsumed() {
return dataConsumed;
}
}