/*************************************************************************
* Copyright 2009-2016 Eucalyptus Systems, Inc.
*
* 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 3 of the License.
*
* 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, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6750 Navigator Way, Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.util.metrics;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.log4j.Logger;
import com.eucalyptus.component.id.Eucalyptus;
import com.eucalyptus.system.Threads;
import com.eucalyptus.util.LockResource;
import com.google.common.collect.EvictingQueue;
import org.apache.commons.collections.map.LRUMap;
public class ThruputMetrics {
public static class DataPoint {
long collectionTimeMs;
long value;
public DataPoint(long value) {
this.collectionTimeMs = System.currentTimeMillis();
this.value = value;
}
}
public static class Aggregates {
final int count;
final double mean;
final double median;
final long firstQuartile;
final long thirdQuartile;
final long min;
final long max;
public Aggregates(int count, double mean, long firstQuartile, double median,
long thirdQuartile, long min, long max) {
this.count = count;
this.mean = mean;
this.median = median;
this.firstQuartile = firstQuartile;
this.thirdQuartile = thirdQuartile;
this.min = min;
this.max = max;
}
}
private static final Logger LOG = Logger.getLogger(ThruputMetrics.class);
private static final Map<MonitoredAction, EvictingQueue<DataPoint>> data = new HashMap<MonitoredAction,EvictingQueue<DataPoint>>();
private static final ReentrantReadWriteLock storageLock = new ReentrantReadWriteLock();
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss z");
private static final LRUMap paritalOperations = new LRUMap(10000);
private static String operationKey(MonitoredAction action, String id, boolean start) {
StringBuilder sb = new StringBuilder(start ? "S":"E");
// to make shorter keys
sb.append(" A:").append(action.ordinal()).append(" ID:").append(id);
return sb.toString();
}
/**
* Adds start time for monitored action that will be finished later.
* If the same action was already recorded as ended due to asynchronous communication,
* adds its execution time as a new data point.
*/
public static Future<Boolean> startOperation(final MonitoredAction action, final String id, final long startTime) {
return Threads.enqueue(Eucalyptus.class, ThruputMetrics.class,
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
String startKey = operationKey(action, id, true);
String endKey = operationKey(action, id, false);
Long endTime = (Long) paritalOperations.get(endKey);
if (endTime != null) {
paritalOperations.remove(endKey);
if (endTime - startTime > 0)
addDataPointNoThread(action, endTime - startTime);
} else {
paritalOperations.put(startKey, startTime);
}
return true;
}
});
}
/**
* Adds end time for monitored action that was started before and adds its execution
* time as a new data point.
*/
public static Future<Boolean> endOperation(final MonitoredAction action, final String id, final long endTime) {
return Threads.enqueue(Eucalyptus.class, ThruputMetrics.class,
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
String startKey = operationKey(action, id, true);
String endKey = operationKey(action, id, false);
Long startTime = (Long) paritalOperations.get(startKey);
if (startTime != null) {
paritalOperations.remove(startKey);
if (endTime - startTime > 0)
addDataPointNoThread(action, endTime - startTime);
} else {
paritalOperations.put(endKey, endTime);
}
return true;
}
});
}
private static void addDataPointNoThread(MonitoredAction action, long newDataPoint) {
try ( final LockResource lock = LockResource.lock(storageLock.writeLock()) ) {
if (data.containsKey(action)) {
data.get(action).add(new DataPoint(newDataPoint));
} else {
EvictingQueue<DataPoint> newQ = EvictingQueue.create(MetricsConfiguration.METRICS_COLLECTION_SIZE);
newQ.add(new DataPoint(newDataPoint));
data.put(action, newQ);
}
}
if (LOG.isTraceEnabled()) {
StringBuilder sb = new StringBuilder(action.name);
sb.append("=");
sb.append(newDataPoint);
LOG.trace(sb.toString());
}
}
private static final class QuickFuture implements Future<Boolean> {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return true;
}
@Override
public Boolean get() throws InterruptedException, ExecutionException {
return true;
}
@Override
public Boolean get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return true;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return true;
}
}
private static final QuickFuture emptyCallable = new QuickFuture();
/**
* Adds new data point (non-negative long) for storing and logging.
* Function ignores negative input values
*/
public static Future<Boolean> addDataPoint(final MonitoredAction action, final long newDataPoint) {
if (newDataPoint < 0)
return emptyCallable;
return Threads.enqueue(Eucalyptus.class, ThruputMetrics.class,
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
if (newDataPoint < 0)
throw new IllegalArgumentException();
addDataPointNoThread(action, newDataPoint);
return true;
}
});
}
/**
* Returns all known data point for an action.
*/
public static DataPoint[] getDataPoints(MonitoredAction action) {
try ( final LockResource lock = LockResource.lock(storageLock.readLock()) ) {
DataPoint[] ret;
if (data.containsKey(action)) {
ret = data.get(action).toArray(new DataPoint[0]);
} else {
ret = new DataPoint[0];
}
return ret;
}
}
/**
* Clean data points for a giving action
*/
public static void clearDatapoints(MonitoredAction action) {
try ( final LockResource lock = LockResource.lock(storageLock.writeLock()) ) {
if (data.containsKey(action))
data.get(action).clear();
}
}
/**
* Clean all data points
*/
public static void clearAllDatapoints() {
try ( final LockResource lock = LockResource.lock(storageLock.writeLock()) ) {
for(MonitoredAction action : MonitoredAction.values()) {
if (data.containsKey(action))
data.get(action).clear();
}
}
}
/**
* Returns mean, first quartile, median, third quartile, min, max, and count
* for all known data point for an action.
* Warning: function does not provide valid results if sum of all values exceeds Long.MAX_VALUE.
*/
public static Aggregates getAggregates(MonitoredAction action) {
DataPoint[] l = getDataPoints(action);
if (l.length == 0)
return new Aggregates(0, 0.0, 0, 0.0, 0, 0, 0);
long val[] = new long[l.length];
int i=0;
long sum = 0;
for(DataPoint p:l){
val[i] = p.value;
sum += p.value;
i++;
}
Arrays.sort(val);
double median = ((val.length & 1) == 1) ? val[val.length/2]/1.0d : (val[(val.length-1)/2] + val[(val.length+1)/2])/2.0d;
// percentiles are calculated using Nearest Rank method
long firstQuartile = val.length > 1 ? val[(int)Math.round(25*val.length/100.0) - 1] : val[0];
long thirdQuartile = val.length > 1 ? val[(int)Math.round(75*val.length/100.0) - 1] : val[0];
if (sum < 0) {
// assuming sum did not exceeded 1.5 * Long.MAX_VALUE
LOG.error("Max long value is exceeded while calculating aggregates for metrics.");
return new Aggregates(val.length, Double.NaN, firstQuartile, median, thirdQuartile, val[0], val[val.length-1]);
}
return new Aggregates(val.length, sum/1.0d/val.length, firstQuartile, median, thirdQuartile, val[0], val[val.length-1]);
}
/*
* Return last N data points as a string
* [action]
* [Time] [Value]
*/
public static String getDataPoints(int count) {
StringBuilder sb = new StringBuilder();
try ( final LockResource lock = LockResource.lock(storageLock.readLock()) ) {
for(MonitoredAction action : MonitoredAction.values()){
DataPoint[] dataPoints = getDataPoints(action);
if (dataPoints.length == 0)
continue;
sb.append(action.name).append("\n");
for(int i = dataPoints.length > count ? dataPoints.length - count : 0; i < dataPoints.length; i++)
sb.append(dateFormat.format( new Date(dataPoints[i].collectionTimeMs) )).append("\t")
.append(dataPoints[i].value).append("\n");
}
}
return sb.toString();
}
public static void changeSize(int newSize) {
try ( final LockResource lock = LockResource.lock(storageLock.writeLock()) ) {
for(MonitoredAction action : MonitoredAction.values()){
if (data.containsKey(action)) {
EvictingQueue<DataPoint> newQ = EvictingQueue.create(newSize);
DataPoint[] values = data.get(action).toArray(new DataPoint[0]);
for(int i = values.length > newSize ? values.length - newSize : 0; i < values.length; i++)
newQ.add(values[i]);
data.put(action, newQ);
}
}
}
}
}