c64053a8ce238c6bb5c4d727fb7843ebbbbf070b
[folly.git] / folly / stats / BucketedTimeSeries.h
1 /*
2  * Copyright 2016 Facebook, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #pragma once
18
19 #include <chrono>
20 #include <vector>
21
22 #include <folly/detail/Stats.h>
23
24 namespace folly {
25
26 /*
27  * A helper clock type to helper older code using BucketedTimeSeries with
28  * std::chrono::seconds transition to properly using clock types and time_point
29  * objects.
30  */
31 template <typename TT = std::chrono::seconds>
32 class LegacyStatsClock {
33  public:
34   using duration = TT;
35   using time_point = std::chrono::time_point<LegacyStatsClock, TT>;
36
37   // This clock does not actually implement now(), since the older API
38   // did not really specify what clock should be used.  (In practice most
39   // callers unfortuantely used wall clock time rather than a monotonic clock.)
40 };
41
42 /*
43  * This class represents a bucketed time series which keeps track of values
44  * added in the recent past, and merges these values together into a fixed
45  * number of buckets to keep a lid on memory use if the number of values
46  * added is very large.
47  *
48  * For example, a BucketedTimeSeries() with duration == 60s and 10 buckets
49  * will keep track of 10 6-second buckets, and discard all data added more
50  * than 1 minute ago.  As time ticks by, a 6-second bucket at a time will
51  * be discarded and new data will go into the newly opened bucket.  Internally,
52  * it uses a circular array of buckets that it reuses as time advances.
53  *
54  * This class assumes that time advances forwards.  The window of time tracked
55  * by the timeseries will advance forwards whenever a more recent timestamp is
56  * passed to addValue().  While it is possible to pass old time values to
57  * addValue(), this will never move the time window backwards.  If the old time
58  * value falls outside the tracked window of time, the data point will be
59  * ignored.
60  *
61  * This class is not thread-safe -- use your own synchronization!
62  */
63 template <typename VT, typename CT = LegacyStatsClock<std::chrono::seconds>>
64 class BucketedTimeSeries {
65  public:
66   using ValueType = VT;
67   using Clock = CT;
68   using Duration = typename Clock::duration;
69   using TimePoint = typename Clock::time_point;
70   // The legacy TimeType.  The older code used this instead of Duration and
71   // TimePoint.  This will eventually be removed as the code is transitioned to
72   // Duration and TimePoint.
73   using TimeType = typename Clock::duration;
74   using Bucket = detail::Bucket<ValueType>;
75
76   /*
77    * Create a new BucketedTimeSeries.
78    *
79    * This creates a new BucketedTimeSeries with the specified number of
80    * buckets, storing data for the specified amount of time.
81    *
82    * If the duration is 0, the BucketedTimeSeries will track data forever,
83    * and does not need the rolling buckets.  The numBuckets parameter is
84    * ignored when duration is 0.
85    */
86   BucketedTimeSeries(size_t numBuckets, Duration duration);
87
88   /*
89    * Adds the value 'val' at time 'now'
90    *
91    * This function expects time to generally move forwards.  The window of time
92    * tracked by this time series will move forwards with time.  If 'now' is
93    * more recent than any time previously seen, addValue() will automatically
94    * call update(now) to advance the time window tracked by this data
95    * structure.
96    *
97    * Values in the recent past may be added to the data structure by passing in
98    * a slightly older value of 'now', as long as this time point still falls
99    * within the tracked duration.  If 'now' is older than the tracked duration
100    * of time, the data point value will be ignored, and addValue() will return
101    * false without doing anything else.
102    *
103    * Returns true on success, or false if now was older than the tracked time
104    * window.
105    */
106   bool addValue(TimeType now, const ValueType& val);
107
108   /*
109    * Adds the value 'val' the given number of 'times' at time 'now'
110    */
111   bool addValue(TimeType now, const ValueType& val, int64_t times);
112
113   /*
114    * Adds the value 'sum' as the sum of 'nsamples' samples
115    */
116   bool addValueAggregated(TimeType now, const ValueType& sum, int64_t nsamples);
117
118   /*
119    * Updates the container to the specified time, doing all the necessary
120    * work to rotate the buckets and remove any stale data points.
121    *
122    * The addValue() methods automatically call update() when adding new data
123    * points.  However, when reading data from the timeseries, you should make
124    * sure to manually call update() before accessing the data.  Otherwise you
125    * may be reading stale data if update() has not been called recently.
126    *
127    * Returns the current bucket index after the update.
128    */
129   size_t update(TimeType now);
130
131   /*
132    * Reset the timeseries to an empty state,
133    * as if no data points have ever been added to it.
134    */
135   void clear();
136
137   /*
138    * Get the latest time that has ever been passed to update() or addValue().
139    *
140    * If no data has ever been added to this timeseries, 0 will be returned.
141    */
142   TimeType getLatestTime() const {
143     return latestTime_;
144   }
145
146   /*
147    * Get the time of the earliest data point stored in this timeseries.
148    *
149    * If no data has ever been added to this timeseries, 0 will be returned.
150    *
151    * If isAllTime() is true, this is simply the time when the first data point
152    * was recorded.
153    *
154    * For non-all-time data, the timestamp reflects the first data point still
155    * remembered.  As new data points are added, old data will be expired.
156    * getEarliestTime() returns the timestamp of the oldest bucket still present
157    * in the timeseries.  This will never be older than (getLatestTime() -
158    * duration()).
159    */
160   TimeType getEarliestTime() const;
161
162   /*
163    * Return the number of buckets.
164    */
165   size_t numBuckets() const {
166     return buckets_.size();
167   }
168
169   /*
170    * Return the maximum duration of data that can be tracked by this
171    * BucketedTimeSeries.
172    */
173   TimeType duration() const {
174     return duration_;
175   }
176
177   /*
178    * Returns true if this BucketedTimeSeries stores data for all-time, without
179    * ever rolling over into new buckets.
180    */
181   bool isAllTime() const {
182     return (duration_ == TimeType(0));
183   }
184
185   /*
186    * Returns true if no calls to update() have been made since the last call to
187    * clear().
188    */
189   bool empty() const {
190     // We set firstTime_ greater than latestTime_ in the constructor and in
191     // clear, so we use this to distinguish if the timeseries is empty.
192     //
193     // Once a data point has been added, latestTime_ will always be greater
194     // than or equal to firstTime_.
195     return firstTime_ > latestTime_;
196   }
197
198   /*
199    * Get the amount of time tracked by this timeseries.
200    *
201    * For an all-time timeseries, this returns the length of time since the
202    * first data point was added to the time series.
203    *
204    * Otherwise, this never returns a value greater than the overall timeseries
205    * duration.  If the first data point was recorded less than a full duration
206    * ago, the time since the first data point is returned.  If a full duration
207    * has elapsed, and we have already thrown away some data, the time since the
208    * oldest bucket is returned.
209    *
210    * For example, say we are tracking 600 seconds worth of data, in 60 buckets.
211    * - If less than 600 seconds have elapsed since the first data point,
212    *   elapsed() returns the total elapsed time so far.
213    * - If more than 600 seconds have elapsed, we have already thrown away some
214    *   data.  However, we throw away a full bucket (10 seconds worth) at once,
215    *   so at any point in time we have from 590 to 600 seconds worth of data.
216    *   elapsed() will therefore return a value between 590 and 600.
217    *
218    * Note that you generally should call update() before calling elapsed(), to
219    * make sure you are not reading stale data.
220    */
221   TimeType elapsed() const;
222
223   /*
224    * Get the amount of time tracked by this timeseries, between the specified
225    * start and end times.
226    *
227    * If the timeseries contains data for the entire time range specified, this
228    * simply returns (end - start).  However, if start is earlier than
229    * getEarliestTime(), this returns (end - getEarliestTime()).
230    */
231   TimeType elapsed(TimeType start, TimeType end) const;
232
233   /*
234    * Return the sum of all the data points currently tracked by this
235    * BucketedTimeSeries.
236    *
237    * Note that you generally should call update() before calling sum(), to
238    * make sure you are not reading stale data.
239    */
240   const ValueType& sum() const {
241     return total_.sum;
242   }
243
244   /*
245    * Return the number of data points currently tracked by this
246    * BucketedTimeSeries.
247    *
248    * Note that you generally should call update() before calling count(), to
249    * make sure you are not reading stale data.
250    */
251   uint64_t count() const {
252     return total_.count;
253   }
254
255   /*
256    * Return the average value (sum / count).
257    *
258    * The return type may be specified to control whether floating-point or
259    * integer division should be performed.
260    *
261    * Note that you generally should call update() before calling avg(), to
262    * make sure you are not reading stale data.
263    */
264   template <typename ReturnType=double>
265   ReturnType avg() const {
266     return total_.template avg<ReturnType>();
267   }
268
269   /*
270    * Return the sum divided by the elapsed time.
271    *
272    * Note that you generally should call update() before calling rate(), to
273    * make sure you are not reading stale data.
274    */
275   template <typename ReturnType=double, typename Interval=TimeType>
276   ReturnType rate() const {
277     return rateHelper<ReturnType, Interval>(total_.sum, elapsed());
278   }
279
280   /*
281    * Return the count divided by the elapsed time.
282    *
283    * The Interval template parameter causes the elapsed time to be converted to
284    * the Interval type before using it.  For example, if Interval is
285    * std::chrono::seconds, the return value will be the count per second.
286    * If Interval is std::chrono::hours, the return value will be the count per
287    * hour.
288    *
289    * Note that you generally should call update() before calling countRate(),
290    * to make sure you are not reading stale data.
291    */
292   template <typename ReturnType=double, typename Interval=TimeType>
293   ReturnType countRate() const {
294     return rateHelper<ReturnType, Interval>(total_.count, elapsed());
295   }
296
297   /*
298    * Estimate the sum of the data points that occurred in the specified time
299    * period.
300    *
301    * The range queried is [start, end).
302    * That is, start is inclusive, and end is exclusive.
303    *
304    * Note that data outside of the timeseries duration will no longer be
305    * available for use in the estimation.  Specifying a start time earlier than
306    * getEarliestTime() will not have much effect, since only data points after
307    * that point in time will be counted.
308    *
309    * Note that the value returned is an estimate, and may not be precise.
310    */
311   ValueType sum(TimeType start, TimeType end) const;
312
313   /*
314    * Estimate the number of data points that occurred in the specified time
315    * period.
316    *
317    * The same caveats documented in the sum(TimeType start, TimeType end)
318    * comments apply here as well.
319    */
320   uint64_t count(TimeType start, TimeType end) const;
321
322   /*
323    * Estimate the average value during the specified time period.
324    *
325    * The same caveats documented in the sum(TimeType start, TimeType end)
326    * comments apply here as well.
327    */
328   template <typename ReturnType=double>
329   ReturnType avg(TimeType start, TimeType end) const;
330
331   /*
332    * Estimate the rate during the specified time period.
333    *
334    * The same caveats documented in the sum(TimeType start, TimeType end)
335    * comments apply here as well.
336    */
337   template <typename ReturnType=double, typename Interval=TimeType>
338   ReturnType rate(TimeType start, TimeType end) const {
339     ValueType intervalSum = sum(start, end);
340     TimeType interval = elapsed(start, end);
341     return rateHelper<ReturnType, Interval>(intervalSum, interval);
342   }
343
344   /*
345    * Estimate the rate of data points being added during the specified time
346    * period.
347    *
348    * The same caveats documented in the sum(TimeType start, TimeType end)
349    * comments apply here as well.
350    */
351   template <typename ReturnType=double, typename Interval=TimeType>
352   ReturnType countRate(TimeType start, TimeType end) const {
353     uint64_t intervalCount = count(start, end);
354     TimeType interval = elapsed(start, end);
355     return rateHelper<ReturnType, Interval>(intervalCount, interval);
356   }
357
358   /*
359    * Invoke a function for each bucket.
360    *
361    * The function will take as arguments the bucket index,
362    * the bucket start time, and the start time of the subsequent bucket.
363    *
364    * It should return true to continue iterating through the buckets, and false
365    * to break out of the loop and stop, without calling the function on any
366    * more buckets.
367    *
368    * bool function(const Bucket& bucket, TimeType bucketStart,
369    *               TimeType nextBucketStart)
370    */
371   template <typename Function>
372   void forEachBucket(Function fn) const;
373
374   /*
375    * Get the index for the bucket containing the specified time.
376    *
377    * Note that the index is only valid if this time actually falls within one
378    * of the current buckets.  If you pass in a value more recent than
379    * getLatestTime() or older than (getLatestTime() - elapsed()), the index
380    * returned will not be valid.
381    *
382    * This method may not be called for all-time data.
383    */
384   size_t getBucketIdx(TimeType time) const;
385
386   /*
387    * Get the bucket at the specified index.
388    *
389    * This method may not be called for all-time data.
390    */
391   const Bucket& getBucketByIndex(size_t idx) const {
392     return buckets_[idx];
393   }
394
395   /*
396    * Compute the bucket index that the specified time falls into,
397    * as well as the bucket start time and the next bucket's start time.
398    *
399    * This method may not be called for all-time data.
400    */
401   void getBucketInfo(TimeType time, size_t* bucketIdx,
402                      TimeType* bucketStart, TimeType* nextBucketStart) const;
403
404  private:
405   template <typename ReturnType=double, typename Interval=TimeType>
406   ReturnType rateHelper(ReturnType numerator, TimeType elapsedTime) const {
407     return detail::rateHelper<ReturnType, TimeType, Interval>(numerator,
408                                                               elapsedTime);
409   }
410
411   TimeType getEarliestTimeNonEmpty() const;
412   size_t updateBuckets(TimeType now);
413
414   ValueType rangeAdjust(TimeType bucketStart, TimeType nextBucketStart,
415                         TimeType start, TimeType end,
416                         ValueType input) const;
417
418   template <typename Function>
419   void forEachBucket(TimeType start, TimeType end, Function fn) const;
420
421   TimeType firstTime_;   // time of first update() since clear()/constructor
422   TimeType latestTime_;  // time of last update()
423   TimeType duration_;    // total duration ("window length") of the time series
424
425   Bucket total_;                 // sum and count of everything in time series
426   std::vector<Bucket> buckets_;  // actual buckets of values
427 };
428
429 } // folly