/*
* Copyright 2012, Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.LinkBench.stats;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Random;
import org.apache.log4j.Logger;
import com.facebook.LinkBench.ConfigUtil;
import com.facebook.LinkBench.LinkBenchOp;
import com.facebook.LinkBench.LinkStore;
/**
* This class is used to keep track of statistics. It collects a sample of the
* total data (controlled by maxsamples) and can then compute statistics based
* on that sample.
*
* The overall idea is to compute reasonably accurate statistics using bounded
* space.
*
* Currently the class is used to print out stats at given intervals, with the
* sample taken over a given time interval and printed at the end of the interval.
*/
public class SampledStats {
// Actual number of operations per type that caller did
private long numops[];
// Max samples per type
private int maxsamples;
// samples for various optypes
private long[][] samples;
/** Number of operations that the sample is drawn from */
private int opsSinceReset[];
// minimums encounetered per operation type
private long minimums[];
// maximums encountered per operation type
private long maximums[];
// #errors encountered per type
private long errors[];
// Displayed along with stats
private int threadID;
private final Logger logger = Logger.getLogger(ConfigUtil.LINKBENCH_LOGGER);
/** Stream to write csv output to ( null if no csv output ) */
private final PrintStream csvOutput;
/** Random number generator used to decide which to include in sample */
private Random rng;
public SampledStats(int input_threadID,
int input_maxsamples, PrintStream csvOutput) {
threadID = input_threadID;
maxsamples = input_maxsamples;
this.csvOutput = csvOutput;
samples = new long[LinkStore.MAX_OPTYPES][maxsamples];
opsSinceReset = new int[LinkStore.MAX_OPTYPES];
minimums = new long[LinkStore.MAX_OPTYPES];
maximums = new long[LinkStore.MAX_OPTYPES];
numops = new long[LinkStore.MAX_OPTYPES];
errors = new long[LinkStore.MAX_OPTYPES];
rng = new Random();
csvOutput = null;
}
public void addStats(LinkBenchOp type, long timetaken, boolean error) {
if (error) {
errors[type.ordinal()]++;
}
if ((minimums[type.ordinal()] == 0) || (minimums[type.ordinal()] > timetaken)) {
minimums[type.ordinal()] = timetaken;
}
if (timetaken > maximums[type.ordinal()]) {
maximums[type.ordinal()] = timetaken;
}
numops[type.ordinal()]++;
int opIndex = opsSinceReset[type.ordinal()];
opsSinceReset[type.ordinal()]++;
if (opIndex < maxsamples) {
samples[type.ordinal()][opIndex] = timetaken;
} else {
// Replacing with the probability guarantees that each measurement
// has an equal probability of being included in the sample
double pReplace = ((double)maxsamples) / opIndex;
if (rng.nextDouble() < pReplace) {
// Select sample to replace randomly
samples[type.ordinal()][rng.nextInt(maxsamples)] = timetaken;
}
}
}
public void resetSamples() {
for (LinkBenchOp type: LinkBenchOp.values()) {
opsSinceReset[type.ordinal()] = 0;
}
}
/**
* display stats for samples from start (inclusive) to end (exclusive)
* @param type
* @param start
* @param end
* @param startTime_ms
* @param nowTime_ms
*/
private void displayStats(LinkBenchOp type, int start, int end,
long sampleStartTime_ms, long nowTime_ms) {
int elems = end - start;
long timestamp = nowTime_ms / 1000;
long sampleDuration = nowTime_ms - sampleStartTime_ms;
if (elems <= 0) {
logger.info("ThreadID = " + threadID +
" " + type.displayName() +
" totalops = " + numops[type.ordinal()] +
" totalErrors = " + errors[type.ordinal()] +
" ops = " + opsSinceReset[type.ordinal()] +
" sampleDuration = " + sampleDuration + "ms" +
" samples = " + elems);
if (csvOutput != null) {
csvOutput.println(threadID + "," + timestamp + "," + type.name() +
"," + numops[type.ordinal()] + "," + errors[type.ordinal()] +
"," + 0 + "," + sampleDuration +
",0,,,,,,,,,");
}
return;
}
// sort from start (inclusive) to end (exclusive)
Arrays.sort(samples[type.ordinal()], start, end);
RunningMean meanCalc = new RunningMean(samples[type.ordinal()][0]);
for (int i = start + 1; i < end; i++) {
meanCalc.addSample(samples[type.ordinal()][i]);
}
long min = samples[type.ordinal()][start];
long p25 = samples[type.ordinal()][start + elems/4];
long p50 = samples[type.ordinal()][start + elems/2];
long p75 = samples[type.ordinal()][end - 1 - elems/4];
long p90 = samples[type.ordinal()][end - 1 - elems/10];
long p95 = samples[type.ordinal()][end - 1 - elems/20];
long p99 = samples[type.ordinal()][end - 1 - elems/100];
long max = samples[type.ordinal()][end - 1];
double mean = meanCalc.mean();
DecimalFormat df = new DecimalFormat("#.##");
logger.info("ThreadID = " + threadID +
" " + type.displayName() +
" totalOps = " + numops[type.ordinal()] +
" totalErrors = " + errors[type.ordinal()] +
" ops = " + opsSinceReset[type.ordinal()] +
" sampleDuration = " + sampleDuration + "ms" +
" samples = " + elems +
" mean = " + df.format(mean) +
" min = " + min +
" 25% = " + p25 +
" 50% = " + p50 +
" 75% = " + p75 +
" 90% = " + p90 +
" 95% = " + p95 +
" 99% = " + p99 +
" max = " + max);
if (csvOutput != null) {
csvOutput.println(threadID + "," + timestamp + "," + type.name() +
"," + numops[type.ordinal()] + "," + errors[type.ordinal()] +
"," + opsSinceReset[type.ordinal()] + "," + sampleDuration +
"," + elems + "," + mean + "," + min + "," + p25 + "," + p50 +
"," + p75 + "," + p90 + "," + p95 + "," + p99 + "," + max);
}
}
/**
* Write a header with column names for a csv file showing progress
* @param out
*/
public static void writeCSVHeader(PrintStream out) {
out.println("threadID,timestamp,op,totalops,totalerrors,ops," +
"sampleDuration_us,sampleOps,mean_us,min_us,p25_us,p50_us," +
"p75_us,p90_us,p95_us,p99_us,max_us");
}
public void displayStatsAll(long sampleStartTime_ms, long nowTime_ms) {
displayStats(sampleStartTime_ms, nowTime_ms,
Arrays.asList(LinkBenchOp.values()));
}
public void displayStats(long sampleStartTime_ms, long nowTime_ms,
Collection<LinkBenchOp> ops) {
for (LinkBenchOp op: ops) {
displayStats(op, 0, Math.min(maxsamples, opsSinceReset[op.ordinal()]),
sampleStartTime_ms, nowTime_ms);
}
}
/**
* @return total operation count so far for type
*/
public long getCount(LinkBenchOp type) {
return this.numops[type.ordinal()];
}
}