/*
* 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 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;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
/**
* 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<String, Future<Integer>>();
/**
* 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
* @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 : "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);
}
// 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, 64000), op,
true, false);
Runnable r = new Runnable() {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
pw.stopProcess();
}
};
Thread failsafe = new Thread(r, "SpeedStats Failsafe");
failsafe.start();
pw.runInSameThread();
List<String> ls = pw.getOtherResults();
int time = 0;
int c = 0;
for (String line : ls) {
int msPos = line.indexOf("ms");
if (msPos > -1) {
String timeString = line.substring(line.lastIndexOf("=", msPos) + 1, msPos).trim();
try {
time += Double.parseDouble(timeString);
c++;
} catch (NumberFormatException e) {
// no big deal
logger.debug("Could not parse time from \"" + timeString + "\"");
}
}
}
if (c > 0) {
time = time / c;
}
if (time > 0) {
int speedInMbits = 1024 / time;
logger.info("Address " + addr + " has an estimated network speed of: " + speedInMbits + " Mb/s");
synchronized(speedStats) {
CompletedFuture<Integer> result = new CompletedFuture<Integer>(speedInMbits);
// update the statistics with a computed future value
speedStats.put(ip, result);
speedStats.put(hostname, result);
}
return speedInMbits;
}
return -1;
}
}
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;
}
}
}