/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.engine.calcnode.stats;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicLong;
import org.fudgemsg.MutableFudgeMsg;
import org.fudgemsg.mapping.FudgeSerializer;
import org.threeten.bp.Duration;
import com.opengamma.engine.calcnode.msg.Invocations;
import com.opengamma.engine.calcnode.msg.Invocations.PerConfiguration;
import com.opengamma.engine.calcnode.msg.Invocations.PerConfiguration.PerFunction;
import com.opengamma.engine.calcnode.msg.RemoteCalcNodeMessage;
import com.opengamma.transport.FudgeMessageSender;
import com.opengamma.util.ArgumentChecker;
/**
* Gatherer collecting invocation information and sending statistics onwards.
* <p>
* This runs on each calculation node to collect statistics.
* It gathers the data and from time-to-time forwards the information centrally.
*/
public class FunctionInvocationStatisticsSender implements FunctionInvocationStatisticsGatherer {
/**
* The storage of the statistics not yet sent to the server.
*/
private final ConcurrentMap<String, ConcurrentMap<String, PerFunction>> _data = new ConcurrentHashMap<String, ConcurrentMap<String, PerFunction>>();
private final AtomicLong _lastSent = new AtomicLong();
private FudgeMessageSender _messageSender;
private ExecutorService _executorService;
private double _convergenceFactor = 0.8;
private double _serverScalingHint = 1.0;
private volatile double _invocationTimeScale = 1.0;
private long _frequencyNanos = 5000000000L; // 5_sec
/**
* Creates an instance.
*/
public FunctionInvocationStatisticsSender() {
}
// -------------------------------------------------------------------------
/**
* Gets the Fudge message sender.
*
* @return the sender
*/
protected FudgeMessageSender getFudgeMessageSender() {
return _messageSender;
}
/**
* Sets the Fudge message sender.
*
* @param messageSender the sender
*/
public void setFudgeMessageSender(final FudgeMessageSender messageSender) {
_messageSender = messageSender;
}
/**
* Gets the executor service.
*
* @return the executor service
*/
protected ExecutorService getExecutorService() {
return _executorService;
}
/**
* Sets the executor service.
*
* @param executorService the executor service
*/
public void setExecutorService(final ExecutorService executorService) {
_executorService = executorService;
}
/**
* Gets the update period for sending statistics.
*
* @return the duration of the update period, not null
*/
public Duration getUpdatePeriod() {
return Duration.ofNanos(_frequencyNanos);
}
/**
* Sets the update period for sending statistics.
*
* @param duration the duration of the update period, not null
*/
public void setUpdatePeriod(final Duration duration) {
ArgumentChecker.notNull(duration, "duration");
_frequencyNanos = duration.toNanos();
}
// -------------------------------------------------------------------------
/**
* Sets the scale for the invocation time metric.
* <p>
* This is the ratio of local node performance to the "standard" (typically a node running on
* the view processor). The default initial value is 1.0.
* <p>
* If a manual value is set (other than {@code 1.0}) and hints from the server are disabled,
* convergence should also be disabled to prevent it being shifted towards {@code 1.0}.</p>
*
* @param invocationTimeScale the scaling factor, must be positive and non-zero
*/
public void setInvocationTimeScale(final double invocationTimeScale) {
ArgumentChecker.notNegativeOrZero(invocationTimeScale, "invocationTimeScale");
_invocationTimeScale = invocationTimeScale;
}
/**
* Sets the convergence factor metric.
* <p>
* If set to {@code 1.0} has no effect, otherwise forces the scales to attempt to converge towards
* {@code 1.0}. This is useful if there are no local nodes to act as reference point - a set of purely
* remote (and identical) notes all automatically tuning their parameters will converge on similar
* scales but the exact value may drift if there is no fixed reference. The default value is {@code 0.8}.
*
* @param convergenceFactor power, must be in the range {@code (0..1]}.
*/
public void setConvergenceFactor(final double convergenceFactor) {
ArgumentChecker.isInRangeExcludingLow(0d, 1d, convergenceFactor);
_convergenceFactor = convergenceFactor;
}
/**
* Sets the server scaling hint metric.
* <p>
* This is set to {@code 1.0} to use the hints from the server, or {@code 0.0} to completely ignore
* the hints from the server. Values in between will have a partial effect. The default value is {@code 1.0}.
*
* @param serverScalingHint power, must be in the range {@code [0,1]}.
*/
public void setServerScalingHint(final double serverScalingHint) {
ArgumentChecker.isInRangeInclusive(0d, 1d, serverScalingHint);
_serverScalingHint = serverScalingHint;
}
/**
* Sets the scaling from a single value using the server hint and convergence.
*
* @param invocationTimeScale the scaling value
*/
public void setScaling(final double invocationTimeScale) {
// Doesn't matter that these three aren't updated and used atomically
// Note the scale we get sent is relative to the values we previously sent so is used to adjust the scales we previously used.
// We also want a reluctance to head away from 1.0 to avoid the creep that otherwise occurs
setInvocationTimeScale(Math.pow(_invocationTimeScale * Math.pow(invocationTimeScale, _serverScalingHint), _convergenceFactor));
}
// -------------------------------------------------------------------------
@Override
public void functionInvoked(final String configurationName, final String functionId, final int invocationCount,
final double executionNanos, final double dataInputBytes, final double dataOutputBytes) {
final ConcurrentMap<String, PerFunction> statsMap = getConfigurationData(configurationName);
PerFunction stats = statsMap.get(functionId);
if (stats == null) {
stats = new PerFunction(functionId, invocationCount, executionNanos, dataInputBytes, dataOutputBytes);
PerFunction newStats = statsMap.putIfAbsent(functionId, stats);
if (newStats == null) {
return; // data stored in constructor of PerFunction above
}
stats = newStats;
}
updateStatistics(stats, invocationCount, executionNanos, dataInputBytes, dataOutputBytes);
checkAndSendStatistics();
}
/**
* Gets the configuration data.
*
* @param calculationConfiguration the configuration key, not null
* @return the configuration map, not null
*/
protected ConcurrentMap<String, PerFunction> getConfigurationData(final String calculationConfiguration) {
ConcurrentMap<String, PerFunction> data = _data.get(calculationConfiguration);
if (data == null) {
_data.putIfAbsent(calculationConfiguration, new ConcurrentHashMap<String, PerFunction>());
data = _data.get(calculationConfiguration);
}
return data;
}
/**
* Updates the statistics after a successful function invocation.
*
* @param stats the statistics to update, not null
* @param invocationCount the number of invocations the data is for
* @param executionNanos the execution time, in nanoseconds, of the invocation(s)
* @param dataInputBytes the mean data input, bytes per input node, or {@code NaN} if unavailable
* @param dataOutputBytes the mean data output, bytes per output node, or {@code NaN} if unavailable
*/
protected void updateStatistics(final PerFunction stats, final int invocationCount, final double executionNanos, final double dataInputBytes, final double dataOutputBytes) {
synchronized (stats) {
stats.setInvocation(stats.getInvocation() + executionNanos * _invocationTimeScale);
if (Double.isNaN(dataInputBytes)) {
// no data available, so increase at previous rate to keep average the same
stats.setDataInput(stats.getDataInput() * (1.0 + (double) invocationCount / (double) stats.getCount()));
} else {
stats.setDataInput(stats.getDataInput() + dataInputBytes);
}
if (Double.isNaN(dataOutputBytes)) {
// no data available, so increase at previous rate to keep average the same
stats.setDataOutput(stats.getDataOutput() * (1.0 + (double) invocationCount / (double) stats.getCount()));
} else {
stats.setDataOutput(stats.getDataOutput() + dataOutputBytes);
}
stats.setCount(stats.getCount() + invocationCount);
}
}
/**
* Checks if it is time to send the statistics, if so then send them.
*/
protected void checkAndSendStatistics() {
long timeNow = System.nanoTime();
long lastSent = _lastSent.get();
if (lastSent + _frequencyNanos < timeNow) {
if (_lastSent.compareAndSet(lastSent, timeNow)) {
getExecutorService().execute(new Runnable() {
@Override
public void run() {
sendStatistics();
}
});
}
}
}
/**
* Sends the statistics to the central location.
*/
protected void sendStatistics() {
final List<PerConfiguration> configurations = new ArrayList<PerConfiguration>(_data.size());
final Iterator<Map.Entry<String, ConcurrentMap<String, PerFunction>>> configurationIterator = _data.entrySet().iterator();
while (configurationIterator.hasNext()) {
final Map.Entry<String, ConcurrentMap<String, PerFunction>> configuration = configurationIterator.next();
// Note the race condition in this logic; it is possible we may lose data if functionInvoked is called
// while we're doing this. Hopefully it won't happen often enough to be problematic.
// We're only gathering heuristics so as long as it isn't a rarely executing function that always gets missed we'll be okay!
configurationIterator.remove();
if (!configuration.getValue().isEmpty()) {
final List<PerFunction> functionData = new ArrayList<PerFunction>(configuration.getValue().size());
for (PerFunction function : configuration.getValue().values()) {
synchronized (function) {
functionData.add(function.clone());
}
}
configurations.add(new PerConfiguration(configuration.getKey(), functionData));
}
}
final MutableFudgeMsg message = getFudgeMessageSender().getFudgeContext().newMessage();
FudgeSerializer.addClassHeader(message, Invocations.class, RemoteCalcNodeMessage.class);
new Invocations(configurations).toFudgeMsg(new FudgeSerializer(getFudgeMessageSender().getFudgeContext()), message);
getFudgeMessageSender().send(message);
}
/**
* Flushes by sending statistics.
*/
public void flush() {
sendStatistics();
}
}