package org.bundlebee.weaver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Keeps track of how long local/remote calls took.
*
* @author Hendrik Schreiber
*/
public class ServiceCallStats {
private static Logger LOG = LoggerFactory.getLogger(ServiceCallStats.class);
private static final int DEFAULT_MAX_SAMPLES = 1000;
private static final int DEFAULT_MIN_SAMPLES = 5;
private final Map> URICallMap = new ConcurrentHashMap>();
private final Map callCount = new HashMap();
private final int maxSamples;
private final int minSamples;
/**
* @param maxSamples max number of recent samples considered for computing means.
* @param minSamples min number of samples required for meaningful means
*/
public ServiceCallStats(final int maxSamples, final int minSamples) {
if (maxSamples < 1) throw new IllegalArgumentException("MaxSamples must be greater than 0.");
if (minSamples > maxSamples) throw new IllegalArgumentException("MinSamples must be less or equal to MaxSamples.");
if (minSamples < 1) throw new IllegalArgumentException("MinSamples must be greater than 0.");
this.maxSamples = maxSamples;
this.minSamples = minSamples;
}
public ServiceCallStats() {
this(DEFAULT_MAX_SAMPLES, DEFAULT_MIN_SAMPLES);
}
/**
* Max number of samples considered for computing averages.
*
* @return a positive integer
*/
public int getMaxSamples() {
return maxSamples;
}
/**
* Min number of samples for meaningful means.
*
* @return min samples
*/
public int getMinSamples() {
return minSamples;
}
/**
* Let's you log how long a local call took.
*
* @param service the service object
* @param methodName the methodname
* @param parameterTypes the parameter types
* @param duration the duration (whatever unit you put in, you get out)
*/
public void logLocalCall(final Object service, final String methodName,
final Class[] parameterTypes, final long duration) {
logCall(ServiceCallAspect.LOCAL_URI, service, methodName, parameterTypes, duration);
}
/**
* Let's you log how long a call took.
*
* @param uri URI - this can be {@link org.bundlebee.weaver.ServiceCallAspect#LOCAL_URI}
* @param service the service object
* @param methodName the methodname
* @param parameterTypes the parameter types
* @param duration the duration (whatever unit you put in, you get out)
*/
public void logCall(final URI uri, final Object service, final String methodName,
final Class[] parameterTypes, final long duration) {
final URI actualURI = uri == null ? ServiceCallAspect.LOCAL_URI : uri;
if (LOG.isDebugEnabled()) LOG.debug("Call to " + actualURI +": " + duration + " time units");
final ServiceCall serviceCall = new ServiceCall(service, methodName, parameterTypes);
incrementCallCount(serviceCall);
getMean(actualURI, serviceCall).add(duration);
}
private void incrementCallCount(final ServiceCall serviceCall) {
synchronized (callCount) {
final Long count = callCount.get(serviceCall);
if (count == null) callCount.put(serviceCall, 1L);
else callCount.put(serviceCall, count + 1L);
}
}
/**
* Indicates how many times a method was called regardless of which URI it was called on.
*
* @param service service
* @param methodName method
* @param parameterTypes parameter types
* @return count
*/
public long getCallCount(final Object service, final String methodName,
final Class[] parameterTypes) {
final ServiceCall serviceCall = new ServiceCall(service, methodName, parameterTypes);
Long count;
synchronized (callCount) {
count = callCount.get(serviceCall);
if (count == null) count = 0L;
}
return count;
}
/**
* Let's you find out how long a local call took in the past (on average).
*
* @param service the service object
* @param methodName the methodname
* @param parameterTypes the parameter types
* @return mean of earlier logged call durations or -1, if fewer than {@link #getMinSamples()} calls were logged
*/
public long getLocalCallMean(final Object service, final String methodName,
final Class[] parameterTypes) {
return getMean(ServiceCallAspect.LOCAL_URI, service, methodName, parameterTypes).getMean();
}
/**
* Let's you find out how long a remote call took in the past (on average).
*
* @param uri URI for the remote host in question
* @param service the service object
* @param methodName the methodname
* @param parameterTypes the parameter types @return mean of earlier logged call durations or -1,
* if fewer than {@link #getMinSamples()} calls were logged
* @return mean of earlier logged call durations or -1, if fewer than {@link #getMinSamples()} calls were logged
*/
public long getCallMean(final URI uri, final Object service, final String methodName,
final Class[] parameterTypes) {
return getMean(uri == null ? ServiceCallAspect.LOCAL_URI : uri, service, methodName, parameterTypes).getMean();
}
/**
* Indicates, whether a local call should be cheaper.
* This method is biased towards false
as return value, meaning its biased towards
* remote calls.
*
* @param uri URI of the remote manager to compare with
* @param service service
* @param methodName method name
* @param parameterTypes parameter types
* @return true, if a local call is cheaper AND we actually have enough data.
* false, if we don't have enough data OR the remote call is cheaper
*/
public boolean isLocalCallCheaper(final URI uri, final Object service, final String methodName,
final Class[] parameterTypes) {
final long localCallMean = getLocalCallMean(service, methodName, parameterTypes);
final long remoteCallMean = getCallMean(uri, service, methodName, parameterTypes);
return localCallMean > 0 && remoteCallMean > 0 && localCallMean < remoteCallMean;
}
/**
* Indicates, whether a local call should be cheaper.
* This method is biased towards false
as return value, meaning its biased towards
* remote calls.
*
* @param service service
* @param methodName method name
* @param parameterTypes parameter types
* @return true, if a local call is cheaper AND we actually have enough data.
* false, if we don't have enough data OR the remote call is cheaper
*/
public boolean isLocalCallCheaper(final Object service, final String methodName,
final Class[] parameterTypes) {
final long localCallMean = getLocalCallMean(service, methodName, parameterTypes);
long minRemoteCallMean = -1;
for (final URI uri : this.URICallMap.keySet()) {
if (uri == ServiceCallAspect.LOCAL_URI) continue;
final long remoteCallMean = getCallMean(uri, service, methodName, parameterTypes);
if (remoteCallMean > 0 && remoteCallMean < minRemoteCallMean) minRemoteCallMean = remoteCallMean;
}
return localCallMean > 0 && minRemoteCallMean > 0 && localCallMean < minRemoteCallMean;
}
/**
* Returns the URI with the lowest call mean.
*
* @param service service
* @param methodName method name
* @param parameterTypes parameter types
* @return regular URI, {@link org.bundlebee.weaver.ServiceCallAspect#LOCAL_URI}
* or null, if we don't have enough data
*/
public URI getMinCallMeanURI(final Object service, final String methodName,
final Class[] parameterTypes) {
long minCallMean = -1;
URI minCallMeanURI = null;
for (final URI uri : this.URICallMap.keySet()) {
final long remoteCallMean = getCallMean(uri, service, methodName, parameterTypes);
if (remoteCallMean > 0 && remoteCallMean < minCallMean) {
minCallMean = remoteCallMean;
minCallMeanURI = uri;
}
}
return minCallMeanURI;
}
private Mean getMean(final URI uri, final Object service, final String methodName, final Class[] parameterTypes) {
final ServiceCall serviceCall = new ServiceCall(service, methodName, parameterTypes);
return getMean(uri, serviceCall);
}
private Mean getMean(final URI uri, final ServiceCall serviceCall) {
Map calls = this.URICallMap.get(uri);
if (calls == null) {
calls = new ConcurrentHashMap();
this.URICallMap.put(uri, calls);
}
Mean mean = calls.get(serviceCall);
if (mean == null) {
mean = new Mean(maxSamples, minSamples);
calls.put(serviceCall, mean);
}
return mean;
}
/**
* Mean.
*/
private static class Mean {
private List durations = new LinkedList();
private long sum;
private int maxSamples = 1000;
private int minSamples = 5;
private Mean(final int maxSamples, final int minSamples) {
if (maxSamples < 1) throw new IllegalArgumentException();
this.maxSamples = maxSamples;
this.minSamples = minSamples;
}
public synchronized void add(final long duration) {
// avoid overflow
while ((Long.MAX_VALUE - sum < duration && !durations.isEmpty()) || durations.size() >= maxSamples) {
sum -= durations.remove(0);
}
durations.add(duration);
sum += duration;
}
public synchronized long getMean() {
if (durations.size() < minSamples) return -1;
return sum/durations.size();
}
/**
* @return the number of samples this mean is based on
*/
public synchronized int getSamples() {
return durations.size();
}
public String toString() {
return "Mean[sum=" + sum +",samples=" +durations.size() + "mean=" + getMean() +"]";
}
}
/**
* Service call key for hashmap.
*/
private static class ServiceCall {
private String className;
private String methodName;
private String[] parameterTypeNames;
private ServiceCall(final Object service, final String methodName, final Class[] parameterTypes) {
this.className = service.getClass().getName();
this.methodName = methodName;
this.parameterTypeNames = new String[parameterTypes.length];
for (int i=0; i