/*
* 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 org.apache.log4j.Logger;
import com.facebook.LinkBench.ConfigUtil;
import com.facebook.LinkBench.LinkBenchOp;
import com.facebook.LinkBench.LinkStore;
/**
* Class used to track and compute latency statistics, particularly
* percentiles. Times are divided into buckets, with counts maintained
* per bucket. The division into buckets is based on typical latencies
* for database operations: most are in the range of 0.1ms to 100ms.
* we have 0.1ms-granularity buckets up to 1ms, then 1ms-granularity from
* 1-100ms, then 100ms-granularity, and then 1s-granularity.
*/
public class LatencyStats {
public static int MAX_MILLIS = 100;
/**
* Keep track of running mean per thread and op type
*/
private RunningMean means[][];
/** Final means per op type */
double finalMeans[];
// Displayed along with stats
private int maxThreads;
public LatencyStats(int maxThreads) {
this.maxThreads = maxThreads;
means = new RunningMean[maxThreads][LinkStore.MAX_OPTYPES];
bucketCounts = new long[maxThreads][LinkStore.MAX_OPTYPES][NUM_BUCKETS];
maxLatency = new long[maxThreads][LinkStore.MAX_OPTYPES];
}
private static final int SUB_MS_BUCKETS = 10; // Sub-ms granularity
private static final int MS_BUCKETS = 99; // ms-granularity buckets
private static final int HUNDREDMS_BUCKETS = 9; // 100ms-granularity buckets
private static final int SEC_BUCKETS = 9; // 1s-granularity buckets
public static final int NUM_BUCKETS = SUB_MS_BUCKETS + MS_BUCKETS +
HUNDREDMS_BUCKETS + SEC_BUCKETS + 1;
/** Counts of operations falling into each bucket */
private final long bucketCounts[][][];
/** Counts of samples per type */
private long sampleCounts[];
/** Cumulative bucket counts keyed by type, bucket# (calculated at end) */
private long bucketCountsCumulative[][];
/** Maximum latency by thread and type */
private long maxLatency[][];
public static int latencyToBucket(long microTime) {
long ms = 1000;
long msTime = microTime / ms; // Floored
if (msTime == 0) {
// Bucket per 0.1 ms
return (int) (microTime / 100);
} else if (msTime < 100) {
// msBucket = 0 means 1-2 ms
int msBucket = (int) msTime - 1;
// Bucket per ms
return SUB_MS_BUCKETS + msBucket;
} else if (msTime < 1000){
int hundredMSBucket = (int) ( msTime / 100 ) - 1;
return SUB_MS_BUCKETS + MS_BUCKETS + hundredMSBucket;
} else if (msTime < 10000) {
int secBucket = (int) (msTime / 1000) - 1;
return SUB_MS_BUCKETS + MS_BUCKETS + HUNDREDMS_BUCKETS + secBucket;
} else {
return NUM_BUCKETS - 1;
}
}
/**
*
* @param bucket
* @return inclusive min and exclusive max time in microsecs for bucket
*/
public static long[] bucketBound(int bucket) {
int ms = 1000;
long s = ms * 1000;
long res[] = new long[2];
if (bucket < SUB_MS_BUCKETS) {
res[0] = bucket * 100;
res[1] = (bucket+1) * 100;
} else if (bucket < SUB_MS_BUCKETS + MS_BUCKETS) {
res[0] = (bucket - SUB_MS_BUCKETS + 1) * ms;
res[1] = (bucket - SUB_MS_BUCKETS + 2) * ms;
} else if (bucket < SUB_MS_BUCKETS + MS_BUCKETS + HUNDREDMS_BUCKETS) {
int hundredMS = bucket - SUB_MS_BUCKETS - MS_BUCKETS + 1;
res[0] = hundredMS * 100 * ms;
res[1] = (hundredMS + 1) * 100 * ms;
} else if (bucket < SUB_MS_BUCKETS + MS_BUCKETS + HUNDREDMS_BUCKETS + SEC_BUCKETS) {
int secBucket = bucket - SUB_MS_BUCKETS - MS_BUCKETS - SEC_BUCKETS + 1;
res[0] = secBucket * s;
res[1] = (secBucket + 1) * s;
} else {
res[0] = (SEC_BUCKETS + 1)* s;
res[1] = 100 * s;
}
return res;
}
/**
* Used by the linkbench driver to record latency of each
* individual call
*/
public void recordLatency(int threadid, LinkBenchOp type,
long microtimetaken) {
long opBuckets[] = bucketCounts[threadid][type.ordinal()];
int bucket = latencyToBucket(microtimetaken);
opBuckets[bucket]++;
double time_ms = microtimetaken / 1000.0;
if (means[threadid][type.ordinal()] == null) {
means[threadid][type.ordinal()] = new RunningMean(time_ms);
} else {
means[threadid][type.ordinal()].addSample(time_ms);
}
if (maxLatency[threadid][type.ordinal()] < microtimetaken) {
maxLatency[threadid][type.ordinal()] = microtimetaken;
}
}
/**
* Print out percentile values
*/
public void displayLatencyStats() {
calcMeans();
calcCumulativeBuckets();
Logger logger = Logger.getLogger(ConfigUtil.LINKBENCH_LOGGER);
// print percentiles
for (LinkBenchOp type: LinkBenchOp.values()) {
if (sampleCounts[type.ordinal()] == 0) { // no samples of this type
continue;
}
DecimalFormat df = new DecimalFormat("#.###"); // Format to max 3 decimal place
logger.info(type.displayName() +
" count = " + sampleCounts[type.ordinal()] + " " +
" p25 = " + percentileString(type, 25) + "ms " +
" p50 = " + percentileString(type, 50) + "ms " +
" p75 = " + percentileString(type, 75) + "ms " +
" p95 = " + percentileString(type, 95) + "ms " +
" p99 = " + percentileString(type, 99) + "ms " +
" max = " + df.format(getMax(type)) + "ms " +
" mean = " + df.format(getMean(type))+ "ms");
}
}
public void printCSVStats(PrintStream out, boolean header) {
printCSVStats(out, header, LinkBenchOp.values());
}
public void printCSVStats(PrintStream out, boolean header, LinkBenchOp... ops) {
int percentiles[] = new int[] {25, 50, 75, 95, 99};
// Write out the header
if (header) {
out.print("op,count");
for (int percentile: percentiles) {
out.print(String.format(",p%d_low,p%d_high", percentile, percentile));
}
out.print(",max,mean");
out.println();
}
// Print in milliseconds down to 10us granularity
DecimalFormat df = new DecimalFormat("#.##");
for (LinkBenchOp op: ops) {
long samples = sampleCounts[op.ordinal()];
if (samples == 0) {
continue;
}
out.print(op.name());
out.print(",");
out.print(samples);
for (int percentile: percentiles) {
long bounds[] = getBucketBounds(op, percentile);
out.print(",");
out.print(df.format(bounds[0] / 1000.0));
out.print(",");
out.print(df.format(bounds[1] / 1000.0));
}
out.print(",");
out.print(df.format(getMax(op)));
out.print(",");
out.print(df.format(getMean(op)));
out.println();
}
}
/**
* Fill in the counts and means arrays
*/
private void calcMeans() {
sampleCounts = new long[LinkStore.MAX_OPTYPES];
finalMeans = new double[LinkStore.MAX_OPTYPES];
for (int i = 0; i < LinkStore.MAX_OPTYPES; i++) {
long samples = 0;
for (int thread = 0; thread < maxThreads; thread++) {
if (means[thread][i] != null) {
samples += means[thread][i].samples();
}
}
sampleCounts[i] = samples;
double weightedMean = 0.0;
for (int thread = 0; thread < maxThreads; thread++) {
if (means[thread][i] != null) {
weightedMean += (means[thread][i].samples() / (double) samples) *
means[thread][i].mean();
}
}
finalMeans[i] = weightedMean;
}
}
private void calcCumulativeBuckets() {
// Calculate the cumulative operation counts by bucket for each type
bucketCountsCumulative = new long[LinkStore.MAX_OPTYPES][NUM_BUCKETS];
for (int type = 0; type < LinkStore.MAX_OPTYPES; type++) {
long count = 0;
for (int bucket = 0; bucket < NUM_BUCKETS; bucket++) {
for (int thread = 0; thread < maxThreads; thread++) {
count += bucketCounts[thread][type][bucket];
}
bucketCountsCumulative[type][bucket] = count;
}
}
}
private long[] getBucketBounds(LinkBenchOp type, long percentile) {
long n = sampleCounts[type.ordinal()];
// neededRank is the rank of the sample at the desired percentile
long neededRank = (long) ((percentile / 100.0) * n);
int bucketNum = -1;
for (int i = 0; i < NUM_BUCKETS; i++) {
long rank = bucketCountsCumulative[type.ordinal()][i];
if (neededRank <= rank) {
// We have found the right bucket
bucketNum = i;
break;
}
}
assert(bucketNum >= 0); // Should definitely be found;
return bucketBound(bucketNum);
}
/**
*
* @return A human-readable string for the bucket bounds
*/
private String percentileString(LinkBenchOp type, long percentile) {
return boundsToString(getBucketBounds(type, percentile));
}
static String boundsToString(long[] bucketBounds) {
double minMs = bucketBounds[0] / 1000.0;
double maxMs = bucketBounds[1] / 1000.0;
DecimalFormat df = new DecimalFormat("#.##"); // Format to max 1 decimal place
return "["+ df.format(minMs) + "," + df.format(maxMs) + "]";
}
private double getMean(LinkBenchOp type) {
return finalMeans[type.ordinal()];
}
private double getMax(LinkBenchOp type) {
long max_us = 0;
for (int thread = 0; thread < maxThreads; thread++) {
max_us = Math.max(max_us, maxLatency[thread][type.ordinal()]);
}
return max_us / 1000.0;
}
}