Home | History | Annotate | Download | only in value
      1 <!DOCTYPE html>
      2 <!--
      3 Copyright 2016 The Chromium Authors. All rights reserved.
      4 Use of this source code is governed by a BSD-style license that can be
      5 found 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 
     20 tr.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.}
    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