/**
* Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.web.analytics;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections.buffer.CircularFifoBuffer;
import org.threeten.bp.Duration;
import org.threeten.bp.Instant;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.opengamma.engine.value.ComputedValueResult;
import com.opengamma.engine.value.ValueSpecification;
import com.opengamma.engine.view.AggregatedExecutionLog;
import com.opengamma.engine.view.ViewResultEntry;
import com.opengamma.engine.view.ViewResultModel;
import com.opengamma.financial.analytics.LocalDateLabelledMatrix1D;
import com.opengamma.id.ObjectId;
import com.opengamma.id.UniqueIdentifiable;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.money.CurrencyAmount;
/**
* <p>Cache of results from a running view process. This is intended for use with view clients where the first
* set of results is a full set and subsequent results are deltas. This cache maintains a full set of results
* which includes every target that has ever had a value calculated. It also keeps track of which values were
* updated in the previous calculation cycle.</p>
* <p>This class isn't thread safe.</p>
*/
/* package */ class ResultsCache {
/** Maximum number of history values stored for each item. */
private static final int MAX_HISTORY_SIZE = 20;
// is this likely to change? will it ever by dynamic? i.e. client specifies what types it wants history for?
/** Types of result values for which history is stored. */
private static final Set<Class<?>> s_historyTypes =
ImmutableSet.of(Double.class, BigDecimal.class, CurrencyAmount.class, LocalDateLabelledMatrix1D.class);
/** Empty result for types that don't have history, makes for cleaner code than using null. */
private static final Result s_emptyResult = Result.empty();
/** Empty result for types that have history, makes for cleaner code than using null. */
private static final Result s_emptyResultWithHistory = Result.emptyWithHistory();
/** The cached results. */
private final Map<ResultKey, CacheItem> _results = Maps.newHashMap();
/** Cache of portfolio entities, i.e. trades, positions, securities */
private final Map<ObjectId, CacheItem> _entities = Maps.newHashMap();
/** ID that's incremented each time results are received, used for keeping track of which items were updated. */
private long _lastUpdateId;
/** Duration of the last calculation cycle. */
private Duration _lastCalculationDuration = Duration.ZERO;
/** Last valuation time */
private Instant _valuationTime = Instant.MIN;
/**
* Puts a set of main grid results into the cache.
* @param results The results, not null
*/
/* package */ void put(ViewResultModel results) {
ArgumentChecker.notNull(results, "results");
_lastUpdateId++;
_lastCalculationDuration = results.getCalculationDuration();
_valuationTime = results.getViewCycleExecutionOptions().getValuationTime();
List<ViewResultEntry> allResults = results.getAllResults();
Set<ResultKey> updatedKeys = Sets.newHashSet();
for (ViewResultEntry result : allResults) {
put(result.getCalculationConfiguration(), result.getComputedValue());
updatedKeys.add(new ResultKey(result.getCalculationConfiguration(), result.getComputedValue().getSpecification()));
}
// duplicate the last history item for anything that hasn't changed this cycle
for (Map.Entry<ResultKey, CacheItem> entry : _results.entrySet()) {
if (entry.getValue().getHistory() != null && !updatedKeys.contains(entry.getKey())) {
entry.getValue().valueUnchanged();
}
}
}
/**
* Puts a set of dependency graph results into the cache.
* @param calcConfigName The name of the calculation configuration used to calculate the results
* @param results The results
* @param duration Duration of the calculation cycle that produced the results
*/
/* package */ void put(String calcConfigName, Map<ValueSpecification, ComputedValueResult> results, Duration duration) {
_lastUpdateId++;
_lastCalculationDuration = duration;
for (ComputedValueResult result : results.values()) {
put(calcConfigName, result);
}
}
/**
* Puts a single value into the cache.
* @param calcConfigName The name of the calculation configuration used to calculate the results
* @param result The result value and associated data
*/
private void put(String calcConfigName, ComputedValueResult result) {
ValueSpecification spec = result.getSpecification();
Object value = result.getValue();
ResultKey key = new ResultKey(calcConfigName, spec);
CacheItem cacheResult = _results.get(key);
if (cacheResult == null) {
_results.put(key, new CacheItem(value, result.getAggregatedExecutionLog(), _lastUpdateId));
} else {
cacheResult.setLatestValue(value, result.getAggregatedExecutionLog(), _lastUpdateId);
}
}
/* package */ void put(List<UniqueIdentifiable> entities) {
ArgumentChecker.notNull(entities, "entities");
_lastUpdateId++;
for (UniqueIdentifiable entity : entities) {
// TODO why is this failing sometimes?
//ArgumentChecker.notNull(entity, "entity");
if (entity != null) {
putEntity(entity);
}
}
}
/* package */ void put(UniqueIdentifiable entity) {
ArgumentChecker.notNull(entity, "entity");
++_lastUpdateId;
putEntity(entity);
}
/* package */ void remove(ObjectId id) {
++_lastUpdateId;
_entities.remove(id);
}
private void putEntity(UniqueIdentifiable entity) {
ObjectId id = entity.getUniqueId().getObjectId();
CacheItem cacheResult = _entities.get(id);
if (cacheResult == null) {
_entities.put(id, new CacheItem(entity, null, _lastUpdateId));
} else {
cacheResult.setLatestValue(entity, null, _lastUpdateId);
}
}
/* package */ Result getEntity(ObjectId id) {
CacheItem item = _entities.get(id);
if (item != null) {
// flag whether this result was updated by the last set of results that were put into the cache
boolean updatedByLastResults = (item.getLastUpdateId() == _lastUpdateId);
return Result.forValue(item.getValue(), null, null, updatedByLastResults);
} else {
return s_emptyResult;
}
}
/**
* Returns a cache result for a value specification and calculation configuration.
* @param calcConfigName The calculation configuration name
* @param valueSpec The value specification
* @param columnType The expected type of the value, used to decide whether empty history should be provided for
* a value that isn't in the cache but would have history if it were. Can be null in which case no history is
* provided for missing values.
* @return A cache result, not null
*/
/* package */ Result getResult(String calcConfigName, ValueSpecification valueSpec, Class<?> columnType) {
CacheItem item = _results.get(new ResultKey(calcConfigName, valueSpec));
if (item != null) {
// flag whether this result was updated by the last set of results that were put into the cache
boolean updatedByLastResults = (item.getLastUpdateId() == _lastUpdateId);
return Result.forValue(item.getValue(), item.getHistory(), item.getAggregatedExecutionLog(), updatedByLastResults);
} else {
if (s_historyTypes.contains(columnType)) {
return s_emptyResultWithHistory;
} else {
return s_emptyResult;
}
}
}
/**
* @return Duration of the last calculation cycle
*/
/* package */ Duration getLastCalculationDuration() {
return _lastCalculationDuration;
}
/**
* Gets the lastCalculationTime.
* @return the lastCalculationTime
*/
/* package */ Instant getValuationTime() {
return _valuationTime;
}
/**
* Returns empty history appropriate for the type. For types that support history it will be an empty collection,
* for types that don't it will be null.
* @param type The type, possibly null
* @return The history, possibly null
*/
/* package */ Collection<Object> emptyHistory(Class<?> type) {
if (s_historyTypes.contains(type)) {
return Collections.emptyList();
} else {
return null;
}
}
/**
* An item from the cache including its history and a flag indicating whether it was updated by the most recent
* calculation cycle. Instances of this class are intended for users of the cache.
*/
/* package */ static final class Result {
private final Object _value;
private final Collection<Object> _history;
private final boolean _updated;
private final AggregatedExecutionLog _aggregatedExecutionLog;
private Result(Object value, Collection<Object> history, AggregatedExecutionLog aggregatedExecutionLog, boolean updated) {
_value = value;
_history = history;
_aggregatedExecutionLog = aggregatedExecutionLog;
_updated = updated;
}
/**
* @return The most recent value, null if no value has ever been calculated for the requirement
*/
/* package */ Object getValue() {
return _value;
}
/**
* @return The history for the value, empty if no value has been calculated, null if history isn't stored for the
* requirement
*/
/* package */ Collection<Object> getHistory() {
return _history;
}
/**
* @return true if the value was updated by the most recent calculation cycle
*/
/* package */ boolean isUpdated() {
return _updated;
}
private static Result forValue(Object value,
Collection<Object> history,
AggregatedExecutionLog aggregatedExecutionLog,
boolean updated) {
ArgumentChecker.notNull(value, "value");
return new Result(value, history, aggregatedExecutionLog, updated);
}
/**
* @return A result with no value and no history, for value requirements that never have history
*/
private static Result empty() {
return new Result(null, null, null, false);
}
/**
* @return A result with no value and empty history, for value requirements that can have history
*/
private static Result emptyWithHistory() {
return new Result(null, Collections.emptyList(), null, false);
}
/* package */ AggregatedExecutionLog getAggregatedExecutionLog() {
return _aggregatedExecutionLog;
}
}
/**
* An item stored in the cache, this is an internal implementation detail.
*/
private static final class CacheItem {
private Collection<Object> _history;
private Object _latestValue;
private long _lastUpdateId = -1;
private AggregatedExecutionLog _aggregatedExecutionLog;
private CacheItem(Object value, AggregatedExecutionLog executionLog, long lastUpdateId) {
setLatestValue(value, executionLog, lastUpdateId);
}
/**
* Sets the latest value and the ID of the update that calculated it.
* @param latestValue The value
* @param executionLog The execution log associated generated when calculating the value
* @param lastUpdateId ID of the set of results that calculated it
*/
@SuppressWarnings("unchecked")
private void setLatestValue(Object latestValue, AggregatedExecutionLog executionLog, long lastUpdateId) {
ArgumentChecker.notNull(latestValue, "latestValue");
_latestValue = latestValue;
_lastUpdateId = lastUpdateId;
_aggregatedExecutionLog = executionLog;
// this can happen if the first value is an error and then real values arrive. this is possible if market
// data subscriptions take time to set up. in that case the history will initially be null (because error
// sentinel types aren't in s_historyTypes) and then when a valid value arrives the type can be checked and
// history created if required
if (_history == null && s_historyTypes.contains(latestValue.getClass())) {
_history = new CircularFifoBuffer(MAX_HISTORY_SIZE);
}
if (_history != null) {
_history.add(latestValue);
}
}
private Object getValue() {
return _latestValue;
}
/* package */ Collection<Object> getHistory() {
if (_history != null) {
return Collections.unmodifiableCollection(_history);
} else {
return null;
}
}
/**
* @return ID of the set of results that updated this item, used to decide whether the item was updated by
* the most recent calculation cycle.
*/
private long getLastUpdateId() {
return _lastUpdateId;
}
private AggregatedExecutionLog getAggregatedExecutionLog() {
return _aggregatedExecutionLog;
}
/**
* Invoked when a calculation cycle completes and doesn't update the value for an item. The latest value is
* inserted into the history again to ensure the history is up to date.
*/
private void valueUnchanged() {
_history.add(_latestValue);
}
}
/**
* Immutable key for items in the cache, this is in implelemtation detail.
*/
private static final class ResultKey {
private final String _calcConfigName;
private final ValueSpecification _valueSpec;
private ResultKey(String calcConfigName, ValueSpecification valueSpec) {
_calcConfigName = calcConfigName;
_valueSpec = valueSpec;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ResultKey resultKey = (ResultKey) o;
if (!_calcConfigName.equals(resultKey._calcConfigName)) {
return false;
}
return _valueSpec.equals(resultKey._valueSpec);
}
@Override
public int hashCode() {
int result = _calcConfigName.hashCode();
result = 31 * result + _valueSpec.hashCode();
return result;
}
@Override
public String toString() {
return _valueSpec.toString() + "/" + _calcConfigName;
}
}
}