/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2011 G. Zsombor
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.network;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import net.pms.PMS;
import net.pms.io.OutputParams;
import net.pms.io.ProcessWrapperImpl;
import net.pms.io.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Network speed tester class. This can be used in an asynchronous way, as it returns Future objects.
*
* Future<Integer> speed = SpeedStats.getInstance().getSpeedInMBits(addr);
*
* @see Future
*
* @author zsombor <gzsombor@gmail.com>
*
*/
public class SpeedStats {
private static SpeedStats instance = new SpeedStats();
private static ExecutorService executor = Executors.newCachedThreadPool();
public static SpeedStats getInstance() {
return instance;
}
private static final Logger LOGGER = LoggerFactory.getLogger(SpeedStats.class);
private final Map<String, Future<Integer>> speedStats = new HashMap<>();
public Future<Integer> getSpeedInMBitsStored(InetAddress addr, String rendererName) {
// only look in the store
// if no pings are done resort to conf values
synchronized (speedStats) {
return speedStats.get(addr.getHostAddress());
}
}
/**
* Return the network throughput for the given IP address in MBits. It is calculated in the background, and cached,
* so only a reference is given to the result, which can be retrieved by calling the get() method on it.
*
* @param addr
* @param rendererName
*
* @return The network throughput
*/
public Future<Integer> getSpeedInMBits(InetAddress addr, String rendererName) {
synchronized (speedStats) {
Future<Integer> value = speedStats.get(addr.getHostAddress());
if (value != null) {
return value;
}
value = executor.submit(new MeasureSpeed(addr, rendererName));
speedStats.put(addr.getHostAddress(), value);
return value;
}
}
class MeasureSpeed implements Callable<Integer> {
InetAddress addr;
String rendererName;
public MeasureSpeed(InetAddress addr, String rendererName) {
this.addr = addr;
this.rendererName = rendererName != null ? rendererName.replaceAll("\n", "") : "Unknown";
}
@Override
public Integer call() throws Exception {
try {
return doCall();
} catch (Exception e) {
LOGGER.warn("Error measuring network throughput : " + e.getMessage(), e);
throw e;
}
}
private Integer doCall() throws Exception {
String ip = addr.getHostAddress();
LOGGER.info("Checking IP: " + ip + " for " + rendererName);
// calling the canonical host name the first time is slow, so we call it in a separate thread
String hostname = addr.getCanonicalHostName();
synchronized (speedStats) {
Future<Integer> otherTask = speedStats.get(hostname);
if (otherTask != null) {
// wait a little bit
try {
// probably we are waiting for ourself to finish the work...
Integer value = otherTask.get(100, TimeUnit.MILLISECONDS);
// if the other task already calculated the speed, we get the result,
// unless we do it now
if (value != null) {
return value;
}
} catch (TimeoutException e) {
LOGGER.trace("We couldn't get the value based on the canonical name");
}
}
}
if (!ip.equals(hostname)) {
LOGGER.info("Renderer " + rendererName + " found on this address: " + hostname + " (" + ip + ")");
} else {
LOGGER.info("Renderer " + rendererName + " found on this address: " + ip);
}
int[] sizes = {512, 1476, 9100, 32000, 64000};
double bps = 0;
int cnt = 0;
for (int i = 0; i < sizes.length; i++) {
double p = doPing(sizes[i]);
if (p != 0) {
bps += p;
cnt++;
}
}
int speedInMbits = (int) (bps / (cnt * 1000000));
LOGGER.info("Renderer " + rendererName + " has an estimated network speed of " + speedInMbits + " Mb/s");
synchronized (speedStats) {
CompletedFuture<Integer> result = new CompletedFuture<>(speedInMbits);
// update the statistics with a computed future value
speedStats.put(ip, result);
speedStats.put(hostname, result);
}
return speedInMbits;
}
private double doPing(int size) {
// let's get that speed
OutputParams op = new OutputParams(null);
op.log = true;
op.maxBufferSize = 1;
SystemUtils sysUtil = PMS.get().getRegistry();
final ProcessWrapperImpl pw = new ProcessWrapperImpl(sysUtil.getPingCommand(addr.getHostAddress(), 3, size), op, true, false);
Runnable r = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
pw.stopProcess();
}
};
Thread failsafe = new Thread(r, "SpeedStats Failsafe");
failsafe.start();
pw.runInSameThread();
List<String> ls = pw.getOtherResults();
double time = 0;
int c = 0;
String timeString;
for (String line : ls) {
timeString = sysUtil.parsePingLine(line);
if (timeString == null) {
continue;
}
try {
time += Double.parseDouble(timeString);
c++;
} catch (NumberFormatException e) {
// no big deal
LOGGER.debug("Could not estimate network speed from time: \"" + timeString + "\"");
}
}
if (c > 0) {
time /= c;
int frags = ((size + 8) / 1500) + 1;
LOGGER.debug("est speed for " + size + " " + frags + " is " + ((size + 8 + (frags * 32)) * 8000 * 2) / time);
return ((size + 8 + (frags * 32)) * 8000 * 2) / time;
}
return time;
}
}
static class CompletedFuture<X> implements Future<X> {
X value;
public CompletedFuture(X value) {
this.value = value;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return true;
}
@Override
public X get() throws InterruptedException, ExecutionException {
return value;
}
@Override
public X get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return value;
}
}
}