package com.samknows.tests; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import com.samknows.libcore.SKPorting; import org.json.JSONObject; public class LatencyTest extends SKAbstractBaseTest implements Runnable { public static final String STRING_ID = "JUDPLATENCY"; private static final String LATENCYRUN = "Running latency and loss tests"; private static final String LATENCYDONE = "Latency and loss tests completed"; public static final String JSON_RTT_AVG = "rtt_avg"; public static final String JSON_RTT_MIN = "rtt_min"; public static final String JSON_RTT_MAX = "rtt_max"; public static final String JSON_RTT_STDDEV = "rtt_stddev"; public static final String JSON_RECEIVED_PACKETS = "received_packets"; public static final String JSON_LOST_PACKETS = "lost_packets"; // Create an interface class, which will allow us to inject a test socket for mock testing. public interface ISKUDPSocket { void open() throws SocketException; void send(DatagramPacket pack) throws IOException; void receive(DatagramPacket pack) throws IOException; void setSoTimeout(int timeout) throws SocketException; void close(); long getStartTimeNanoseconds(); long getTimeNowNanoseconds(); InetAddress getInetAddressByName(String host) throws UnknownHostException; } // Define a real instantiation of the ISKUDPSocket interface, which is used for "real" testing. public class SKUDPSocket implements ISKUDPSocket { private DatagramSocket socket = null; public SKUDPSocket() { } public void open() throws SocketException { SKPorting.sAssert(socket == null); socket = new DatagramSocket(); SKPorting.sAssert(socket != null); } public void send(DatagramPacket pack) throws IOException { if (socket == null) { SKPorting.sAssert(false); return; } socket.send(pack); } public void receive(DatagramPacket pack) throws IOException { if (socket == null) { SKPorting.sAssert(false); return; } socket.receive(pack); } public void setSoTimeout(int timeout) throws SocketException { if (socket == null) { SKPorting.sAssert(false); return; } socket.setSoTimeout(timeout); } public void close() { if (socket == null) { SKPorting.sAssert(false); return; } socket.close(); socket = null; } public long getStartTimeNanoseconds() { return System.nanoTime(); } public long getTimeNowNanoseconds() { return System.nanoTime(); } public InetAddress getInetAddressByName(String host) throws UnknownHostException { return InetAddress.getByName(host); } } public static LatencyTest sCreateLatencyTest(List<Param> params) { LatencyTest ret = new LatencyTest(); try { for (Param param : params) { String value = param.getValue(); if (param.contains(TestFactory.TARGET)) { ret.setTarget(value); } else if (param.contains(TestFactory.PORT)) { ret.setPort(Integer.parseInt(value)); } else if (param.contains(TestFactory.NUMBEROFPACKETS)) { ret.setNumberOfDatagrams(Integer.parseInt(value)); } else if (param.contains(TestFactory.DELAYTIMEOUT)) { ret.setDelayTimeout(Integer.parseInt(value)); } else if (param.contains(TestFactory.INTERPACKETTIME)) { ret.setInterPacketTime(Integer.parseInt(value)); } else if (param.contains(TestFactory.PERCENTILE)) { ret.setPercentile(Integer.parseInt(value)); } else if (param.contains(TestFactory.MAXTIME)) { ret.setMaxExecutionTimeMicroseconds(Long.parseLong(value)); } else { SKPorting.sAssert(false); ret = null; break; } } } catch (NumberFormatException nfe) { SKPorting.sAssert(false); ret = null; } return ret; } static public class Result { public String target; public long rttMicroseconds; public Result(String _target, long nanoseconds) { target = _target; rttMicroseconds = nanoseconds / 1000; } } // Used internally ... and externally, by the HttpTest fallback for ClosestTarget testing. static void sCreateAndPushLatencyResultNanoseconds(BlockingQueue<Result> bq_results, String inTarget, double inRttNanoseconds) { if (bq_results != null) { // Pass-in the value in nanoseconds Result r = new Result(inTarget, (long) inRttNanoseconds); try { bq_results.put(r); } catch (InterruptedException e) { SKPorting.sAssert(LatencyTest.class, false); } } } // Used internally ... private void setLatencyValueNanoseconds(double inRttNanoseconds) { sCreateAndPushLatencyResultNanoseconds(bq_results, target, inRttNanoseconds); } public static int getPacketSize() { return UdpDatagram.PACKETSIZE; } @SuppressWarnings("serial") static private class PacketTimeOutException extends Exception { } public class UdpDatagram { static final int PACKETSIZE = 16; public static final int SERVERTOCLIENTMAGIC = 0x00006000; static final int CLIENTTOSERVERMAGIC = 0x00009000; int datagramid; @SuppressWarnings("unused") int starttimesec; @SuppressWarnings("unused") int starttimeusec; int magic; // When we make the "ping" we don't want to lose any time in memory // allocations, as much as possible should be ready (I miss structs...) byte[] arrayRepresentation; UdpDatagram(byte[] byteArray) { arrayRepresentation = byteArray; ByteBuffer bb = ByteBuffer.wrap(byteArray); datagramid = bb.getInt(); starttimesec = bb.getInt(); starttimeusec = bb.getInt(); magic = bb.getInt(); } UdpDatagram(int datagramid, int magic) { this.datagramid = datagramid; this.magic = magic; arrayRepresentation = new byte[PACKETSIZE]; arrayRepresentation[0] = (byte) (datagramid >>> 24); arrayRepresentation[1] = (byte) (datagramid >>> 16); arrayRepresentation[2] = (byte) (datagramid >>> 8); arrayRepresentation[3] = (byte) (datagramid); arrayRepresentation[12] = (byte) (magic >>> 24); arrayRepresentation[13] = (byte) (magic >>> 16); arrayRepresentation[14] = (byte) (magic >>> 8); arrayRepresentation[15] = (byte) (magic); } byte[] byteArray() { return arrayRepresentation; } void setTime(long time) { int starttimesec = (int) (time / (int) 1e9); int starttimeusec = (int) ((time / (int) 1e3) % (int) 1e6); arrayRepresentation[4] = (byte) (starttimesec >>> 24); arrayRepresentation[5] = (byte) (starttimesec >>> 16); arrayRepresentation[6] = (byte) (starttimesec >>> 8); arrayRepresentation[7] = (byte) (starttimesec); arrayRepresentation[8] = (byte) (starttimeusec >>> 24); arrayRepresentation[9] = (byte) (starttimeusec >>> 16); arrayRepresentation[10] = (byte) (starttimeusec >>> 8); arrayRepresentation[11] = (byte) (starttimeusec); } } private LatencyTest() { } public String getStringID() { return STRING_ID; } public LatencyTest(String server, int port, int numdatagrams, int interPacketTime, int delayTimeout) { target = server; this.port = port; this.numdatagrams = numdatagrams; results = new long[numdatagrams]; this.interPacketTime = interPacketTime * 1000; // nanoSeconds this.delayTimeout = delayTimeout / 1000; // mSeconds } public void setBlockingQueueResult(BlockingQueue<Result> queue) { bq_results = queue; } @Override public int getNetUsage() { return UdpDatagram.PACKETSIZE * (sentPackets + recvPackets); } // @SuppressLint("NewApi") @Override public boolean isReady() { if (target.length() == 0) { SKPorting.sAssert(getClass(), false); return false; } if (port == 0) { SKPorting.sAssert(getClass(), false); return false; } if (numdatagrams == 0 || results == null) { SKPorting.sAssert(getClass(), false); return false; } if (delayTimeout == 0) { SKPorting.sAssert(getClass(), false); return false; } if (interPacketTime == 0) { SKPorting.sAssert(getClass(), false); return false; } if (percentile < 0 || percentile > 100) { SKPorting.sAssert(getClass(), false); return false; } return true; } ISKUDPSocket mSKUDPSocket = null; @Override public void runBlockingTestToFinishInThisThread() { SKPorting.sAssert(mSKUDPSocket == null); mSKUDPSocket = new SKUDPSocket(); // Note that we do NOT run a separate thread, when execute is called! runInCurrentThread(); } public void executeWithSKUDPSocket(ISKUDPSocket skUDPSocket) { mSKUDPSocket = skUDPSocket; runInCurrentThread(); } @Override public boolean isSuccessful() { return testStatus.equals("OK"); } public String getInfo() { return infoString; } public String getTestStatus() { return testStatus; } public int getResultLatencyMilliseconds() { int result = ((int) (averageNanoseconds / 1000000)); return result; } public int getResultLossPercent0To100() { int result = ((int) (100 * (((float) sentPackets - recvPackets) / sentPackets))); return result; } public long getResultJitterMilliseconds() { long jitterMicroseconds = getAverageMicroseconds() - getMinimumMicroseconds(); long jitterMilliseconds = jitterMicroseconds / 1000; return jitterMilliseconds; } public long getAverageMicroseconds() { return (long) (averageNanoseconds / 1000); } public long getMinimumMicroseconds() { return minimumNanoseconds / 1000L; } public long getMaximumMicroseconds() { return maximumNanoseconds / 1000L; } public long getStdDeviationMicroseconds() { return (long) (stddeviationNanoseconds / 1000); } public String getIpAddress() { return ipAddress; } public long getJitter() { return getAverageMicroseconds() - getMinimumMicroseconds(); } public int getSentPackets() { return sentPackets; } public int getReceivedPackets() { return recvPackets; } public int getLostPackets() { return sentPackets - recvPackets; } private Long mTimestamp = SKAbstractBaseTest.sGetUnixTimeStampSeconds(); @Override public synchronized void finish() { mTimestamp = SKAbstractBaseTest.sGetUnixTimeStampSeconds(); status = STATUS.DONE; } @Override public long getTimestamp() { return mTimestamp; } @Override public void setTimestamp(long timestamp) { mTimestamp = timestamp; } @Override public JSONObject getJSONResult() { Map<String, Object> output = new HashMap<>(); // 0 - test string id output.put(JsonData.JSON_TYPE, STRING_ID); // 1- time output.put(JsonData.JSON_TIMESTAMP, mTimestamp); // 2- date output.put(JsonData.JSON_DATETIME, SKPorting.sGetDateAsIso8601String(new java.util.Date(mTimestamp * 1000))); // 3 - test status output.put(JsonData.JSON_SUCCESS, isSuccessful()); // 4 - target output.put(JsonData.JSON_TARGET, target); // 5 - target ipaddress output.put(JsonData.JSON_TARGET_IPADDRESS, ipAddress); // 6- average output.put(JSON_RTT_AVG, getAverageMicroseconds()); // 7 -minimum output.put(JSON_RTT_MIN, getMinimumMicroseconds()); // 8 - maximum output.put(JSON_RTT_MAX, getMaximumMicroseconds()); // 9 - standard deviation output.put(JSON_RTT_STDDEV, getStdDeviationMicroseconds()); // 10 - recvPackets output.put(JSON_RECEIVED_PACKETS, recvPackets); // 11 - lost packets output.put(JSON_LOST_PACKETS, getLostPackets()); //setOutput(o.toArray(new String[1])); JSONObject json_output = new JSONObject(output); return json_output; } // The test can ALSO get run via ClosestTarget, via a new Thread(theLatencyTest), knowing that LatencyTest // is a runnable; using this method! @Override public void run() { runInCurrentThread(); } boolean mbAlreadyRunning = false; private void runInCurrentThread() { SKPorting.sAssert(mbAlreadyRunning == false); mbAlreadyRunning = true; setStateToRunning(); if (mSKUDPSocket == null) { // This should happen ONLY in the "live" app - it should not happen in the unit test. mSKUDPSocket = new SKUDPSocket(); } setStateToRunning(); //set to zero internal variables in case the same test object is executed severals times sentPackets = 0; recvPackets = 0; startTimeNanonseconds = mSKUDPSocket.getStartTimeNanoseconds(); ISKUDPSocket socket = null; try { socket = mSKUDPSocket; socket.open(); socket.setSoTimeout(delayTimeout); } catch (SocketException e) { failure(); return; } // try { // int sendBufferSizeBytes = socket.getSendBufferSize(); // SKCommon.sDoLogDgetClass().getName(), "LatencyTest: sendBufferSizeBytes=" + sendBufferSizeBytes); // int receiveBufferSizeBytes = socket.getReceiveBufferSize(); // SKCommon.sDoLogDgetClass().getName(), "LatencyTest: receiveBufferSizeBytes=" + receiveBufferSizeBytes); // } catch (SocketException e1) { // SKCommon.sAssert(getClass(), false); // } InetAddress address = null; try { address = mSKUDPSocket.getInetAddressByName(target); ipAddress = address.getHostAddress(); } catch (UnknownHostException e) { SKPorting.sAssert(false); failure(); socket.close(); socket = null; return; } for (int i = 0; i < numdatagrams; ++i) { if (maxExecutionTimeNanoseconds > 0) { long timeSoFarNano = socket.getTimeNowNanoseconds() - startTimeNanonseconds; if (timeSoFarNano > maxExecutionTimeNanoseconds) { break; } } UdpDatagram data = new UdpDatagram(i, UdpDatagram.CLIENTTOSERVERMAGIC); byte[] buf = data.byteArray(); DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port); long answerTime = 0; // It isn't the current time as in the original but a random value. // Let's hope nobody changes the server to make this important... long time = mSKUDPSocket.getTimeNowNanoseconds(); data.setTime(time); try { socket.send(packet); sentPackets++; } catch (IOException e) { continue; } try { UdpDatagram answer; do { //Checks for the current time and set the SoTimeout accordingly //because of duplicate packets or packets received after delayTimeout long now = mSKUDPSocket.getTimeNowNanoseconds(); long timeout = delayTimeout - (now - time) / 1000000; if (timeout < 0) { throw new PacketTimeOutException(); } socket.setSoTimeout((int) timeout); socket.receive(packet); answer = new UdpDatagram(buf); if (answer.magic == UdpDatagram.SERVERTOCLIENTMAGIC) { if (answer.datagramid == i) { break; } } } while (true); answerTime = mSKUDPSocket.getTimeNowNanoseconds(); recvPackets++; if (getShouldCancel()) { if (SKPorting.sIsDebuggerConnected()) { SKPorting.sLogD("DEBUG", "Latency - run - cancel test!"); } break; } } catch (SocketTimeoutException e) { continue; } catch (IOException e) { continue; } catch (PacketTimeOutException e) { continue; } long rtt = answerTime - time; results[recvPackets - 1] = rtt; long latencyMilli = rtt / 1000000; //SKTestRunner.sDoReportCurrentLatencyCalculated(latencyMilli); sleep(rtt); } socket.close(); getStats(); setLatencyValueNanoseconds(averageNanoseconds); } private void sleep(long rtt) { long sleepPeriod = interPacketTime - rtt; if (sleepPeriod > 0) { long millis = (long) Math.floor(sleepPeriod / 1000000); int nanos = (int) sleepPeriod % 1000000; try { Thread.sleep(millis, nanos); } catch (InterruptedException e) { SKPorting.sAssert(false); } } } private void failure() { SKPorting.sAssert(false); testStatus = "FAIL"; finish(); } private void getStats() { if (recvPackets <= 0) { failure(); return; } testStatus = "OK"; // Calculate statistics // Results sorted in order to take into account the percentile int nResults = 0; if (recvPackets < 100) { nResults = recvPackets; } else { nResults = (int) Math.ceil(percentile / 100.0 * recvPackets); } Arrays.sort(results, 0, recvPackets); minimumNanoseconds = results[0]; maximumNanoseconds = results[nResults - 1]; averageNanoseconds = 0; for (int i = 0; i < nResults; i++) { averageNanoseconds += results[i]; } averageNanoseconds /= nResults; stddeviationNanoseconds = 0; for (int i = 0; i < nResults; ++i) { stddeviationNanoseconds += Math.pow(results[i] - averageNanoseconds, 2); } if (nResults - 1 > 0) { stddeviationNanoseconds = Math.sqrt(stddeviationNanoseconds / (nResults - 1)); } else { stddeviationNanoseconds = 0; } // Return results finish(); infoString = LATENCYDONE; } public void setTarget(String target) { this.target = target; } public void setPort(int port) { this.port = port; } public void setNumberOfDatagrams(int n) { numdatagrams = n; results = new long[numdatagrams]; } public void setDelayTimeout(int delay) { delayTimeout = delay / 1000; } public void setInterPacketTime(int time) { interPacketTime = time * 1000; // nanoSeconds } public void setPercentile(int n) { percentile = n; } public void setMaxExecutionTimeMicroseconds(long time) { maxExecutionTimeNanoseconds = time * 1000; // convert Microsecodns to NanoSeconds } public boolean isProgressAvailable() { return true; } @Override public int getProgress0To100() { if (mSKUDPSocket == null) { // Not yet prepared! return 0; } double retTime = 0; double retPackets = 0; if (maxExecutionTimeNanoseconds > 0) { long currTime = (mSKUDPSocket.getTimeNowNanoseconds() - startTimeNanonseconds); retTime = (double) currTime / maxExecutionTimeNanoseconds; } retPackets = (double) sentPackets / numdatagrams; double ret = retTime > retPackets ? retTime : retPackets; ret = ret > 1 ? 1 : ret; int percentProgress0To100 = (int) (ret * 100.0); return percentProgress0To100; } public String getTarget() { return target; } private String target = ""; private int port = 0; private String infoString = LATENCYRUN; private String ipAddress; private String testStatus = "FAIL"; private double averageNanoseconds = 0.0; private double stddeviationNanoseconds = 0.0; private long minimumNanoseconds = 0; private long maximumNanoseconds = 0; private long startTimeNanonseconds = 0; private long maxExecutionTimeNanoseconds = 0; private double percentile = 100; private int numdatagrams = 0; private int delayTimeout = 0; private int sentPackets = 0; private int recvPackets = 0; private int interPacketTime = 0; private long[] results = null; private BlockingQueue<Result> bq_results = null; }