/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb.client.exampleutils; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.voltdb.client.ClientResponse; /** * A thread-safe performance counter used to track procedure/statement execution statistics. * * @author Seb Coursol * @since 2.0 */ public class PerfCounter implements Cloneable { private static final Lock lock = new ReentrantLock(); private long StartTime = Long.MAX_VALUE; private long EndTime = Long.MIN_VALUE; private long min = 999999999l; private long max = -1l; private long tot = 0l; private long cnt = 0l; private long err = 0l; /* * The buckets cover 0 - 500ms+. * * The first 100 buckets cover 0 - 100ms, 1ms each (e.g. (0, 1ms]). The next * 8 buckets cover 100 - 500ms, 50ms each. The last one covers 500ms+. */ private long[] lat = new long[109]; /** * Creates a new performance counter and immediately starts tracking time (for rate/second calculations). */ public PerfCounter() { this(true); } /** * Creates a new performance counter, optionally starting time tracking. * * @param start the flag indicating whether time tracking should be started immediately. When false time tracking is started on the first counter update. */ public PerfCounter(boolean start) { if (start) StartTime = System.currentTimeMillis(); } /** * Gets the time (in milliseconds since 1/1/1970 00:00 UTC) at which this counter's time tracking began (the time at which this counter was created - if created with the start parameter set to true - or the time of the first update). */ public long getStartTime() { return StartTime; } /** * Gets the time (in milliseconds since 1/1/1970 00:00 UTC) at which this counter's time tracking ended (the time if the last update). */ public long getEndTime() { return EndTime; } /** * Gets the minimum execution latency tracked by this counter. */ public long getMinLatency() { return min; } /** * Gets the maximum execution latency tracked by this counter. */ public long getMaxLatency() { return max; } /** * Gets the total execution duration tracked by this counter (the sum of execution durations of all calls tracked by this counter). */ public long getTotalExecutionDuration() { return tot; } /** * Gets the number of execution calls tracked by this counter. */ public long getExecutionCount() { return cnt; } /** * Gets the number of execution errors tracked by this counter. */ public long getErrorCount() { return err; } /** * Gets the latency distribution buckets of execution calls tracked by this counter. */ public long[] getLatencyBuckets() { return lat; } /** * Gets the elapsed duration between the start and end time of this counter. */ public long getElapsedDuration() { return this.EndTime-this.StartTime; } /** * Gets the average number of execution calls per second for all execution calls tracked by this counter. */ public double getTransactionRatePerSecond() { return getExecutionCount()*1000d/getElapsedDuration(); } /** * Gets the average execution latency for calls tracked by this counter. */ public double getAverageLatency() { return (double)getTotalExecutionDuration()/(double)getExecutionCount(); } /** * Tracks a call execution by processing the ClientResponse sent back by the VoltDB server. * * @param response the response sent by the VoltDB server, containing details about the procedure/statement execution. */ public void update(ClientResponse response) { this.update(response.getClientRoundtrip(), response.getStatus() == ClientResponse.SUCCESS); } /** * Tracks a generic call execution by reporting the execution duration. This method should be used for successful calls only. * * @param executionDuration the duration of the execution call to track in this counter. * @see #update(long executionDuration, boolean success) */ public void update(long executionDuration) { this.update(executionDuration, true); } /** * Tracks a generic call execution by reporting the execution duration. This method should be used for successful calls only. * * @param executionDuration the duration of the execution call to track in this counter. * @param success the flag indicating whether the execution call was successful. */ public void update(long executionDuration, boolean success) { lock.lock(); try { EndTime = System.currentTimeMillis(); if (StartTime == Long.MAX_VALUE) StartTime = EndTime-executionDuration; cnt++; tot+= executionDuration; if (min > executionDuration) min = executionDuration; if (max < executionDuration) max = executionDuration; int bucket = (int) executionDuration; if (executionDuration > 100) bucket = Math.min((int)((executionDuration-100l)/50l),8) + 100; lat[bucket]++; if (!success) err++; } finally { lock.unlock(); } } /** * Gets a representation of this counter as a single-line short-format string detailing the statistics tracked by this counter. * * @return the string representation of this counter. * @see #toString(boolean useSimpleFormat) */ @Override public String toString() { return toString(true); } /** * Gets a representation of this counter as a string detailing the statistics tracked by this counter. * * @param useSimpleFormat the flag indicating whether to use a short one-line format, or detailed statistics including latency bucketing. * @return the string representation of this counter. */ public String toString(boolean useSimpleFormat) { SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); long elapsedDuration = (this.StartTime == Long.MAX_VALUE) ? 1 : this.EndTime-this.StartTime; if (useSimpleFormat) return String.format( "%8s | Txn.: %,11d%s @ %,11.1f TPS | Lat. = %7d < %7.2f < %7d\n" , dateFormat.format(new Date(Math.round(elapsedDuration/1000d)*1000l)) , this.cnt , this.err > 0 ? String.format(" [!%,11d]", this.err) : "" , (this.cnt*1000d / (double)elapsedDuration) , this.min == 999999999l ? 0l : this.min , (double)this.tot/(double)this.cnt , this.max == -1l ? 0l : this.max ); else { long[] coarseLat = new long[9]; // Roll up the latencies below 100ms for (int i = 0; i < 4; i++) { for (int j = 0; j < 25; j++) { coarseLat[i] += this.lat[i * 25 + j]; } } // Roll up the rest for (int i = 4; i < 8; i++) { coarseLat[i] = this.lat[100 + i - 4]; } for (int j = 104; j < this.lat.length; j++) { coarseLat[8] += this.lat[j]; } return String.format( "-------------------------------------------------------------------------------------\n" + "Final: | Txn.: %,11d%s @ %,11.1f TPS | Lat. = %7d < %7.2f < %7d\n" + "-------------------------------------------------------------------------------------\n" + "Lat.: 25 < 50 < 75 < 100 < 150 < 200 < 250 < 300 < 300+\n" + "-------------------------------------------------------------------------------------\n" + "%% %6.2f | %6.2f | %6.2f | %6.2f | %6.2f | %6.2f | %6.2f | %6.2f | %6.2f\n" , this.cnt , this.err > 0 ? String.format(" [!%,11d]", this.err) : "" , (this.cnt*1000d / elapsedDuration) , this.min == 999999999l ? 0l : this.min , (double)this.tot/(double)this.cnt , this.max == -1l ? 0l : this.max , 100*(double)coarseLat[0]/this.cnt , 100*(double)coarseLat[1]/this.cnt , 100*(double)coarseLat[2]/this.cnt , 100*(double)coarseLat[3]/this.cnt , 100*(double)coarseLat[4]/this.cnt , 100*(double)coarseLat[5]/this.cnt , 100*(double)coarseLat[6]/this.cnt , 100*(double)coarseLat[7]/this.cnt , 100*(double)coarseLat[8]/this.cnt ); } } /** * Format the statistics into a delimiter separated string. * * Currently, the format is * "start (ms), end (ms), total proc calls, min lat., max lat., lat. buckets..." * * @param delimiter Delimiter to separate values, e.g. ',' or '\t' * @return */ public String toRawString(char delimiter) { StringBuilder sb = new StringBuilder(); sb.append(getStartTime()) .append(delimiter) .append(getEndTime()) .append(delimiter) .append(getExecutionCount()) .append(delimiter) .append(getMinLatency()) .append(delimiter) .append(getMaxLatency()); // There are 109 buckets for (long latency : getLatencyBuckets()) { sb.append(delimiter).append(latency); } return sb.toString(); } /** * Clones this counter to provide a snapshot copy of statistics. * * @return the snapshot clone of this counter. */ @Override public PerfCounter clone() { PerfCounter counter = new PerfCounter(false); counter.StartTime = this.StartTime; counter.EndTime = this.EndTime; counter.min = this.min; counter.max = this.max; counter.tot = this.tot; counter.cnt = this.cnt; counter.err = this.err; for(int i=0;i<9;i++) counter.lat[i] = this.lat[i]; return counter; } /** * Merges the statistics of this counter with another counter. * * @param other the counter to merge statistics from. * @return the reference to the current counter after modification. Useful for command-chaining. */ public PerfCounter merge(PerfCounter other) { lock.lock(); try { this.StartTime = Math.min(this.StartTime, other.StartTime); this.EndTime = Math.max(this.EndTime, other.EndTime); this.min = Math.min(this.min,other.min); this.max = Math.max(this.max,other.max); this.tot = this.tot+other.tot; this.cnt = this.cnt+other.cnt; this.err = this.err+other.err; for(int i=0;i<109;i++) this.lat[i] = this.lat[i]+other.lat[i]; } finally { lock.unlock(); } return this; } /** * Merges the statistics of a list of counters. * * @param counters the list of counters to merge statistics from. * @return the new counter containing aggregated statistics from all the provided counters. */ public static PerfCounter merge(PerfCounter[] counters) { PerfCounter counter = counters[0].clone(); for(int i=1;i<counters.length;i++) counter.merge(counters[i]); return counter; } /** * Performs a difference between the statistics of this counter and those of a previously cloned counter, for incremental statistics tracking. * * @param previous the counter to compare statistics with to generate a difference. * @return the new counter containing the incremental statistics between the two counters. The original counter is left unchanged. */ public PerfCounter difference(PerfCounter previous) { PerfCounter diff = this.clone(); if (previous != null) { diff.StartTime = previous.EndTime; diff.tot = this.tot-previous.tot; diff.cnt = this.cnt-previous.cnt; diff.err = this.err-previous.err; for(int i=0;i<109;i++) diff.lat[i] = this.lat[i]-previous.lat[i]; } return diff; } }