View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.metrics.impl;
19  
20  import java.util.concurrent.atomic.AtomicBoolean;
21  import java.util.concurrent.atomic.AtomicLong;
22  
23  import org.apache.hadoop.hbase.classification.InterfaceAudience;
24  import org.apache.hadoop.hbase.classification.InterfaceStability;
25  import org.apache.hadoop.hbase.metrics.Snapshot;
26  import org.apache.hadoop.hbase.util.AtomicUtils;
27  import org.apache.hadoop.hbase.util.LongAdder;
28  
29  /**
30   * FastLongHistogram is a thread-safe class that estimate distribution of data and computes the
31   * quantiles.
32   */
33  @InterfaceAudience.Private
34  @InterfaceStability.Evolving
35  public class FastLongHistogram {
36  
37    /**
38     * Default number of bins.
39     */
40    public static final int DEFAULT_NBINS = 255;
41  
42    public static final double[] DEFAULT_QUANTILES =
43        new double[]{0.25, 0.5, 0.75, 0.90, 0.95, 0.98, 0.99, 0.999};
44  
45    /**
46     * Bins is a class containing a list of buckets(or bins) for estimation histogram of some data.
47     */
48    private static class Bins {
49      private final LongAdder[] counts;
50      // inclusive
51      private final long binsMin;
52      // exclusive
53      private final long binsMax;
54      private final long bins10XMax;
55      private final AtomicLong min = new AtomicLong(Long.MAX_VALUE);
56      private final AtomicLong max = new AtomicLong(0L);
57  
58      private final LongAdder count = new LongAdder();
59      private final LongAdder total = new LongAdder();
60  
61      // set to true when any of data has been inserted to the Bins. It is set after the counts are
62      // updated.
63      private final AtomicBoolean hasData = new AtomicBoolean(false);
64  
65      /**
66       * The constructor for creating a Bins without any prior data.
67       */
68      public Bins(int numBins) {
69        counts = createCounters(numBins + 3);
70        this.binsMin = 1L;
71  
72        // These two numbers are total guesses
73        // and should be treated as highly suspect.
74        this.binsMax = 1000;
75        this.bins10XMax = binsMax * 10;
76      }
77  
78      /**
79       * The constructor for creating a Bins with last Bins.
80       */
81      public Bins(Bins last, int numOfBins, double minQ, double maxQ) {
82        long[] values = last.getQuantiles(new double[] { minQ, maxQ });
83        long wd = values[1] - values[0] + 1;
84        // expand minQ and maxQ in two ends back assuming uniform distribution
85        this.binsMin = Math.max(0L, (long) (values[0] - wd * minQ));
86        long binsMax = (long) (values[1] + wd * (1 - maxQ)) + 1;
87        // make sure each of bins is at least of width 1
88        this.binsMax = Math.max(binsMax, this.binsMin + numOfBins);
89        this.bins10XMax = Math.max((long) (values[1] + (binsMax - 1) * 9), this.binsMax + 1);
90  
91        this.counts = createCounters(numOfBins + 3);
92      }
93  
94      private LongAdder[] createCounters(int num) {
95        LongAdder[] counters = new LongAdder[num];
96        for (int i = 0; i < num; i++) {
97          counters[i] = new LongAdder();
98        }
99        return counters;
100     }
101 
102     private int getIndex(long value) {
103       if (value < this.binsMin) {
104         return 0;
105       } else if (value > this.bins10XMax) {
106         return this.counts.length - 1;
107       } else if (value >= this.binsMax) {
108         return this.counts.length - 2;
109       }
110       // compute the position
111       return 1 + (int) ((value - this.binsMin) * (this.counts.length - 3) /
112           (this.binsMax - this.binsMin));
113 
114     }
115 
116     /**
117      * Adds a value to the histogram.
118      */
119     public void add(long value, long count) {
120       if (value < 0) {
121         // The whole computation is completely thrown off if there are negative numbers
122         //
123         // Normally we would throw an IllegalArgumentException however this is the metrics
124         // system and it should be completely safe at all times.
125         // So silently throw it away.
126         return;
127       }
128       AtomicUtils.updateMin(min, value);
129       AtomicUtils.updateMax(max, value);
130 
131       this.count.add(count);
132       this.total.add(value * count);
133 
134       int pos = getIndex(value);
135       this.counts[pos].add(count);
136 
137       // hasData needs to be updated as last
138       this.hasData.set(true);
139     }
140 
141     /**
142      * Computes the quantiles give the ratios.
143      */
144     public long[] getQuantiles(double[] quantiles) {
145       if (!this.hasData.get()) {
146         // No data yet.
147         return new long[quantiles.length];
148       }
149 
150       // Make a snapshot of lowerCounter, higherCounter and bins.counts to counts.
151       // This is not synchronized, but since the counter are accumulating, the result is a good
152       // estimation of a snapshot.
153       long[] counts = new long[this.counts.length];
154       long total = 0L;
155       for (int i = 0; i < this.counts.length; i++) {
156         counts[i] = this.counts[i].sum();
157         total += counts[i];
158       }
159 
160       int rIndex = 0;
161       double qCount = total * quantiles[0];
162       long cum = 0L;
163 
164       long[] res = new long[quantiles.length];
165       countsLoop: for (int i = 0; i < counts.length; i++) {
166         // mn and mx define a value range
167         long mn, mx;
168         if (i == 0) {
169           mn = this.min.get();
170           mx = this.binsMin;
171         } else if (i == counts.length - 1) {
172           mn = this.bins10XMax;
173           mx = this.max.get();
174         } else if (i == counts.length - 2) {
175           mn = this.binsMax;
176           mx = this.bins10XMax;
177         } else {
178           mn = this.binsMin + (i - 1) * (this.binsMax - this.binsMin) / (this.counts.length - 3);
179           mx = this.binsMin + i * (this.binsMax - this.binsMin) / (this.counts.length - 3);
180         }
181 
182         if (mx < this.min.get()) {
183           continue;
184         }
185         if (mn > this.max.get()) {
186           break;
187         }
188         mn = Math.max(mn, this.min.get());
189         mx = Math.min(mx, this.max.get());
190 
191         // lastCum/cum are the corresponding counts to mn/mx
192         double lastCum = cum;
193         cum += counts[i];
194 
195         // fill the results for qCount is within current range.
196         while (qCount <= cum) {
197           if (cum == lastCum) {
198             res[rIndex] = mn;
199           } else {
200             res[rIndex] = (long) ((qCount - lastCum) * (mx - mn) / (cum - lastCum) + mn);
201           }
202 
203           // move to next quantile
204           rIndex++;
205           if (rIndex >= quantiles.length) {
206             break countsLoop;
207           }
208           qCount = total * quantiles[rIndex];
209         }
210       }
211       // In case quantiles contains values >= 100%
212       for (; rIndex < quantiles.length; rIndex++) {
213         res[rIndex] = this.max.get();
214       }
215 
216       return res;
217     }
218 
219 
220     long getNumAtOrBelow(long val) {
221       final int targetIndex = getIndex(val);
222       long totalToCurrentIndex = 0;
223       for (int i = 0; i <= targetIndex; i++) {
224         totalToCurrentIndex += this.counts[i].sum();
225       }
226       return  totalToCurrentIndex;
227     }
228 
229     public long getMin() {
230       long min = this.min.get();
231       return min == Long.MAX_VALUE ? 0 : min; // in case it is not initialized
232     }
233 
234     public long getMean() {
235       long count = this.count.sum();
236       long total = this.total.sum();
237       if (count == 0) {
238         return 0;
239       }
240       return total / count;
241     }
242   }
243 
244   // The bins counting values. It is replaced with a new one in calling of reset().
245   private volatile Bins bins;
246 
247   /**
248    * Constructor.
249    */
250   public FastLongHistogram() {
251     this(DEFAULT_NBINS);
252   }
253 
254   /**
255    * Constructor.
256    * @param numOfBins the number of bins for the histogram. A larger value results in more precise
257    *          results but with lower efficiency, and vice versus.
258    */
259   public FastLongHistogram(int numOfBins) {
260     this.bins = new Bins(numOfBins);
261   }
262 
263   /**
264    * Constructor setting the bins assuming a uniform distribution within a range.
265    * @param numOfBins the number of bins for the histogram. A larger value results in more precise
266    *          results but with lower efficiency, and vice versus.
267    * @param min lower bound of the region, inclusive.
268    * @param max higher bound of the region, inclusive.
269    */
270   public FastLongHistogram(int numOfBins, long min, long max) {
271     this(numOfBins);
272     Bins bins = new Bins(numOfBins);
273     bins.add(min, 1);
274     bins.add(max, 1);
275     this.bins = new Bins(bins, numOfBins, 0.01, 0.999);
276   }
277 
278   private FastLongHistogram(Bins bins) {
279     this.bins = bins;
280   }
281 
282   /**
283    * Adds a value to the histogram.
284    */
285   public void add(long value, long count) {
286     this.bins.add(value, count);
287   }
288 
289   /**
290    * Computes the quantiles give the ratios.
291    */
292   public long[] getQuantiles(double[] quantiles) {
293     return this.bins.getQuantiles(quantiles);
294   }
295 
296   public long[] getQuantiles() {
297     return this.bins.getQuantiles(DEFAULT_QUANTILES);
298   }
299 
300   public long getMin() {
301     return this.bins.getMin();
302   }
303 
304   public long getMax() {
305     return this.bins.max.get();
306   }
307 
308   public long getCount() {
309     return this.bins.count.sum();
310   }
311 
312   public long getMean() {
313     return this.bins.getMean();
314   }
315 
316   public long getNumAtOrBelow(long value) {
317     return this.bins.getNumAtOrBelow(value);
318   }
319 
320   /**
321    * Resets the histogram for new counting.
322    */
323   public Snapshot snapshotAndReset() {
324     final Bins oldBins = this.bins;
325     this.bins = new Bins(this.bins, this.bins.counts.length - 3, 0.01, 0.99);
326     final long[] percentiles = oldBins.getQuantiles(DEFAULT_QUANTILES);
327     final long count = oldBins.count.sum();
328 
329     return new Snapshot() {
330       @Override
331       public long[] getQuantiles(double[] quantiles) {
332         return oldBins.getQuantiles(quantiles);
333       }
334 
335       @Override
336       public long[] getQuantiles() {
337         return percentiles;
338       }
339 
340       @Override
341       public long getCount() {
342         return count;
343       }
344 
345       @Override
346       public long getCountAtOrBelow(long val) {
347         return oldBins.getNumAtOrBelow(val);
348       }
349 
350       @Override
351       public long get25thPercentile() {
352         return percentiles[0];
353       }
354 
355       @Override
356       public long get75thPercentile() {
357         return percentiles[2];
358       }
359 
360       @Override
361       public long get90thPercentile() {
362         return percentiles[3];
363       }
364 
365       @Override
366       public long get95thPercentile() {
367         return percentiles[4];
368       }
369 
370       @Override
371       public long get98thPercentile() {
372         return percentiles[5];
373       }
374 
375       @Override
376       public long get99thPercentile() {
377         return percentiles[6];
378       }
379 
380       @Override
381       public long get999thPercentile() {
382         return percentiles[7];
383       }
384 
385       @Override
386       public long getMedian() {
387         return percentiles[1];
388       }
389 
390       @Override
391       public long getMax() {
392         return oldBins.max.get();
393       }
394 
395       @Override
396       public long getMean() {
397         return oldBins.getMean();
398       }
399 
400       @Override
401       public long getMin() {
402         return oldBins.getMin();
403       }
404     };
405   }
406 }