• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!DOCTYPE html>
2<!--
3Copyright 2016 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/base/iteration_helpers.html">
9<link rel="import" href="/tracing/base/range.html">
10<link rel="import" href="/tracing/base/running_statistics.html">
11<link rel="import" href="/tracing/base/sorted_array_utils.html">
12<link rel="import" href="/tracing/base/statistics.html">
13<link rel="import" href="/tracing/base/unit.html">
14<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
15<link rel="import" href="/tracing/value/numeric.html">
16
17<script>
18'use strict';
19
20tr.exportTo('tr.v', function() {
21  var MAX_DIAGNOSTIC_MAPS = 16;
22
23  // p-values less than this indicate statistical significance.
24  var DEFAULT_ALPHA = 0.05;
25
26  /** @enum */
27  var Significance = {
28    DONT_CARE: -1,
29    INSIGNIFICANT: 0,
30    SIGNIFICANT: 1
31  };
32
33  var DEFAULT_BOUNDARIES_FOR_UNIT = new Map();
34
35  class HistogramBin {
36    /**
37     * @param {!tr.b.Range} range
38     */
39    constructor(range) {
40      this.range = range;
41      this.count = 0;
42      this.diagnosticMaps = [];
43    }
44
45    /**
46     * @param {*} value
47     */
48    addSample(value) {
49      this.count += 1;
50    }
51
52    /**
53     * @param {!tr.v.d.DiagnosticMap} diagnostics
54     */
55    addDiagnosticMap(diagnostics) {
56      tr.b.Statistics.uniformlySampleStream(
57          this.diagnosticMaps, this.count, diagnostics, MAX_DIAGNOSTIC_MAPS);
58    }
59
60    addBin(other) {
61      if (!this.range.equals(other.range))
62        throw new Error('Merging incompatible Histogram bins.');
63      tr.b.Statistics.mergeSampledStreams(this.diagnosticMaps, this.count,
64          other.diagnosticMaps, other.count, MAX_DIAGNOSTIC_MAPS);
65      this.count += other.count;
66    }
67
68    fromDict(d) {
69      this.count = d.count;
70      for (var map of d.diagnosticMaps)
71        this.diagnosticMaps.push(tr.v.d.DiagnosticMap.fromDict(map));
72    }
73
74    asDict() {
75      return {
76        count: this.count,
77        diagnosticMaps: this.diagnosticMaps.map(d => d.asDict())
78      };
79    }
80  }
81
82  /**
83   * This is basically a histogram, but so much more.
84   * Histogram is serializable using asDict/fromDict.
85   * Histogram computes several statistics of its contents.
86   * Histograms can be merged.
87   * getDifferenceSignificance() test whether one Histogram is statistically
88   * significantly different from another Histogram.
89   * Histogram stores a random sample of the exact number values added to it.
90   * Histogram stores a random sample of optional per-sample DiagnosticMaps.
91   * Histogram is visualized by <tr-v-ui-histogram-span>, which supports
92   * selecting bins, and visualizing the DiagnosticMaps of selected bins.
93   *
94   * @param {!tr.b.Unit} unit
95   * @param {!tr.v.HistogramBinBoundaries=} opt_binBoundaries
96   */
97  class Histogram {
98    constructor(name, unit, opt_binBoundaries) {
99
100      var binBoundaries = opt_binBoundaries;
101      if (!binBoundaries) {
102        var baseUnit = unit.baseUnit ? unit.baseUnit : unit;
103        binBoundaries = DEFAULT_BOUNDARIES_FOR_UNIT.get(baseUnit.unitName);
104      }
105
106      // If this Histogram is being deserialized, then its guid will be set by
107      // fromDict().
108      // If this Histogram is being computed by a metric, then its guid will be
109      // allocated the first time the guid is gotten by asDict().
110      this.guid_ = undefined;
111
112      this.allBins = [];
113      this.centralBins = [];
114      this.description = '';
115      this.diagnostics = new tr.v.d.DiagnosticMap();
116      this.maxCount_ = 0;
117      this.name_ = name;
118      this.nanDiagnosticMaps = [];
119      this.numNans = 0;
120      this.running = new tr.b.RunningStatistics();
121      this.sampleValues_ = [];
122      this.shortName = undefined;
123      this.summaryOptions = {
124        count: true,
125        sum: true,
126        avg: true,
127        geometricMean: false,
128        std: true,
129        min: true,
130        max: true,
131        nans: false,
132        percentile: []
133      };
134      this.unit = unit;
135
136      this.underflowBin = new HistogramBin(tr.b.Range.fromExplicitRange(
137          -Number.MAX_VALUE, binBoundaries.minBinBoundary));
138      this.overflowBin = new HistogramBin(tr.b.Range.fromExplicitRange(
139          binBoundaries.maxBinBoundary, Number.MAX_VALUE));
140
141      for (var range of binBoundaries)
142        this.centralBins.push(new HistogramBin(range));
143
144      this.allBins.push(this.underflowBin);
145      for (var bin of this.centralBins)
146        this.allBins.push(bin);
147      this.allBins.push(this.overflowBin);
148
149      this.maxNumSampleValues = this.allBins.length * 10;
150    }
151
152    get name() {
153      return this.name_;
154    }
155
156    get guid() {
157      if (this.guid_ === undefined)
158        this.guid_ = tr.b.GUID.allocateUUID4();
159
160      return this.guid_;
161    }
162
163    set guid(guid) {
164      if (this.guid_ !== undefined)
165        throw new Error('Cannot reset guid');
166
167      this.guid_ = guid;
168    }
169
170    static fromDict(d) {
171      var boundaries = HistogramBinBoundaries.createWithBoundaries(
172          d.binBoundaries);
173      var n = new Histogram(d.name, tr.b.Unit.fromJSON(d.unit), boundaries);
174      n.guid = d.guid;
175      n.shortName = d.shortName;
176      n.description = d.description;
177      n.diagnostics.addDicts(d.diagnostics);
178
179      n.underflowBin.fromDict(d.underflowBin);
180      for (var i = 0; i < d.centralBins.length; ++i)
181        n.centralBins[i].fromDict(d.centralBins[i]);
182      n.overflowBin.fromDict(d.overflowBin);
183
184      for (var bin of n.allBins)
185        n.maxCount_ = Math.max(n.maxCount_, bin.count);
186
187      if (d.running)
188        n.running = tr.b.RunningStatistics.fromDict(d.running);
189      if (d.summaryOptions)
190        n.customizeSummaryOptions(d.summaryOptions);
191
192      n.maxNumSampleValues = d.maxNumSampleValues;
193      n.sampleValues_ = d.sampleValues;
194
195      n.numNans = d.numNans;
196      for (var map of d.nanDiagnosticMaps)
197        n.nanDiagnosticMaps.push(tr.v.d.DiagnosticMap.fromDict(map));
198
199      return n;
200    }
201
202    /**
203     * Build a Histogram from a set of samples in order to effectively merge a
204     * set of ScalarNumerics.
205     * The range of the resulting histogram is determined by the smallest and
206     * largest sample value, which is unpredictable.
207     * https://github.com/catapult-project/catapult/issues/2685
208     *
209     * @param {!tr.b.Unit} unit
210     * @param {!Array.<number>} samples
211     * @return {!Histogram}
212     */
213    static buildFromSamples(unit, samples) {
214      var boundaries = HistogramBinBoundaries.createFromSamples(samples);
215      var result = new Histogram(unit, boundaries);
216      result.maxNumSampleValues = 1000;
217
218      // TODO(eakuefner): Propagate diagnosticMaps?
219      for (var sample of samples)
220        result.addSample(sample);
221
222      return result;
223    }
224
225    get numValues() {
226      return tr.b.Statistics.sum(this.allBins, function(e) {
227        return e.count;
228      });
229    }
230
231    get average() {
232      return this.running.mean;
233    }
234
235    get geometricMean() {
236      return this.running.geometricMean;
237    }
238
239    get sum() {
240      return this.running.sum;
241    }
242
243    get maxCount() {
244      return this.maxCount_;
245    }
246
247    /**
248     * Requires that units agree.
249     * Returns DONT_CARE if that is the units' improvementDirection.
250     * Returns SIGNIFICANT if the Mann-Whitney U test returns a
251     * p-value less than opt_alpha or DEFAULT_ALPHA. Returns INSIGNIFICANT if
252     * the p-value is greater than alpha.
253     *
254     * @param {!tr.v.Histogram} other
255     * @param {number=} opt_alpha
256     * @return {!tr.v.Significance}
257     */
258    getDifferenceSignificance(other, opt_alpha) {
259      if (this.unit !== other.unit)
260        throw new Error('Cannot compare Numerics with different units');
261
262      if (this.unit.improvementDirection ===
263          tr.b.ImprovementDirection.DONT_CARE) {
264        return tr.v.Significance.DONT_CARE;
265      }
266
267      if (!(other instanceof Histogram))
268        throw new Error('Unable to compute a p-value');
269
270      var mwu = tr.b.Statistics.mwu.test(this.sampleValues, other.sampleValues);
271      if (mwu.p < (opt_alpha || DEFAULT_ALPHA))
272        return tr.v.Significance.SIGNIFICANT;
273      return tr.v.Significance.INSIGNIFICANT;
274    }
275
276    /*
277     * Compute an approximation of percentile based on the counts in the bins.
278     * If the real percentile lies within |this.range| then the result of
279     * the function will deviate from the real percentile by at most
280     * the maximum width of the bin(s) within which the point(s)
281     * from which the real percentile would be calculated lie.
282     * If the real percentile is outside |this.range| then the function
283     * returns the closest range limit: |this.range.min| or |this.range.max|.
284     *
285     * @param {number} percent The percent must be between 0.0 and 1.0.
286     */
287    getApproximatePercentile(percent) {
288      if (!(percent >= 0 && percent <= 1))
289        throw new Error('percent must be [0,1]');
290      if (this.numValues == 0)
291        return 0;
292      var valuesToSkip = Math.floor((this.numValues - 1) * percent);
293      for (var i = 0; i < this.allBins.length; i++) {
294        var bin = this.allBins[i];
295        valuesToSkip -= bin.count;
296        if (valuesToSkip < 0) {
297          if (bin === this.underflowBin)
298            return bin.range.max;
299          else if (bin === this.overflowBin)
300            return bin.range.min;
301          else
302            return bin.range.center;
303        }
304      }
305      throw new Error('Unreachable');
306    }
307
308    getBinForValue(value) {
309      // Don't use subtraction to avoid arithmetic overflow.
310      var binIndex = tr.b.findHighIndexInSortedArray(
311          this.allBins, b => value < b.range.max ? -1 : 1);
312      return this.allBins[binIndex] || this.overflowBin;
313    }
314
315    /**
316     * @param {number|*} value
317     * @param {(!Object|!tr.v.d.DiagnosticMap)=} opt_diagnostics
318     */
319    addSample(value, opt_diagnostics) {
320      if (opt_diagnostics &&
321          !(opt_diagnostics instanceof tr.v.d.DiagnosticMap))
322        opt_diagnostics = tr.v.d.DiagnosticMap.fromObject(opt_diagnostics);
323
324      if (typeof(value) !== 'number' || isNaN(value)) {
325        this.numNans++;
326        if (opt_diagnostics) {
327          tr.b.Statistics.uniformlySampleStream(this.nanDiagnosticMaps,
328              this.numNans, opt_diagnostics, MAX_DIAGNOSTIC_MAPS);
329        }
330      } else {
331        this.running.add(value);
332
333        var bin = this.getBinForValue(value);
334        bin.addSample(value);
335        if (opt_diagnostics)
336          bin.addDiagnosticMap(opt_diagnostics);
337        if (bin.count > this.maxCount_)
338          this.maxCount_ = bin.count;
339      }
340
341      tr.b.Statistics.uniformlySampleStream(this.sampleValues_,
342          this.numValues + this.numNans, value, this.maxNumSampleValues);
343    }
344
345    sampleValuesInto(samples) {
346      for (var sampleValue of this.sampleValues)
347        samples.push(sampleValue);
348    }
349
350    /**
351     * Return true if this Histogram can be added to |other|.
352     *
353     * @param {!tr.v.Histogram} other
354     * @return {boolean}
355     */
356    canAddHistogram(other) {
357      if (this.unit !== other.unit)
358        return false;
359      if (this.allBins.length !== other.allBins.length)
360        return false;
361
362      for (var i = 0; i < this.allBins.length; ++i)
363        if (!this.allBins[i].range.equals(other.allBins[i].range))
364          return false;
365
366      return true;
367    }
368
369    /**
370     * Add |other| to this Histogram in-place if they can be added.
371     *
372     * @param {!tr.v.Histogram} other
373     */
374    addHistogram(other) {
375      if (!this.canAddHistogram(other))
376        throw new Error('Merging incompatible Numerics.');
377
378      tr.b.Statistics.mergeSampledStreams(this.nanDiagnosticMaps, this.numNans,
379          other.nanDiagnosticMaps, other.numNans, MAX_DIAGNOSTIC_MAPS);
380      tr.b.Statistics.mergeSampledStreams(
381          this.sampleValues, this.numValues,
382          other.sampleValues, other.numValues, tr.b.Statistics.mean(
383              [this.maxNumSampleValues, other.maxNumSampleValues]));
384      this.numNans += other.numNans;
385      this.running = this.running.merge(other.running);
386      for (var i = 0; i < this.allBins.length; ++i) {
387        this.allBins[i].addBin(other.allBins[i]);
388      }
389    }
390
391    /**
392     * Controls which statistics are exported to dashboard for this numeric.
393     * The |summaryOptions| parameter is a dictionary with optional boolean
394     * fields |count|, |sum|, |avg|, |std|, |min|, |max| and an optional
395     * array field |percentile|.
396     * Each percentile should be a number between 0.0 and 1.0.
397     * The options not included in the |summaryOptions| will not change.
398     */
399    customizeSummaryOptions(summaryOptions) {
400      tr.b.iterItems(summaryOptions, function(key, value) {
401        this.summaryOptions[key] = value;
402      }, this);
403    }
404
405    /**
406     * Returns a Map {statisticName: ScalarNumeric}.
407     *
408     * Each enabled summary option produces the corresponding value:
409     * min, max, count, sum, avg, or std.
410     * Each percentile 0.x produces pct_0x0.
411     * Each percentile 0.xx produces pct_0xx.
412     * Each percentile 0.xxy produces pct_0xx_y.
413     * Percentile 1.0 produces pct_100.
414     *
415     * @return {!Map.<string, ScalarNumeric>}
416     */
417    get statisticsScalars() {
418      function statNameToKey(stat) {
419        switch (stat) {
420          case 'std':
421            return 'stddev';
422          case 'avg':
423            return 'mean';
424        }
425        return stat;
426      }
427      /**
428       * Converts the given percent to a string in the format specified above.
429       * @param {number} percent The percent must be between 0.0 and 1.0.
430       */
431      function percentToString(percent) {
432        if (percent < 0 || percent > 1)
433          throw new Error('Percent must be between 0.0 and 1.0');
434        switch (percent) {
435          case 0:
436            return '000';
437          case 1:
438            return '100';
439        }
440        var str = percent.toString();
441        if (str[1] !== '.')
442          throw new Error('Unexpected percent');
443        // Pad short strings with zeros.
444        str = str + '0'.repeat(Math.max(4 - str.length, 0));
445        if (str.length > 4)
446          str = str.slice(0, 4) + '_' + str.slice(4);
447        return '0' + str.slice(2);
448      }
449
450      var results = new Map();
451      tr.b.iterItems(this.summaryOptions, function(stat, option) {
452        if (!option)
453          return;
454
455        if (stat === 'percentile') {
456          option.forEach(function(percent) {
457            var percentile = this.getApproximatePercentile(percent);
458            results.set('pct_' + percentToString(percent),
459                new tr.v.ScalarNumeric(this.unit, percentile));
460          }, this);
461        } else if (stat === 'nans') {
462          results.set('nans', new tr.v.ScalarNumeric(
463              tr.b.Unit.byName.count_smallerIsBetter, this.numNans));
464        } else {
465          var statUnit = stat === 'count' ?
466              tr.b.Unit.byName.count_smallerIsBetter : this.unit;
467          var key = statNameToKey(stat);
468          var statValue = this.running[key];
469
470          if (typeof(statValue) === 'number') {
471            results.set(stat, new tr.v.ScalarNumeric(statUnit, statValue));
472          }
473        }
474      }, this);
475      return results;
476    }
477
478    get sampleValues() {
479      return this.sampleValues_;
480    }
481
482    get binBoundaries() {
483      var boundaries = [];
484      for (var bin of this.centralBins)
485        boundaries.push(bin.range.min);
486      boundaries.push(this.overflowBin.range.min);
487      return boundaries;
488    }
489
490    clone() {
491      return Histogram.fromDict(this.asDict());
492    }
493
494    asDict() {
495      return {
496        name: this.name,
497        guid: this.guid,
498        shortName: this.shortName,
499        description: this.description,
500        diagnostics: this.diagnostics.asDict(),
501        unit: this.unit.asJSON(),
502        binBoundaries: this.binBoundaries,
503
504        underflowBin: this.underflowBin.asDict(),
505        centralBins: this.centralBins.map(bin => bin.asDict()),
506        overflowBin: this.overflowBin.asDict(),
507
508        running: this.running.asDict(),
509        summaryOptions: this.summaryOptions,
510
511        maxNumSampleValues: this.maxNumSampleValues,
512        sampleValues: this.sampleValues,
513
514        numNans: this.numNans,
515        nanDiagnosticMaps: this.nanDiagnosticMaps.map(dm => dm.asDict()),
516      };
517    }
518  }
519
520  /**
521   * Reusable builder for tr.v.Histogram objects.
522   *
523   * The bins of the numeric are specified by adding the desired boundaries
524   * between bins. Initially, the builder has only a single boundary:
525   *
526   *       minBinBoundary=maxBinBoundary
527   *                     |
528   *                     |
529   *   -MAX_INT <--------|------------------------------------------> +MAX_INT
530   *       :  resulting  :                   resulting                    :
531   *       :  underflow  :                    overflow                    :
532   *       :     bin     :                      bin                       :
533   *
534   * More boundaries can be added (in increasing order) using addBinBoundary,
535   * addLinearBins and addExponentialBins:
536   *
537   *              minBinBoundary                      maxBinBoundary
538   *                     |         |         |     |         |
539   *                     |         |         |     |         |
540   *   -MAX_INT <--------|---------|---------|-----|---------|------> +MAX_INT
541   *       :  resulting  : result. : result. :     : result. : resulting  :
542   *       :  underflow  : central : central : ... : central :  overflow  :
543   *       :     bin     :  bin 0  :  bin 1  :     : bin N-1 :    bin     :
544   *
545   * An important feature of the builder is that it's reusable, i.e. it can be
546   * used to build multiple numerics with the same unit and bin structure.
547   *
548   * @constructor
549   * @param {!tr.b.Unit} unit Unit of the resulting Histogram(s).
550   * @param {number} minBinBoundary The minimum boundary between bins, namely
551   *     the underflow bin and the first central bin (or the overflow bin if
552   *     no other boundaries are added later).
553   */
554  class HistogramBinBoundaries {
555    /**
556     * Create a linearly scaled tr.v.HistogramBinBoundaries with |numBins| bins
557     * ranging from |min| to |max|.
558     *
559     * @param {number} min
560     * @param {number} max
561     * @param {number} numBins
562     * @return {tr.v.HistogramBinBoundaries}
563     */
564    static createLinear(min, max, numBins) {
565      return new HistogramBinBoundaries(min).addLinearBins(max, numBins);
566    }
567
568    /**
569     * Create an exponentially scaled tr.v.HistogramBinBoundaries with |numBins|
570     * bins ranging from |min| to |max|.
571     *
572     * @param {number} min
573     * @param {number} max
574     * @param {number} numBins
575     * @return {tr.v.HistogramBinBoundaries}
576     */
577    static createExponential(min, max, numBins) {
578      return new HistogramBinBoundaries(min).addExponentialBins(max, numBins);
579    }
580
581    /**
582     * @param {Array.<number>} binBoundaries
583     */
584    static createWithBoundaries(binBoundaries) {
585      var builder = new HistogramBinBoundaries(binBoundaries[0]);
586      for (var boundary of binBoundaries.slice(1))
587        builder.addBinBoundary(boundary);
588      return builder;
589    }
590
591    static createFromSamples(samples) {
592      var range = new tr.b.Range();
593      // Prevent non-numeric samples from introducing NaNs into the range.
594      for (var sample of samples)
595        if (!isNaN(Math.max(sample)))
596          range.addValue(sample);
597
598      // HistogramBinBoundaries.addLinearBins() requires this.
599      if (range.isEmpty)
600        range.addValue(1);
601      if (range.min === range.max)
602        range.addValue(range.min - 1);
603
604      // This optimizes the resolution when samples are uniformly distributed
605      // (which is almost never the case).
606      var numBins = Math.ceil(Math.sqrt(samples.length));
607      var builder = new HistogramBinBoundaries(range.min);
608      builder.addLinearBins(range.max, numBins);
609      return builder;
610    }
611
612    /**
613     * @param {number} minBinBoundary
614     */
615    constructor(minBinBoundary) {
616      this.boundaries_ = [minBinBoundary];
617    }
618
619    get minBinBoundary() {
620      return this.boundaries_[0];
621    }
622
623    get maxBinBoundary() {
624      return this.boundaries_[this.boundaries_.length - 1];
625    }
626
627    /**
628     * Yield Ranges of adjacent boundaries.
629     */
630    *[Symbol.iterator]() {
631      for (var i = 0; i < this.boundaries_.length - 1; ++i) {
632        yield tr.b.Range.fromExplicitRange(
633            this.boundaries_[i], this.boundaries_[i + 1]);
634      }
635    }
636
637    /**
638     * Add a bin boundary |nextMaxBinBoundary| to the builder.
639     *
640     * This operation effectively corresponds to appending a new central bin
641     * with the range [this.maxBinBoundary*, nextMaxBinBoundary].
642     *
643     * @param {number} nextMaxBinBoundary The added bin boundary (must be
644     *     greater than |this.maxMinBoundary|).
645     */
646    addBinBoundary(nextMaxBinBoundary) {
647      if (nextMaxBinBoundary <= this.maxBinBoundary) {
648        throw new Error('The added max bin boundary must be larger than ' +
649            'the current max boundary');
650      }
651      this.boundaries_.push(nextMaxBinBoundary);
652
653      return this;
654    }
655
656    /**
657     * Add |binCount| linearly scaled bin boundaries up to |nextMaxBinBoundary|
658     * to the builder.
659     *
660     * This operation corresponds to appending |binCount| central bins of
661     * constant range width
662     * W = ((|nextMaxBinBoundary| - |this.maxBinBoundary|) / |binCount|)
663     * with the following ranges:
664     *
665     *   [|this.maxMinBoundary|, |this.maxMinBoundary| + W]
666     *   [|this.maxMinBoundary| + W, |this.maxMinBoundary| + 2W]
667     *   [|this.maxMinBoundary| + 2W, |this.maxMinBoundary| + 3W]
668     *   ...
669     *   [|this.maxMinBoundary| + (|binCount| - 2) * W,
670     *    |this.maxMinBoundary| + (|binCount| - 2) * W]
671     *   [|this.maxMinBoundary| + (|binCount| - 1) * W,
672     *    |nextMaxBinBoundary|]
673     *
674     * @param {number} nextBinBoundary The last added bin boundary (must be
675     *     greater than |this.maxMinBoundary|).
676     * @param {number} binCount Number of bins to be added (must be positive).
677     */
678    addLinearBins(nextMaxBinBoundary, binCount) {
679      if (binCount <= 0)
680        throw new Error('Bin count must be positive');
681
682      var curMaxBinBoundary = this.maxBinBoundary;
683      if (curMaxBinBoundary >= nextMaxBinBoundary) {
684        throw new Error('The new max bin boundary must be greater than ' +
685            'the previous max bin boundary');
686      }
687
688      var binWidth = (nextMaxBinBoundary - curMaxBinBoundary) / binCount;
689      for (var i = 1; i < binCount; i++)
690        this.addBinBoundary(curMaxBinBoundary + i * binWidth);
691      this.addBinBoundary(nextMaxBinBoundary);
692
693      return this;
694    }
695
696    /**
697     * Add |binCount| exponentially scaled bin boundaries up to
698     * |nextMaxBinBoundary| to the builder.
699     *
700     * This operation corresponds to appending |binCount| central bins with
701     * a constant difference between the logarithms of their range min and max
702     * D = ((ln(|nextMaxBinBoundary|) - ln(|this.maxBinBoundary|)) / |binCount|)
703     * with the following ranges:
704     *
705     *   [|this.maxMinBoundary|, |this.maxMinBoundary| * exp(D)]
706     *   [|this.maxMinBoundary| * exp(D), |this.maxMinBoundary| * exp(2D)]
707     *   [|this.maxMinBoundary| * exp(2D), |this.maxMinBoundary| * exp(3D)]
708     *   ...
709     *   [|this.maxMinBoundary| * exp((|binCount| - 2) * D),
710     *    |this.maxMinBoundary| * exp((|binCount| - 2) * D)]
711     *   [|this.maxMinBoundary| * exp((|binCount| - 1) * D),
712     *    |nextMaxBinBoundary|]
713     *
714     * This method requires that the current max bin boundary is positive.
715     *
716     * @param {number} nextBinBoundary The last added bin boundary (must be
717     *     greater than |this.maxMinBoundary|).
718     * @param {number} binCount Number of bins to be added (must be positive).
719     */
720    addExponentialBins(nextMaxBinBoundary, binCount) {
721      if (binCount <= 0)
722        throw new Error('Bin count must be positive');
723
724      var curMaxBinBoundary = this.maxBinBoundary;
725      if (curMaxBinBoundary <= 0)
726        throw new Error('Current max bin boundary must be positive');
727      if (curMaxBinBoundary >= nextMaxBinBoundary) {
728        throw new Error('The last added max boundary must be greater than ' +
729            'the current max boundary boundary');
730      }
731
732      var binExponentWidth =
733          Math.log(nextMaxBinBoundary / curMaxBinBoundary) / binCount;
734      for (var i = 1; i < binCount; i++) {
735        this.addBinBoundary(
736            curMaxBinBoundary * Math.exp(i * binExponentWidth));
737      }
738      this.addBinBoundary(nextMaxBinBoundary);
739
740      return this;
741    }
742  }
743
744  DEFAULT_BOUNDARIES_FOR_UNIT.set(
745      tr.b.Unit.byName.timeDurationInMs.unitName,
746      HistogramBinBoundaries.createExponential(1e-3, 1e6, 1e2));
747
748  DEFAULT_BOUNDARIES_FOR_UNIT.set(
749      tr.b.Unit.byName.timeStampInMs.unitName,
750      HistogramBinBoundaries.createLinear(0, 1e10, 1e3));
751
752  DEFAULT_BOUNDARIES_FOR_UNIT.set(
753      tr.b.Unit.byName.normalizedPercentage.unitName,
754      HistogramBinBoundaries.createLinear(0, 1.0, 20));
755
756  DEFAULT_BOUNDARIES_FOR_UNIT.set(
757      tr.b.Unit.byName.sizeInBytes.unitName,
758      HistogramBinBoundaries.createExponential(1, 1e12, 1e2));
759
760  DEFAULT_BOUNDARIES_FOR_UNIT.set(
761      tr.b.Unit.byName.energyInJoules.unitName,
762      HistogramBinBoundaries.createExponential(1e-3, 1e3, 50));
763
764  DEFAULT_BOUNDARIES_FOR_UNIT.set(
765      tr.b.Unit.byName.powerInWatts.unitName,
766      HistogramBinBoundaries.createExponential(1e-3, 1, 50));
767
768  DEFAULT_BOUNDARIES_FOR_UNIT.set(
769      tr.b.Unit.byName.unitlessNumber.unitName,
770      HistogramBinBoundaries.createExponential(1e-3, 1e3, 50));
771
772  DEFAULT_BOUNDARIES_FOR_UNIT.set(
773      tr.b.Unit.byName.count.unitName,
774      HistogramBinBoundaries.createExponential(1, 1e3, 20));
775
776  return {
777    Significance: Significance,
778    Histogram: Histogram,
779    HistogramBinBoundaries: HistogramBinBoundaries,
780  };
781});
782</script>
783