| <!DOCTYPE html> |
| <!-- |
| Copyright 2016 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/tracing/base/iteration_helpers.html"> |
| <link rel="import" href="/tracing/base/math/range.html"> |
| <link rel="import" href="/tracing/base/math/running_statistics.html"> |
| <link rel="import" href="/tracing/base/math/sorted_array_utils.html"> |
| <link rel="import" href="/tracing/base/math/statistics.html"> |
| <link rel="import" href="/tracing/base/scalar.html"> |
| <link rel="import" href="/tracing/base/unit.html"> |
| <link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.v', function() { |
| const MAX_DIAGNOSTIC_MAPS = 16; |
| |
| const DEFAULT_SAMPLE_VALUES_PER_BIN = 10; |
| |
| const DEFAULT_REBINNED_COUNT = 40; |
| |
| const DEFAULT_BOUNDARIES_FOR_UNIT = new Map(); |
| |
| const DELTA = String.fromCharCode(916); |
| const Z_SCORE_NAME = 'z-score'; |
| const P_VALUE_NAME = 'p-value'; |
| const U_STATISTIC_NAME = 'U'; |
| |
| /** |
| * Converts the given percent to a string in the format specified above. |
| * @param {number} percent The percent must be between 0.0 and 1.0. |
| * @return {string} |
| */ |
| function percentToString(percent) { |
| if (percent < 0 || percent > 1) { |
| throw new Error('percent must be in [0,1]'); |
| } |
| if (percent === 0) return '000'; |
| if (percent === 1) return '100'; |
| let str = percent.toString(); |
| if (str[1] !== '.') { |
| throw new Error('Unexpected percent'); |
| } |
| // Pad short strings with zeros. |
| str = str + '0'.repeat(Math.max(4 - str.length, 0)); |
| if (str.length > 4) str = str.slice(0, 4) + '_' + str.slice(4); |
| return '0' + str.slice(2); |
| } |
| |
| /** |
| * Converts the given string to a percent between 0 and 1. |
| * @param {string} |
| * @return {number} |
| */ |
| function percentFromString(s) { |
| return parseFloat(s[0] + '.' + s.substr(1).replace(/_/g, '')); |
| } |
| |
| class HistogramBin { |
| /** |
| * @param {!tr.b.math.Range} range |
| */ |
| constructor(range) { |
| this.range = range; |
| this.count = 0; |
| this.diagnosticMaps = []; |
| } |
| |
| /** |
| * @param {*} value |
| */ |
| addSample(value) { |
| this.count += 1; |
| } |
| |
| /** |
| * @param {!tr.v.d.DiagnosticMap} diagnostics |
| */ |
| addDiagnosticMap(diagnostics) { |
| tr.b.math.Statistics.uniformlySampleStream( |
| this.diagnosticMaps, this.count, diagnostics, MAX_DIAGNOSTIC_MAPS); |
| } |
| |
| addBin(other) { |
| if (!this.range.equals(other.range)) { |
| throw new Error('Merging incompatible Histogram bins.'); |
| } |
| tr.b.math.Statistics.mergeSampledStreams(this.diagnosticMaps, this.count, |
| other.diagnosticMaps, other.count, MAX_DIAGNOSTIC_MAPS); |
| this.count += other.count; |
| } |
| |
| fromDict(dict) { |
| this.count = dict[0]; |
| if (dict.length > 1) { |
| for (let map of dict[1]) { |
| this.diagnosticMaps.push(tr.v.d.DiagnosticMap.fromDict(map)); |
| } |
| } |
| } |
| |
| asDict() { |
| if (!this.diagnosticMaps.length) { |
| return [this.count]; |
| } |
| // It's more efficient to serialize these 2 fields in an array. If you |
| // add any other fields, you should re-evaluate whether it would be more |
| // efficient to serialize as a dict. |
| return [this.count, this.diagnosticMaps.map(d => d.asDict())]; |
| } |
| } |
| |
| const DEFAULT_SUMMARY_OPTIONS = new Map([ |
| ['avg', true], |
| ['count', true], |
| ['geometricMean', false], |
| ['max', true], |
| ['min', true], |
| ['nans', false], |
| ['std', true], |
| ['sum', true], |
| // Don't include 'percentile' here. Its default value is [], which is |
| // modifiable. Callers may push to it, so there must be a different Array |
| // instance for each Histogram instance. |
| ]); |
| |
| /** |
| * This is basically a histogram, but so much more. |
| * Histogram is serializable using asDict/fromDict. |
| * Histogram computes several statistics of its contents. |
| * Histograms can be merged. |
| * getDifferenceSignificance() test whether one Histogram is statistically |
| * significantly different from another Histogram. |
| * Histogram stores a random sample of the exact number values added to it. |
| * Histogram stores a random sample of optional per-sample DiagnosticMaps. |
| * Histogram is visualized by <tr-v-ui-histogram-span>, which supports |
| * selecting bins, and visualizing the DiagnosticMaps of selected bins. |
| * |
| * @param {!tr.b.Unit} unit |
| * @param {!tr.v.HistogramBinBoundaries=} opt_binBoundaries |
| */ |
| class Histogram { |
| constructor(name, unit, opt_binBoundaries) { |
| let binBoundaries = opt_binBoundaries; |
| if (!binBoundaries) { |
| let baseUnit = unit.baseUnit ? unit.baseUnit : unit; |
| binBoundaries = DEFAULT_BOUNDARIES_FOR_UNIT.get(baseUnit.unitName); |
| } |
| |
| // If this Histogram is being deserialized, then its guid will be set by |
| // fromDict(). |
| // If this Histogram is being computed by a metric, then its guid will be |
| // allocated the first time the guid is gotten by asDict(). |
| this.guid_ = undefined; |
| |
| // Serialize binBoundaries here instead of holding a reference to it in |
| // case it is modified. |
| this.binBoundariesDict_ = binBoundaries.asDict(); |
| |
| this.allBins = []; |
| this.description = ''; |
| this.diagnostics = new tr.v.d.DiagnosticMap(); |
| this.name_ = name; |
| this.nanDiagnosticMaps = []; |
| this.numNans = 0; |
| this.running_ = undefined; |
| this.sampleValues_ = []; |
| this.shortName = undefined; |
| this.summaryOptions = new Map(DEFAULT_SUMMARY_OPTIONS); |
| this.summaryOptions.set('percentile', []); |
| this.unit = unit; |
| |
| this.allBins.length = binBoundaries.binRanges.length; |
| for (let i = 0; i < this.allBins.length; ++i) { |
| this.allBins[i] = new HistogramBin(binBoundaries.binRanges[i]); |
| } |
| |
| this.maxNumSampleValues_ = this.defaultMaxNumSampleValues_; |
| } |
| |
| get running() { |
| return this.running_; |
| } |
| |
| get maxNumSampleValues() { |
| return this.maxNumSampleValues_; |
| } |
| |
| set maxNumSampleValues(n) { |
| this.maxNumSampleValues_ = n; |
| tr.b.math.Statistics.uniformlySampleArray( |
| this.sampleValues_, this.maxNumSampleValues_); |
| } |
| |
| get name() { |
| return this.name_; |
| } |
| |
| get guid() { |
| if (this.guid_ === undefined) { |
| this.guid_ = tr.b.GUID.allocateUUID4(); |
| } |
| |
| return this.guid_; |
| } |
| |
| set guid(guid) { |
| if (this.guid_ !== undefined) { |
| throw new Error('Cannot reset guid'); |
| } |
| |
| this.guid_ = guid; |
| } |
| |
| static fromDict(dict) { |
| let hist = new Histogram(dict.name, tr.b.Unit.fromJSON(dict.unit), |
| HistogramBinBoundaries.fromDict(dict.binBoundaries)); |
| hist.guid = dict.guid; |
| if (dict.shortName) { |
| hist.shortName = dict.shortName; |
| } |
| if (dict.description) { |
| hist.description = dict.description; |
| } |
| if (dict.diagnostics) { |
| hist.diagnostics.addDicts(dict.diagnostics); |
| } |
| if (dict.allBins) { |
| if (dict.allBins.length !== undefined) { |
| for (let i = 0; i < dict.allBins.length; ++i) { |
| hist.allBins[i].fromDict(dict.allBins[i]); |
| } |
| } else { |
| for (var [i, binDict] of Object.entries(dict.allBins)) { |
| hist.allBins[i].fromDict(binDict); |
| } |
| } |
| } |
| if (dict.running) { |
| hist.running_ = tr.b.math.RunningStatistics.fromDict(dict.running); |
| } |
| if (dict.summaryOptions) { |
| hist.customizeSummaryOptions(dict.summaryOptions); |
| } |
| if (dict.maxNumSampleValues !== undefined) { |
| hist.maxNumSampleValues = dict.maxNumSampleValues; |
| } |
| if (dict.sampleValues) { |
| hist.sampleValues_ = dict.sampleValues; |
| } |
| if (dict.numNans) { |
| hist.numNans = dict.numNans; |
| } |
| if (dict.nanDiagnostics) { |
| for (let map of dict.nanDiagnostics) { |
| hist.nanDiagnosticMaps.push(tr.v.d.DiagnosticMap.fromDict(map)); |
| } |
| } |
| return hist; |
| } |
| |
| get numValues() { |
| return this.running_ ? this.running_.count : 0; |
| } |
| |
| get average() { |
| return this.running_ ? this.running_.mean : undefined; |
| } |
| |
| get standardDeviation() { |
| return this.running_ ? this.running_.stddev : undefined; |
| } |
| |
| get geometricMean() { |
| return this.running_ ? this.running_.geometricMean : 0; |
| } |
| |
| get sum() { |
| return this.running_ ? this.running_.sum : 0; |
| } |
| |
| get min() { |
| return this.running_ ? this.running_.min : Infinity; |
| } |
| |
| get max() { |
| return this.running_ ? this.running_.max : -Infinity; |
| } |
| |
| /** |
| * Requires that units agree. |
| * Returns DONT_CARE if that is the units' improvementDirection. |
| * Returns SIGNIFICANT if the Mann-Whitney U test returns a |
| * p-value less than opt_alpha or DEFAULT_ALPHA. Returns INSIGNIFICANT if |
| * the p-value is greater than alpha. |
| * |
| * @param {!tr.v.Histogram} other |
| * @param {number=} opt_alpha |
| * @return {!tr.b.math.Statistics.Significance} |
| */ |
| getDifferenceSignificance(other, opt_alpha) { |
| if (this.unit !== other.unit) { |
| throw new Error('Cannot compare Histograms with different units'); |
| } |
| |
| if (this.unit.improvementDirection === |
| tr.b.ImprovementDirection.DONT_CARE) { |
| return tr.b.math.Statistics.Significance.DONT_CARE; |
| } |
| |
| if (!(other instanceof Histogram)) { |
| throw new Error('Unable to compute a p-value'); |
| } |
| |
| let testResult = tr.b.math.Statistics.mwu( |
| this.sampleValues, other.sampleValues, opt_alpha); |
| return testResult.significance; |
| } |
| |
| /* |
| * Compute an approximation of percentile based on the counts in the bins. |
| * If the real percentile lies within |this.range| then the result of |
| * the function will deviate from the real percentile by at most |
| * the maximum width of the bin(s) within which the point(s) |
| * from which the real percentile would be calculated lie. |
| * If the real percentile is outside |this.range| then the function |
| * returns the closest range limit: |this.range.min| or |this.range.max|. |
| * |
| * @param {number} percent The percent must be between 0.0 and 1.0. |
| */ |
| getApproximatePercentile(percent) { |
| if (percent < 0 || percent > 1) { |
| throw new Error('percent must be in [0,1]'); |
| } |
| if (this.numValues === 0) { |
| return 0; |
| } |
| if (this.allBins.length === 1) { |
| // Copy sampleValues, don't sort them in place, in order to preserve |
| // insertion order. |
| let sortedSampleValues = this.sampleValues.slice().sort( |
| (x, y) => x - y); |
| return sortedSampleValues[Math.floor((sortedSampleValues.length - 1) * |
| percent)]; |
| } |
| let valuesToSkip = Math.floor((this.numValues - 1) * percent); |
| for (let bin of this.allBins) { |
| valuesToSkip -= bin.count; |
| if (valuesToSkip >= 0) continue; |
| if (bin.range.min === -Number.MAX_VALUE) |
| return bin.range.max; |
| if (bin.range.max === Number.MAX_VALUE) |
| return bin.range.min; |
| return bin.range.center; |
| } |
| return this.allBins[this.allBins.length - 1].range.min; |
| } |
| |
| getBinForValue(value) { |
| // Don't use subtraction to avoid arithmetic overflow. |
| let binIndex = tr.b.math.findHighIndexInSortedArray( |
| this.allBins, b => ((value < b.range.max) ? -1 : 1)); |
| return this.allBins[binIndex] || this.allBins[this.allBins.length - 1]; |
| } |
| |
| /** |
| * @param {number|*} value |
| * @param {(!Object|!tr.v.d.DiagnosticMap)=} opt_diagnostics |
| */ |
| addSample(value, opt_diagnostics) { |
| if (opt_diagnostics && |
| !(opt_diagnostics instanceof tr.v.d.DiagnosticMap)) { |
| opt_diagnostics = tr.v.d.DiagnosticMap.fromObject(opt_diagnostics); |
| } |
| |
| if (typeof(value) !== 'number' || isNaN(value)) { |
| this.numNans++; |
| if (opt_diagnostics) { |
| tr.b.math.Statistics.uniformlySampleStream(this.nanDiagnosticMaps, |
| this.numNans, opt_diagnostics, MAX_DIAGNOSTIC_MAPS); |
| } |
| } else { |
| if (this.running_ === undefined) { |
| this.running_ = new tr.b.math.RunningStatistics(); |
| } |
| this.running_.add(value); |
| |
| let bin = this.getBinForValue(value); |
| bin.addSample(value); |
| if (opt_diagnostics) { |
| bin.addDiagnosticMap(opt_diagnostics); |
| } |
| } |
| |
| tr.b.math.Statistics.uniformlySampleStream(this.sampleValues_, |
| this.numValues + this.numNans, value, this.maxNumSampleValues); |
| } |
| |
| sampleValuesInto(samples) { |
| for (let sampleValue of this.sampleValues) { |
| samples.push(sampleValue); |
| } |
| } |
| |
| /** |
| * Return true if this Histogram can be added to |other|. |
| * |
| * @param {!tr.v.Histogram} other |
| * @return {boolean} |
| */ |
| canAddHistogram(other) { |
| if (this.unit !== other.unit) { |
| return false; |
| } |
| if (this.binBoundariesDict_ === other.binBoundariesDict_) { |
| return true; |
| } |
| // |binBoundariesDict_| may be equal even if they are not the same object. |
| if (this.binBoundariesDict_.length !== other.binBoundariesDict_.length) { |
| return false; |
| } |
| for (let i = 0; i < this.binBoundariesDict_.length; ++i) { |
| let slice = this.binBoundariesDict_[i]; |
| let otherSlice = other.binBoundariesDict_[i]; |
| if (slice instanceof Array) { |
| if (!(otherSlice instanceof Array)) { |
| return false; |
| } |
| if (slice[0] !== otherSlice[0] || |
| !tr.b.math.approximately(slice[1], otherSlice[1]) || |
| slice[2] !== otherSlice[2]) { |
| return false; |
| } |
| } else { |
| if (otherSlice instanceof Array) { |
| return false; |
| } |
| if (!tr.b.math.approximately(slice, otherSlice)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Add |other| to this Histogram in-place if they can be added. |
| * |
| * @param {!tr.v.Histogram} other |
| */ |
| addHistogram(other) { |
| if (!this.canAddHistogram(other)) { |
| throw new Error('Merging incompatible Histograms'); |
| } |
| |
| tr.b.math.Statistics.mergeSampledStreams(this.nanDiagnosticMaps, |
| this.numNans, other.nanDiagnosticMaps, other.numNans, |
| MAX_DIAGNOSTIC_MAPS); |
| tr.b.math.Statistics.mergeSampledStreams( |
| this.sampleValues, this.numValues + this.numNans, |
| other.sampleValues, other.numValues + other.numNans, |
| (this.maxNumSampleValues + other.maxNumSampleValues) / 2); |
| this.numNans += other.numNans; |
| |
| if (other.running_ !== undefined) { |
| if (this.running_ === undefined) { |
| this.running_ = new tr.b.math.RunningStatistics(); |
| } |
| this.running_ = this.running_.merge(other.running_); |
| } |
| |
| for (let i = 0; i < this.allBins.length; ++i) { |
| this.allBins[i].addBin(other.allBins[i]); |
| } |
| |
| let mergedFrom = this.diagnostics.get(tr.v.d.MERGED_FROM_DIAGNOSTIC_KEY); |
| if (!mergedFrom) { |
| mergedFrom = new tr.v.d.RelatedHistogramSet(); |
| this.diagnostics.set(tr.v.d.MERGED_FROM_DIAGNOSTIC_KEY, mergedFrom); |
| } |
| mergedFrom.add(other); |
| |
| let mergedTo = other.diagnostics.get(tr.v.d.MERGED_TO_DIAGNOSTIC_KEY); |
| if (!mergedTo) { |
| mergedTo = new tr.v.d.RelatedHistogramSet(); |
| other.diagnostics.set(tr.v.d.MERGED_TO_DIAGNOSTIC_KEY, mergedTo); |
| } |
| mergedTo.add(this); |
| |
| this.diagnostics.addDiagnostics(other.diagnostics); |
| |
| for (let [stat, option] of other.summaryOptions) { |
| if (stat === 'percentile') { |
| for (let percent of option) { |
| let percentiles = this.summaryOptions.get(stat); |
| if (percentiles.indexOf(percent) < 0) { |
| percentiles.push(percent); |
| } |
| } |
| } else if (option && !this.summaryOptions.get(stat)) { |
| this.summaryOptions.set(stat, true); |
| } |
| } |
| } |
| |
| /** |
| * Controls which statistics are exported to dashboard for this Histogram. |
| * The options not included in the |summaryOptions| will not change. |
| * |
| * @param {!Object} summaryOptions |
| * @param {boolean=} summaryOptions.avg |
| * @param {boolean=} summaryOptions.count |
| * @param {boolean=} summaryOptions.geometricMean |
| * @param {boolean=} summaryOptions.max |
| * @param {boolean=} summaryOptions.min |
| * @param {boolean=} summaryOptions.nans |
| * @param {boolean=} summaryOptions.std |
| * @param {boolean=} summaryOptions.sum |
| * @param {!Array.<number>=} summaryOptions.percentile Numbers in (0,1) |
| */ |
| customizeSummaryOptions(summaryOptions) { |
| for (var [key, value] of Object.entries(summaryOptions)) { |
| this.summaryOptions.set(key, value); |
| } |
| } |
| |
| /** |
| * @param {string} statName |
| * @param {!tr.v.Histogram=} opt_referenceHistogram |
| * @param {!HypothesisTestResult=} opt_mwu |
| * @return {!tr.b.Scalar} |
| * @throws {Error} When statName is not recognized, such as delta statistics |
| * when !this.canCompare(opt_referenceHistograms). |
| */ |
| getStatisticScalar(statName, opt_referenceHistogram, opt_mwu) { |
| if (statName === 'avg') { |
| if (this.running_ === undefined) return undefined; |
| return new tr.b.Scalar(this.unit, this.average); |
| } |
| if (statName === 'std') { |
| if (this.standardDeviation === undefined) return undefined; |
| return new tr.b.Scalar(this.unit, this.standardDeviation); |
| } |
| if (statName === 'geometricMean') { |
| return new tr.b.Scalar(this.unit, this.geometricMean); |
| } |
| if (statName === 'min' || statName === 'max' || statName === 'sum') { |
| if (this.running_ === undefined) { |
| this.running_ = new tr.b.math.RunningStatistics(); |
| } |
| return new tr.b.Scalar(this.unit, this.running_[statName]); |
| } |
| if (statName === 'nans') { |
| return new tr.b.Scalar( |
| tr.b.Unit.byName.count_smallerIsBetter, this.numNans); |
| } |
| if (statName === 'count') { |
| return new tr.b.Scalar( |
| tr.b.Unit.byName.count_smallerIsBetter, this.numValues); |
| } |
| if (statName.substr(0, 4) === 'pct_') { |
| let percent = percentFromString(statName.substr(4)); |
| let percentile = this.getApproximatePercentile(percent); |
| return new tr.b.Scalar(this.unit, percentile); |
| } |
| |
| if (!this.canCompare(opt_referenceHistogram)) { |
| throw new Error( |
| 'Cannot compute ' + statName + |
| ' when histograms are not comparable'); |
| } |
| |
| const suffix = tr.b.Unit.nameSuffixForImprovementDirection( |
| this.unit.improvementDirection); |
| |
| const deltaIndex = statName.indexOf(DELTA); |
| if (deltaIndex >= 0) { |
| const baseStatName = statName.substr(deltaIndex + 1); |
| const thisStat = this.getStatisticScalar(baseStatName); |
| const otherStat = opt_referenceHistogram.getStatisticScalar( |
| baseStatName); |
| const deltaValue = thisStat.value - otherStat.value; |
| |
| if (statName[0] === '%') { |
| return new tr.b.Scalar( |
| tr.b.Unit.byName['normalizedPercentageDelta' + suffix], |
| deltaValue / otherStat.value); |
| } |
| return new tr.b.Scalar( |
| thisStat.unit.correspondingDeltaUnit, deltaValue); |
| } |
| |
| if (statName === Z_SCORE_NAME) { |
| return new tr.b.Scalar( |
| tr.b.Unit.byName['sigmaDelta' + suffix], |
| (this.average - opt_referenceHistogram.average) / |
| opt_referenceHistogram.standardDeviation); |
| } |
| |
| let mwu = opt_mwu || tr.b.math.Statistics.mwu( |
| this.sampleValues, opt_referenceHistogram.sampleValues); |
| if (statName === P_VALUE_NAME) { |
| return new tr.b.Scalar(tr.b.Unit.byName.unitlessNumber, mwu.p); |
| } |
| if (statName === U_STATISTIC_NAME) { |
| return new tr.b.Scalar(tr.b.Unit.byName.unitlessNumber, mwu.U); |
| } |
| |
| throw new Error('Unrecognized statistic name: ' + statName); |
| } |
| |
| /** |
| * @return {!Array.<string>} names of enabled summary statistics |
| */ |
| get statisticsNames() { |
| let statisticsNames = new Set(); |
| for (let [statName, option] of this.summaryOptions) { |
| if (statName === 'percentile') { |
| for (let pctile of option) { |
| statisticsNames.add('pct_' + tr.v.percentToString(pctile)); |
| } |
| } else if (option) { |
| statisticsNames.add(statName); |
| } |
| } |
| return statisticsNames; |
| } |
| |
| /** |
| * Returns true if delta statistics can be computed between |this| and |
| * |other|. |
| * |
| * @param {!tr.v.Histogram=} other |
| * @return {boolean} |
| */ |
| canCompare(other) { |
| return other instanceof Histogram && |
| this.unit === other.unit && |
| this.numValues > 0 && |
| other.numValues > 0; |
| } |
| |
| /** |
| * Returns |statName| if it can be computed, or the related non-delta |
| * statistic if |statName| is a delta statistic and |
| * !this.canCompare(opt_referenceHist). |
| * |
| * @param {string} statName |
| * @param {!tr.v.Histogram=} opt_referenceHist |
| * @return {string} |
| */ |
| getAvailableStatisticName(statName, opt_referenceHist) { |
| if (this.canCompare(opt_referenceHist)) return statName; |
| if (statName === Z_SCORE_NAME || |
| statName === P_VALUE_NAME || |
| statName === U_STATISTIC_NAME) { |
| return 'avg'; |
| } |
| const deltaIndex = statName.indexOf(DELTA); |
| if (deltaIndex < 0) return statName; |
| return statName.substr(deltaIndex + 1); |
| } |
| |
| /** |
| * Returns names of delta statistics versions of given non-delta statistics |
| * names. |
| * |
| * @param {!Array.<string>} statNames |
| * @return {!Array.<string>} |
| */ |
| static getDeltaStatisticsNames(statNames) { |
| const deltaNames = []; |
| for (const statName of statNames) { |
| deltaNames.push(`${DELTA}${statName}`); |
| deltaNames.push(`%${DELTA}${statName}`); |
| } |
| return deltaNames.concat([Z_SCORE_NAME, P_VALUE_NAME, U_STATISTIC_NAME]); |
| } |
| |
| /** |
| * Returns a Map {statisticName: Scalar}. |
| * |
| * Each enabled summary option produces the corresponding value: |
| * min, max, count, sum, avg, or std. |
| * Each percentile 0.x produces pct_0x0. |
| * Each percentile 0.xx produces pct_0xx. |
| * Each percentile 0.xxy produces pct_0xx_y. |
| * Percentile 1.0 produces pct_100. |
| * |
| * @return {!Map.<string, Scalar>} |
| */ |
| get statisticsScalars() { |
| let results = new Map(); |
| for (let statName of this.statisticsNames) { |
| let scalar = this.getStatisticScalar(statName); |
| if (scalar === undefined) continue; |
| results.set(statName, scalar); |
| } |
| return results; |
| } |
| |
| get sampleValues() { |
| return this.sampleValues_; |
| } |
| |
| /** |
| * Create a new Histogram instance that is just like |this| except for its |
| * guid. This is useful when merging Histograms. |
| * @return {!tr.v.Histogram} |
| */ |
| clone() { |
| let binBoundaries = HistogramBinBoundaries.fromDict( |
| this.binBoundariesDict_); |
| let hist = new Histogram(this.name, this.unit, binBoundaries); |
| for (let [stat, option] of this.summaryOptions) { |
| // Copy arrays. |
| if (stat === 'percentile') { |
| option = Array.from(option); |
| } |
| hist.summaryOptions.set(stat, option); |
| } |
| hist.addHistogram(this); |
| return hist; |
| } |
| |
| /** |
| * Produce a Histogram with |this| Histogram's name, unit, description, |
| * statistics, summaryOptions, sampleValues, and diagnostics, but with |
| * |newBoundaries|. |
| * guid and sample diagnostics are not copied. In-bound Relationship |
| * diagnostics are broken. |
| * |
| * @param {!tr.v.HistogramBinBoundaries} newBoundaries |
| * @return {!tr.v.Histogram} |
| */ |
| rebin(newBoundaries) { |
| const rebinned = new tr.v.Histogram(this.name, this.unit, newBoundaries); |
| rebinned.description = this.description; |
| for (const sample of this.sampleValues) { |
| rebinned.addSample(sample); |
| } |
| rebinned.running_ = this.running_; |
| for (const [name, diagnostic] of this.diagnostics) { |
| rebinned.diagnostics.set(name, diagnostic); |
| } |
| for (let [stat, option] of this.summaryOptions) { |
| // Copy the array of percentiles. |
| if (stat === 'percentile') option = Array.from(option); |
| rebinned.summaryOptions.set(stat, option); |
| } |
| return rebinned; |
| } |
| |
| asDict() { |
| let dict = {}; |
| dict.name = this.name; |
| dict.unit = this.unit.asJSON(); |
| dict.guid = this.guid; |
| if (this.binBoundariesDict_ !== undefined) { |
| dict.binBoundaries = this.binBoundariesDict_; |
| } |
| if (this.shortName) { |
| dict.shortName = this.shortName; |
| } |
| if (this.description) { |
| dict.description = this.description; |
| } |
| if (this.diagnostics.size) { |
| dict.diagnostics = this.diagnostics.asDict(); |
| } |
| if (this.maxNumSampleValues !== this.defaultMaxNumSampleValues_) { |
| dict.maxNumSampleValues = this.maxNumSampleValues; |
| } |
| if (this.numNans) { |
| dict.numNans = this.numNans; |
| } |
| if (this.nanDiagnosticMaps.length) { |
| dict.nanDiagnostics = this.nanDiagnosticMaps.map( |
| dm => dm.asDict()); |
| } |
| |
| if (this.numValues) { |
| dict.sampleValues = this.sampleValues.slice(); |
| dict.running = this.running_.asDict(); |
| dict.allBins = this.allBinsAsDict_(); |
| } |
| |
| let summaryOptions = {}; |
| let anyOverriddenSummaryOptions = false; |
| for (let [name, option] of this.summaryOptions) { |
| if (name === 'percentile') { |
| if (option.length === 0) { |
| continue; |
| } |
| option = option.slice(); |
| } else if (option === DEFAULT_SUMMARY_OPTIONS.get(name)) { |
| continue; |
| } |
| summaryOptions[name] = option; |
| anyOverriddenSummaryOptions = true; |
| } |
| if (anyOverriddenSummaryOptions) { |
| dict.summaryOptions = summaryOptions; |
| } |
| |
| return dict; |
| } |
| |
| allBinsAsDict_() { |
| // dict.allBins may be either an array or a dict, whichever is more |
| // efficient. |
| // The overhead of the array form is significant when the histogram is |
| // sparse, and the overhead of the dict form is significant when the |
| // histogram is dense. |
| // The dict form is more efficient when more than half of allBins are |
| // empty. The array form is more efficient when fewer than half of |
| // allBins are empty. |
| |
| let numBins = this.allBins.length; |
| |
| // If allBins are empty, then don't serialize anything for them. |
| let emptyBins = 0; |
| |
| for (let i = 0; i < numBins; ++i) { |
| if (this.allBins[i].count === 0) { |
| ++emptyBins; |
| } |
| } |
| |
| if (emptyBins === numBins) { |
| return undefined; |
| } |
| |
| if (emptyBins > (numBins / 2)) { |
| let allBinsDict = {}; |
| for (let i = 0; i < numBins; ++i) { |
| let bin = this.allBins[i]; |
| if (bin.count > 0) { |
| allBinsDict[i] = bin.asDict(); |
| } |
| } |
| return allBinsDict; |
| } |
| |
| let allBinsArray = []; |
| for (let i = 0; i < numBins; ++i) { |
| allBinsArray.push(this.allBins[i].asDict()); |
| } |
| return allBinsArray; |
| } |
| |
| get defaultMaxNumSampleValues_() { |
| // Single-bin histograms might be rebin()ned, so they should retain enough |
| // samples that the rebinned histogram looks close enough. |
| return DEFAULT_SAMPLE_VALUES_PER_BIN * Math.max( |
| this.allBins.length, DEFAULT_REBINNED_COUNT); |
| } |
| } |
| |
| const HISTOGRAM_BIN_BOUNDARIES_CACHE = new Map(); |
| |
| /* |
| * Reusable builder for tr.v.Histogram objects. |
| * |
| * The bins of the Histogram are specified by adding the desired boundaries |
| * between bins. Initially, the builder has only a single boundary: |
| * |
| * range.min=range.max |
| * | |
| * | |
| * -MAX_VALUE <-----|-----------> +MAX_VALUE |
| * : resulting : resulting : |
| * : underflow : overflow : |
| * : bin : bin : |
| * |
| * If the single boundary is set to either -Number.MAX_VALUE or |
| * +Number.MAX_VALUE, then the builder will construct only a single bin: |
| * |
| * range.min=range.max |
| * | |
| * | |
| * -MAX_VALUE <-> +MAX_VALUE |
| * : resulting : |
| * : bin : |
| * |
| * More boundaries can be added (in increasing order) using addBinBoundary, |
| * addLinearBins and addExponentialBins: |
| * |
| * range.min range.max |
| * | | | | | |
| * | | | | | |
| * -MAX_VALUE <------|---------|---------|-----|---------|------> +MAX_VALUE |
| * : resulting : result. : result. : : result. : resulting : |
| * : underflow : central : central : ... : central : overflow : |
| * : bin : bin 0 : bin 1 : : bin N-1 : bin : |
| * |
| * An important feature of the builder is that it's reusable, i.e. it can be |
| * used to build multiple Histograms with the same bin structure. |
| */ |
| class HistogramBinBoundaries { |
| /** |
| * Create a linearly scaled tr.v.HistogramBinBoundaries with |numBins| bins |
| * ranging from |min| to |max|. |
| * |
| * @param {number} min |
| * @param {number} max |
| * @param {number} numBins |
| * @return {tr.v.HistogramBinBoundaries} |
| */ |
| static createLinear(min, max, numBins) { |
| return new HistogramBinBoundaries(min).addLinearBins(max, numBins); |
| } |
| |
| /** |
| * Create an exponentially scaled tr.v.HistogramBinBoundaries with |numBins| |
| * bins ranging from |min| to |max|. |
| * |
| * @param {number} min |
| * @param {number} max |
| * @param {number} numBins |
| * @return {tr.v.HistogramBinBoundaries} |
| */ |
| static createExponential(min, max, numBins) { |
| return new HistogramBinBoundaries(min).addExponentialBins(max, numBins); |
| } |
| |
| /** |
| * @param {Array.<number>} binBoundaries |
| */ |
| static createWithBoundaries(binBoundaries) { |
| let builder = new HistogramBinBoundaries(binBoundaries[0]); |
| for (let boundary of binBoundaries.slice(1)) { |
| builder.addBinBoundary(boundary); |
| } |
| return builder; |
| } |
| |
| /** |
| * |minBinBoundary| will be the boundary between the underflow bin and the |
| * first central bin if other bin boundaries are added. |
| * If no other bin boundaries are added, then |minBinBoundary| will be the |
| * boundary between the underflow bin and the overflow bin. |
| * If no other bin boundaries are added and |minBinBoundary| is either |
| * -Number.MAX_VALUE or +Number.MAX_VALUE, then only a single binRange will |
| * be built. |
| * |
| * @param {number} minBinBoundary The minimum boundary between bins. |
| */ |
| constructor(minBinBoundary) { |
| this.builder_ = [minBinBoundary]; |
| this.range_ = new tr.b.math.Range(); |
| this.range_.addValue(minBinBoundary); |
| this.binRanges_ = undefined; |
| } |
| |
| get range() { |
| return this.range_; |
| } |
| |
| asDict() { |
| if (this.builder_.length === 1 && this.builder_[0] === Number.MAX_VALUE) { |
| return undefined; |
| } |
| |
| // Don't copy builder_ here so that Histogram.canAddHistogram() can test |
| // for object identity. |
| return this.builder_; |
| } |
| |
| pushBuilderSlice_(slice) { |
| this.builder_.push(slice); |
| // Copy builder_ when it's modified so that Histogram.canAddHistogram() |
| // can test for object identity. |
| this.builder_ = this.builder_.slice(); |
| } |
| |
| static fromDict(dict) { |
| if (dict === undefined) { |
| return HistogramBinBoundaries.SINGULAR; |
| } |
| |
| // When loading a results2.html with many Histograms with the same bin |
| // boundaries, caching the HistogramBinBoundaries not only speeds up |
| // loading, but also prevents a bug where build_ is occasionally |
| // non-deterministic, which causes similar Histograms to be unmergeable. |
| let cacheKey = JSON.stringify(dict); |
| if (HISTOGRAM_BIN_BOUNDARIES_CACHE.has(cacheKey)) { |
| return HISTOGRAM_BIN_BOUNDARIES_CACHE.get(cacheKey); |
| } |
| |
| let binBoundaries = new HistogramBinBoundaries(dict[0]); |
| for (let slice of dict.slice(1)) { |
| if (!(slice instanceof Array)) { |
| binBoundaries.addBinBoundary(slice); |
| continue; |
| } |
| switch (slice[0]) { |
| case HistogramBinBoundaries.SLICE_TYPE.LINEAR: |
| binBoundaries.addLinearBins(slice[1], slice[2]); |
| break; |
| |
| case HistogramBinBoundaries.SLICE_TYPE.EXPONENTIAL: |
| binBoundaries.addExponentialBins(slice[1], slice[2]); |
| break; |
| |
| default: |
| throw new Error('Unrecognized HistogramBinBoundaries slice type'); |
| } |
| } |
| HISTOGRAM_BIN_BOUNDARIES_CACHE.set(cacheKey, binBoundaries); |
| return binBoundaries; |
| } |
| |
| /** |
| * @return {!Array.<!tr.b.math.Range>} |
| */ |
| get binRanges() { |
| if (this.binRanges_ === undefined) { |
| this.build_(); |
| } |
| return this.binRanges_; |
| } |
| |
| build_() { |
| if (typeof this.builder_[0] !== 'number') { |
| throw new Error('Invalid start of builder_'); |
| } |
| this.binRanges_ = []; |
| let prevBoundary = this.builder_[0]; |
| |
| if (prevBoundary > -Number.MAX_VALUE) { |
| // underflow bin |
| this.binRanges_.push(tr.b.math.Range.fromExplicitRange( |
| -Number.MAX_VALUE, prevBoundary)); |
| } |
| |
| for (let slice of this.builder_.slice(1)) { |
| if (!(slice instanceof Array)) { |
| this.binRanges_.push( |
| tr.b.math.Range.fromExplicitRange(prevBoundary, slice)); |
| prevBoundary = slice; |
| continue; |
| } |
| let nextMaxBinBoundary = slice[1]; |
| let binCount = slice[2]; |
| let sliceMinBinBoundary = prevBoundary; |
| |
| switch (slice[0]) { |
| case HistogramBinBoundaries.SLICE_TYPE.LINEAR: |
| { |
| let binWidth = (nextMaxBinBoundary - prevBoundary) / binCount; |
| for (let i = 1; i < binCount; i++) { |
| let boundary = sliceMinBinBoundary + i * binWidth; |
| this.binRanges_.push(tr.b.math.Range.fromExplicitRange( |
| prevBoundary, boundary)); |
| prevBoundary = boundary; |
| } |
| break; |
| } |
| |
| case HistogramBinBoundaries.SLICE_TYPE.EXPONENTIAL: |
| { |
| let binExponentWidth = |
| Math.log(nextMaxBinBoundary / prevBoundary) / binCount; |
| for (let i = 1; i < binCount; i++) { |
| let boundary = sliceMinBinBoundary * Math.exp( |
| i * binExponentWidth); |
| this.binRanges_.push(tr.b.math.Range.fromExplicitRange( |
| prevBoundary, boundary)); |
| prevBoundary = boundary; |
| } |
| break; |
| } |
| |
| default: |
| throw new Error('Unrecognized HistogramBinBoundaries slice type'); |
| } |
| this.binRanges_.push(tr.b.math.Range.fromExplicitRange( |
| prevBoundary, nextMaxBinBoundary)); |
| prevBoundary = nextMaxBinBoundary; |
| } |
| if (prevBoundary < Number.MAX_VALUE) { |
| // overflow bin |
| this.binRanges_.push(tr.b.math.Range.fromExplicitRange( |
| prevBoundary, Number.MAX_VALUE)); |
| } |
| } |
| |
| /** |
| * Add a bin boundary |nextMaxBinBoundary| to the builder. |
| * |
| * This operation effectively corresponds to appending a new central bin |
| * with the range [this.range.max, nextMaxBinBoundary]. |
| * |
| * @param {number} nextMaxBinBoundary The added bin boundary (must be |
| * greater than |this.maxMinBoundary|). |
| */ |
| addBinBoundary(nextMaxBinBoundary) { |
| if (nextMaxBinBoundary <= this.range.max) { |
| throw new Error('The added max bin boundary must be larger than ' + |
| 'the current max boundary'); |
| } |
| |
| // If binRanges_ had been built, then clear them. |
| this.binRanges_ = undefined; |
| |
| this.pushBuilderSlice_(nextMaxBinBoundary); |
| this.range.addValue(nextMaxBinBoundary); |
| return this; |
| } |
| |
| /** |
| * Add |binCount| linearly scaled bin boundaries up to |nextMaxBinBoundary| |
| * to the builder. |
| * |
| * This operation corresponds to appending |binCount| central bins of |
| * constant range width |
| * W = ((|nextMaxBinBoundary| - |this.range.max|) / |binCount|) |
| * with the following ranges: |
| * |
| * [|this.maxMinBoundary|, |this.maxMinBoundary| + W] |
| * [|this.maxMinBoundary| + W, |this.maxMinBoundary| + 2W] |
| * [|this.maxMinBoundary| + 2W, |this.maxMinBoundary| + 3W] |
| * ... |
| * [|this.maxMinBoundary| + (|binCount| - 2) * W, |
| * |this.maxMinBoundary| + (|binCount| - 2) * W] |
| * [|this.maxMinBoundary| + (|binCount| - 1) * W, |
| * |nextMaxBinBoundary|] |
| * |
| * @param {number} nextBinBoundary The last added bin boundary (must be |
| * greater than |this.maxMinBoundary|). |
| * @param {number} binCount Number of bins to be added (must be positive). |
| */ |
| addLinearBins(nextMaxBinBoundary, binCount) { |
| if (binCount <= 0) { |
| throw new Error('Bin count must be positive'); |
| } |
| |
| if (nextMaxBinBoundary <= this.range.max) { |
| throw new Error('The new max bin boundary must be greater than ' + |
| 'the previous max bin boundary'); |
| } |
| |
| // If binRanges_ had been built, then clear them. |
| this.binRanges_ = undefined; |
| |
| this.pushBuilderSlice_([ |
| HistogramBinBoundaries.SLICE_TYPE.LINEAR, |
| nextMaxBinBoundary, binCount]); |
| this.range.addValue(nextMaxBinBoundary); |
| return this; |
| } |
| |
| /** |
| * Add |binCount| exponentially scaled bin boundaries up to |
| * |nextMaxBinBoundary| to the builder. |
| * |
| * This operation corresponds to appending |binCount| central bins with |
| * a constant difference between the logarithms of their range min and max |
| * D = ((ln(|nextMaxBinBoundary|) - ln(|this.range.max|)) / |binCount|) |
| * with the following ranges: |
| * |
| * [|this.maxMinBoundary|, |this.maxMinBoundary| * exp(D)] |
| * [|this.maxMinBoundary| * exp(D), |this.maxMinBoundary| * exp(2D)] |
| * [|this.maxMinBoundary| * exp(2D), |this.maxMinBoundary| * exp(3D)] |
| * ... |
| * [|this.maxMinBoundary| * exp((|binCount| - 2) * D), |
| * |this.maxMinBoundary| * exp((|binCount| - 2) * D)] |
| * [|this.maxMinBoundary| * exp((|binCount| - 1) * D), |
| * |nextMaxBinBoundary|] |
| * |
| * This method requires that the current max bin boundary is positive. |
| * |
| * @param {number} nextBinBoundary The last added bin boundary (must be |
| * greater than |this.maxMinBoundary|). |
| * @param {number} binCount Number of bins to be added (must be positive). |
| */ |
| addExponentialBins(nextMaxBinBoundary, binCount) { |
| if (binCount <= 0) { |
| throw new Error('Bin count must be positive'); |
| } |
| if (this.range.max <= 0) { |
| throw new Error('Current max bin boundary must be positive'); |
| } |
| if (this.range.max >= nextMaxBinBoundary) { |
| throw new Error('The last added max boundary must be greater than ' + |
| 'the current max boundary boundary'); |
| } |
| |
| // If binRanges_ had been built, then clear them. |
| this.binRanges_ = undefined; |
| |
| this.pushBuilderSlice_([ |
| HistogramBinBoundaries.SLICE_TYPE.EXPONENTIAL, |
| nextMaxBinBoundary, binCount]); |
| this.range.addValue(nextMaxBinBoundary); |
| return this; |
| } |
| } |
| |
| HistogramBinBoundaries.SLICE_TYPE = { |
| LINEAR: 0, |
| EXPONENTIAL: 1, |
| }; |
| |
| // This special HistogramBinBoundaries instance produces a singe binRange, |
| // allowing Histograms to have a single bin. |
| // This is the only way for Histograms to have fewer than 2 bins, since |
| // HistogramBinBoundaries.build_() ensures that there is always a bin whose |
| // min is -Number.MAX_VALUE, and a bin whose max is Number.MAX_VALUE. SINGULAR |
| // is the only HistogramBinBoundaries in which those bins are one and the |
| // same. |
| HistogramBinBoundaries.SINGULAR = new HistogramBinBoundaries( |
| Number.MAX_VALUE); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.timeDurationInMs.unitName, |
| HistogramBinBoundaries.createExponential(1e-3, 1e6, 1e2)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.timeStampInMs.unitName, |
| HistogramBinBoundaries.createLinear(0, 1e10, 1e3)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.normalizedPercentage.unitName, |
| HistogramBinBoundaries.createLinear(0, 1.0, 20)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.sizeInBytes.unitName, |
| HistogramBinBoundaries.createExponential(1, 1e12, 1e2)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.energyInJoules.unitName, |
| HistogramBinBoundaries.createExponential(1e-3, 1e3, 50)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.powerInWatts.unitName, |
| HistogramBinBoundaries.createExponential(1e-3, 1, 50)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.unitlessNumber.unitName, |
| HistogramBinBoundaries.createExponential(1e-3, 1e3, 50)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.count.unitName, |
| HistogramBinBoundaries.createExponential(1, 1e3, 20)); |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT.set( |
| tr.b.Unit.byName.sigma.unitName, |
| HistogramBinBoundaries.createLinear(-5, 5, 50)); |
| |
| return { |
| DEFAULT_REBINNED_COUNT, |
| DELTA, |
| Histogram, |
| HistogramBinBoundaries, |
| P_VALUE_NAME, |
| U_STATISTIC_NAME, |
| Z_SCORE_NAME, |
| percentFromString, |
| percentToString, |
| }; |
| }); |
| </script> |