BUG-1813: fix DurationStatsTracker performance
[yangtools.git] / common / util / src / main / java / org / opendaylight / yangtools / util / DurationStatsTracker.java
1 /*
2  * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8
9 package org.opendaylight.yangtools.util;
10
11 import static java.util.concurrent.TimeUnit.MICROSECONDS;
12 import static java.util.concurrent.TimeUnit.MILLISECONDS;
13 import static java.util.concurrent.TimeUnit.NANOSECONDS;
14 import static java.util.concurrent.TimeUnit.SECONDS;
15 import java.text.DecimalFormat;
16 import java.text.DecimalFormatSymbols;
17 import java.util.Date;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.atomic.AtomicLongFieldUpdater;
20 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
23
24 /**
25  * Class that calculates and tracks time duration statistics.
26  *
27  * @author Thomas Pantelis
28  * @author Robert Varga
29  */
30 public class DurationStatsTracker {
31     private static final AtomicReferenceFieldUpdater<DurationStatsTracker, DurationWithTime> LONGEST_UPDATER =
32             AtomicReferenceFieldUpdater.newUpdater(DurationStatsTracker.class, DurationWithTime.class, "longest");
33     private static final AtomicReferenceFieldUpdater<DurationStatsTracker, DurationWithTime> SHORTEST_UPDATER =
34             AtomicReferenceFieldUpdater.newUpdater(DurationStatsTracker.class, DurationWithTime.class, "shortest");
35     private static final AtomicLongFieldUpdater<DurationStatsTracker> COUNT_UPDATER =
36             AtomicLongFieldUpdater.newUpdater(DurationStatsTracker.class, "count");
37     private static final AtomicLongFieldUpdater<DurationStatsTracker> SUM_UPDATER =
38             AtomicLongFieldUpdater.newUpdater(DurationStatsTracker.class, "sum");
39
40     private static final Logger LOG = LoggerFactory.getLogger(DurationStatsTracker.class);
41     private static final DecimalFormat DECIMAL_FORMAT;
42
43     private volatile long sum = 0;
44     private volatile long count = 0;
45     private volatile DurationWithTime longest = null;
46     private volatile DurationWithTime shortest = null;
47
48     static {
49         final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance();
50         symbols.setDecimalSeparator('.');
51         DECIMAL_FORMAT = new DecimalFormat("0.00", symbols);
52     }
53
54     /**
55      * Add a duration to track.
56      *
57      * @param duration
58      *            the duration in nanoseconds.
59      */
60     public void addDuration(final long duration) {
61         // First update the quick stats
62         SUM_UPDATER.addAndGet(this, duration);
63         COUNT_UPDATER.incrementAndGet(this);
64
65         /*
66          * Now the hairy 'min/max' things. The notion of "now" we cache,
67          * so the first time we use it, we do not call it twice. We populate
68          * it lazily, though.
69          *
70          * The longest/shortest stats both are encapsulated in an object,
71          * so we update them atomically and we minimize the number of volatile
72          * operations.
73          */
74         DurationWithTime current = shortest;
75         if (current == null || current.getDuration() > duration) {
76             final DurationWithTime newObj = new DurationWithTime(duration, System.currentTimeMillis());
77             while (!SHORTEST_UPDATER.compareAndSet(this, current, newObj)) {
78                 current = shortest;
79                 if (current != null && current.getDuration() <= duration) {
80                     break;
81                 }
82             }
83         }
84
85         current = longest;
86         if (current == null || current.getDuration() < duration) {
87             final DurationWithTime newObj = new DurationWithTime(duration, System.currentTimeMillis());
88             while (!LONGEST_UPDATER.compareAndSet(this, current, newObj)) {
89                 current = longest;
90                 if (current != null && current.getDuration() >= duration) {
91                     break;
92                 }
93             }
94         }
95     }
96
97     /**
98      * Returns the total number of tracked durations.
99      */
100     public long getTotalDurations() {
101         return count;
102     }
103
104     private static long getDuration(final DurationWithTime current) {
105         return current == null ? 0L : current.getDuration();
106     }
107
108     private static long getTimeMillis(final DurationWithTime current) {
109         return current == null ? 0L : current.getTimeMillis();
110     }
111
112     /**
113      * Returns the longest duration in nanoseconds.
114      */
115     public long getLongestDuration() {
116         return getDuration(longest);
117     }
118
119     /**
120      * Returns the shortest duration in nanoseconds.
121      */
122     public long getShortestDuration() {
123         return getDuration(shortest);
124     }
125
126     /**
127      * Returns the average duration in nanoseconds.
128      */
129     public double getAverageDuration() {
130         final long mySum = sum;
131         final long myCount = count;
132
133         return myCount == 0 ? 0 : ((double) mySum) / myCount;
134     }
135
136     /**
137      * Returns the time stamp of the longest duration.
138      */
139     public long getTimeOfLongestDuration() {
140         return getTimeMillis(longest);
141     }
142
143     /**
144      * Returns the time stamp of the shortest duration.
145      */
146     public long getTimeOfShortestDuration() {
147         return getTimeMillis(shortest);
148     }
149
150     /**
151      * Resets all statistics back to their defaults.
152      */
153     public synchronized void reset() {
154         longest = null;
155         shortest = null;
156         count = 0;
157         sum = 0;
158     }
159
160     /**
161      * Returns the average duration as a displayable String with units, e.g.
162      * "12.34 ms".
163      */
164     public String getDisplayableAverageDuration() {
165         return formatDuration(getAverageDuration(), null);
166     }
167
168     /**
169      * Returns the shortest duration as a displayable String with units and the
170      * date/time at which it occurred, e.g. "12.34 ms at 08/02/2014 12:30:24".
171      */
172     public String getDisplayableShortestDuration() {
173         return formatDuration(shortest);
174     }
175
176     /**
177      * Returns the longest duration as a displayable String with units and the
178      * date/time at which it occurred, e.g. "12.34 ms at 08/02/2014 12:30:24".
179      */
180     public String getDisplayableLongestDuration() {
181         return formatDuration(longest);
182     }
183
184     /**
185      * Returns formatted value of number, e.g. "12.34". Always is used dot as
186      * decimal separator.
187      */
188     private static synchronized String formatDecimalValue(final double value) {
189         return DECIMAL_FORMAT.format(value);
190     }
191
192     private static String formatDuration(final DurationWithTime current) {
193         if (current != null) {
194             return formatDuration(current.getDuration(), current.getTimeMillis());
195         } else {
196             return formatDuration(0, null);
197         }
198     }
199
200     private static String formatDuration(final double duration, final Long timeStamp) {
201         final TimeUnit unit = chooseUnit((long) duration);
202         final double value = duration / NANOSECONDS.convert(1, unit);
203
204         final StringBuilder sb = new StringBuilder();
205         sb.append(formatDecimalValue(value));
206         sb.append(' ');
207         sb.append(abbreviate(unit));
208
209         if (timeStamp != null) {
210             sb.append(String.format(" at %1$tD %1$tT", new Date(timeStamp)));
211         }
212
213         return sb.toString();
214     }
215
216     private static TimeUnit chooseUnit(final long nanos) {
217         // TODO: this could be inlined, as we are doing needless divisions
218         if (NANOSECONDS.toSeconds(nanos) > 0) {
219             return SECONDS;
220         }
221         if (NANOSECONDS.toMillis(nanos) > 0) {
222             return MILLISECONDS;
223         }
224         if (NANOSECONDS.toMicros(nanos) > 0) {
225             return MICROSECONDS;
226         }
227         return NANOSECONDS;
228     }
229
230     private static String abbreviate(final TimeUnit unit) {
231         switch (unit) {
232         case NANOSECONDS:
233             return "ns";
234         case MICROSECONDS:
235             return "\u03bcs"; // μs
236         case MILLISECONDS:
237             return "ms";
238         case SECONDS:
239             return "s";
240         case MINUTES:
241             return "m";
242         case HOURS:
243             return "h";
244         case DAYS:
245             return "d";
246         }
247
248         LOG.warn("Unhandled time unit {}", unit);
249         return "";
250     }
251 }