com.mobiperf.speedometer.measurements.PingTask.java Source code

Java tutorial

Introduction

Here is the source code for com.mobiperf.speedometer.measurements.PingTask.java

Source

/* 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.speedometer.measurements;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.Inet6Address;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;

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 android.content.Context;
import android.net.http.AndroidHttpClient;

import com.mobiperf.mobiperf.R;
import com.mobiperf.speedometer.Config;
import com.mobiperf.speedometer.Logger;
import com.mobiperf.speedometer.MeasurementDesc;
import com.mobiperf.speedometer.MeasurementError;
import com.mobiperf.speedometer.MeasurementResult;
import com.mobiperf.speedometer.MeasurementTask;
import com.mobiperf.util.MeasurementJsonConvertor;
import com.mobiperf.util.PhoneUtils;
import com.mobiperf.util.Util;
import java.util.regex.Pattern;

/**
 * A callable that executes a ping task using one of three methods
 * 
 * @author wenjiezeng@google.com (Steve Zeng)
 * @author zad522@gmail.com (Andong Zhan)
 */
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 targetIp = null;

    /*
     * used to check if the targetip is a ipv4 address
     */
    public static final String _255 = "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
    public static final Pattern ipv4 = Pattern.compile("^(?:" + _255 + "\\.){3}" + _255 + "$");

    public static boolean isValidIpv4(String ip) {
        return ipv4.matcher(ip).matches();
    }

    /**
     * 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, null, null);
            initalizeParams(params);
            if (this.target == null || this.target.length() == 0) {
                throw new InvalidParameterException("PingTask cannot be created due " + " to null target string");
            }
        }

        @Override
        protected void initalizeParams(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);
    }

    /**
     * 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;
        try {
            /*
             * only select ipv4 address
             */
            InetAddress[] addrs = InetAddress.getAllByName(desc.target);

            for (InetAddress address : addrs) {
                // All ping methods ping against targetIp rather than desc.target
                targetIp = address.getHostAddress();
                if (PingTask.isValidIpv4(targetIp)) {
                    break;
                }

            }
            if (!PingTask.isValidIpv4(targetIp)) {
                throw new MeasurementError("No Ipv4 address for host" + desc.target);
            }

        } 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();
        } 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) {
        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);
        return result;
    }

    private void cleanUp(Process proc) {
        try {
            if (proc != null) {
                proc.destroy();
            }
        } catch (Exception e) {
            Logger.w("Unable to kill ping process", e);
        }
    }

    /*
     * 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() 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 = parent.getString(R.string.ping_executable);
        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);
            command = "/system/bin/getevent";
            Logger.i("Running: " + command);
            pingProc = Runtime.getRuntime().exec(command);

            // 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);
            Logger.i(MeasurementJsonConvertor.toJsonString(measurementResult));
        } 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) (1000 * (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;
                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);
            result = constructResult(rrts, packetLoss, Config.PING_COUNT_PER_MEASUREMENT);
        } 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;

            HttpClient client = AndroidHttpClient.newInstance(Util.prepareUserAgent(this.parent));
            HttpHead headMethod;

            headMethod = new HttpHead("http://" + targetIp);

            headMethod.addHeader(new BasicHeader("Connection", "close"));
            headMethod.setParams(new BasicHttpParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 1000));

            int timeOut = (int) (1000 * (double) pingTask.pingTimeoutSec / Config.PING_COUNT_PER_MEASUREMENT);
            HttpConnectionParams.setConnectionTimeout(headMethod.getParams(), timeOut);

            for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) {
                pingStartTime = System.currentTimeMillis();
                HttpResponse response = client.execute(headMethod);
                pingEndTime = System.currentTimeMillis();
                rrts.add((double) (pingEndTime - pingStartTime));
                this.progress = 100 * i / Config.PING_COUNT_PER_MEASUREMENT;
                broadcastProgressForUser(progress);
            }
            Logger.i("HTTP get ping succeeds");
            double packetLoss = 1 - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT);
            result = constructResult(rrts, packetLoss, Config.PING_COUNT_PER_MEASUREMENT);
            // close the client to avoid memory leak
            ((AndroidHttpClient) client).close();
        } 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);
    }
}