| <!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/math/range.html"> |
| <link rel="import" href="/tracing/base/math/running_statistics.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/base/utils.html"> |
| <link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> |
| <link rel="import" href="/tracing/value/diagnostics/reserved_names.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 DEFAULT_ITERATION_FOR_BOOTSTRAP_RESAMPLING = 500; |
| |
| 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. |
| * @param {boolean=} opt_force3 Whether to force the result to be 3 chars long |
| * @return {string} |
| */ |
| function percentToString(percent, opt_force3) { |
| 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) { |
| if (opt_force3) { |
| str = str.slice(0, 4); |
| } else { |
| 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; |
| } |
| |
| deserialize(data, deserializer) { |
| if (!(data instanceof Array)) { |
| this.count = data; |
| return; |
| } |
| this.count = data[0]; |
| for (const sample of data.slice(1)) { |
| // TODO(benjhayden): class Sample |
| if (!(sample instanceof Array)) continue; |
| this.diagnosticMaps.push(tr.v.d.DiagnosticMap.deserialize( |
| sample.slice(1), deserializer)); |
| } |
| } |
| |
| fromDict(dict) { |
| this.count = dict[0]; |
| if (dict.length > 1) { |
| for (const map of dict[1]) { |
| this.diagnosticMaps.push(tr.v.d.DiagnosticMap.fromDict(map)); |
| } |
| } |
| } |
| |
| serialize(serializer) { |
| if (!this.diagnosticMaps.length) { |
| return this.count; |
| } |
| return [this.count, ...this.diagnosticMaps.map(d => [ |
| undefined, ...d.serialize(serializer)])]; |
| } |
| |
| 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' or 'iprs' here. Their default values are [], |
| // which is mutable. 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) { |
| if (!(unit instanceof tr.b.Unit)) { |
| throw new Error('unit must be a Unit: ' + unit); |
| } |
| let binBoundaries = opt_binBoundaries; |
| if (!binBoundaries) { |
| const baseUnit = unit.baseUnit ? unit.baseUnit : unit; |
| binBoundaries = DEFAULT_BOUNDARIES_FOR_UNIT.get(baseUnit.unitName); |
| } |
| |
| // Serialize binBoundaries here instead of holding a reference to it in |
| // case it is modified. |
| this.binBoundariesDict_ = binBoundaries.asDict(); |
| |
| // HistogramBinBoundaries create empty HistogramBins. Save memory by |
| // sharing those empty HistogramBin instances with other Histograms. Wait |
| // to copy HistogramBins until we need to modify it (copy-on-write). |
| this.allBins = binBoundaries.bins.slice(); |
| this.description = ''; |
| const allowReservedNames = false; |
| this.diagnostics_ = new tr.v.d.DiagnosticMap(allowReservedNames); |
| this.maxNumSampleValues_ = this.defaultMaxNumSampleValues_; |
| this.name_ = name; |
| this.nanDiagnosticMaps = []; |
| this.numNans = 0; |
| this.running_ = undefined; |
| this.sampleValues_ = []; |
| this.sampleMeans_ = []; |
| this.summaryOptions = new Map(DEFAULT_SUMMARY_OPTIONS); |
| this.summaryOptions.set('percentile', []); |
| this.summaryOptions.set('iprs', []); |
| this.summaryOptions.set('ci', []); |
| this.unit = unit; |
| } |
| |
| /** |
| * Create a Histogram, configure it, and add samples to it. |
| * |
| * |samples| can be either |
| * 0. a number, or |
| * 1. a dictionary {value: number, diagnostics: dictionary}, or |
| * 2. an array of |
| * 2a. number, or |
| * 2b. dictionaries {value, diagnostics}. |
| * |
| * @param {string} name |
| * @param {!tr.b.Unit} unit |
| * @param {number|!Object|!Array.<(number|!Object)>} samples |
| * @param {!Object=} opt_options |
| * @param {!tr.v.HistogramBinBoundaries} opt_options.binBoundaries |
| * @param {!Object|!Map} opt_options.summaryOptions |
| * @param {!Object|!Map} opt_options.diagnostics |
| * @param {string} opt_options.description |
| * @return {!tr.v.Histogram} |
| */ |
| static create(name, unit, samples, opt_options) { |
| const options = opt_options || {}; |
| const hist = new Histogram(name, unit, options.binBoundaries); |
| if (options.alertGrouping !== undefined) { |
| hist.setAlertGrouping(options.alertGrouping); |
| } |
| |
| if (options.description) hist.description = options.description; |
| |
| if (options.summaryOptions) { |
| let summaryOptions = options.summaryOptions; |
| if (!(summaryOptions instanceof Map)) { |
| summaryOptions = Object.entries(summaryOptions); |
| } |
| for (const [name, value] of summaryOptions) { |
| hist.summaryOptions.set(name, value); |
| } |
| } |
| |
| if (options.diagnostics !== undefined) { |
| let diagnostics = options.diagnostics; |
| if (!(diagnostics instanceof Map)) { |
| diagnostics = Object.entries(diagnostics); |
| } |
| for (const [name, diagnostic] of diagnostics) { |
| if (!diagnostic) continue; |
| hist.diagnostics.set(name, diagnostic); |
| } |
| } |
| |
| if (!(samples instanceof Array)) samples = [samples]; |
| |
| for (const sample of samples) { |
| if (typeof sample === 'object') { |
| hist.addSample(sample.value, sample.diagnostics); |
| } else { |
| hist.addSample(sample); |
| } |
| } |
| |
| return hist; |
| } |
| |
| get diagnostics() { |
| return this.diagnostics_; |
| } |
| |
| /* Set alert grouping for a Histogram. |
| * See https://go/chromeperf-alert-grouping-dd |
| * @param {Array.< String >=} opt_alertGrouping |
| */ |
| setAlertGrouping(alertGrouping) { |
| if (alertGrouping === undefined || |
| alertGrouping === null || |
| alertGrouping.length === undefined) { |
| throw Error('alertGrouping must be an array'); |
| } |
| for (const alertGroup of alertGrouping) { |
| if (!Object.values(tr.v.d.ALERT_GROUPS).includes(alertGroup)) { |
| throw Error(`Alert group ${alertGroup} must be added to ` + |
| '/tracing/value/diagnostics/alert_groups.html'); |
| } |
| } |
| this.diagnostics.set(tr.v.d.RESERVED_NAMES.ALERT_GROUPING, |
| new tr.v.d.GenericSet(alertGrouping)); |
| } |
| |
| 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_; |
| } |
| |
| deserializeStatistics_() { |
| const statisticsNames = this.diagnostics.get( |
| tr.v.d.RESERVED_NAMES.STATISTICS_NAMES); |
| if (!statisticsNames) return; |
| for (const statName of statisticsNames) { |
| if (statName.startsWith('pct_')) { |
| const percent = percentFromString(statName.substr(4)); |
| this.summaryOptions.get('percentile').push(percent); |
| } else if (statName.startsWith('ipr_')) { |
| const lower = percentFromString(statName.substr(4, 3)); |
| const upper = percentFromString(statName.substr(8)); |
| this.summaryOptions.get('iprs').push( |
| tr.b.math.Range.fromExplicitRange(lower, upper)); |
| } else if (statName.startsWith('ci_')) { |
| const percent = percentFromString( |
| statName.replace('_lower', '').replace('_upper', '').substr(3)); |
| if (!this.summaryOptions.get('ci').includes(percent)) { |
| this.summaryOptions.get('ci').push(percent); |
| } |
| } |
| } |
| for (const statName of this.summaryOptions.keys()) { |
| if (statName === 'percentile' || |
| statName === 'iprs' || |
| statName === 'ci') { |
| continue; |
| } |
| this.summaryOptions.set(statName, statisticsNames.has(statName)); |
| } |
| } |
| |
| deserializeBin_(i, bin, deserializer) { |
| // Copy HistogramBin on write, share the rest with the other |
| // Histograms that use the same HistogramBinBoundaries. |
| this.allBins[i] = new HistogramBin(this.allBins[i].range); |
| this.allBins[i].deserialize(bin, deserializer); |
| |
| // TODO(benjhayden): Remove after class Sample. |
| if (!(bin instanceof Array)) return; |
| for (let sample of bin.slice(1)) { |
| if (sample instanceof Array) { |
| sample = sample[0]; |
| } |
| this.sampleValues_.push(sample); |
| } |
| } |
| |
| deserializeBins_(bins, deserializer) { |
| if (bins instanceof Array) { |
| for (let i = 0; i < bins.length; ++i) { |
| this.deserializeBin_(i, bins[i], deserializer); |
| } |
| } else { |
| for (const [i, binData] of Object.entries(bins)) { |
| this.deserializeBin_(i, binData, deserializer); |
| } |
| } |
| } |
| |
| static deserialize(data, deserializer) { |
| const [name, unit, boundaries, diagnostics, running, bins, nanBin] = data; |
| const hist = new Histogram( |
| deserializer.getObject(name), |
| tr.b.Unit.fromJSON(unit), |
| HistogramBinBoundaries.fromDict(deserializer.getObject(boundaries))); |
| |
| hist.diagnostics.deserializeAdd(diagnostics, deserializer); |
| |
| const description = hist.diagnostics.get( |
| tr.v.d.RESERVED_NAMES.DESCRIPTION); |
| if (description && description.length) { |
| hist.description = [...description][0]; |
| } |
| |
| hist.deserializeStatistics_(); |
| |
| if (running) { |
| hist.running_ = tr.b.math.RunningStatistics.fromDict(running); |
| } |
| |
| if (bins) { |
| hist.deserializeBins_(bins, deserializer); |
| } |
| |
| if (nanBin) { |
| // TODO(benjhayden): hist.nanBin |
| if (!(nanBin instanceof Array)) { |
| hist.numNans = nanBin; |
| } else { |
| hist.numNans = nanBin[0]; |
| for (const sample of nanBin.slice(1)) { |
| if (!(sample instanceof Array)) continue; |
| hist.nanDiagnosticMaps.push(tr.v.d.DiagnosticMap.deserialize( |
| sample.slice(1), deserializer)); |
| } |
| } |
| } |
| |
| return hist; |
| } |
| |
| static fromDict(dict) { |
| const hist = new Histogram(dict.name, tr.b.Unit.fromJSON(dict.unit), |
| HistogramBinBoundaries.fromDict(dict.binBoundaries)); |
| 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) { |
| // Copy HistogramBin on write, share the rest with the other |
| // Histograms that use the same HistogramBinBoundaries. |
| hist.allBins[i] = new HistogramBin(hist.allBins[i].range); |
| hist.allBins[i].fromDict(dict.allBins[i]); |
| } |
| } else { |
| for (const [i, binDict] of Object.entries(dict.allBins)) { |
| // Check whether i is a valid index before indexing it. |
| if (i >= hist.allBins.length || i < 0) { |
| throw new Error( |
| 'Invalid index "' + i + |
| '" out of bounds of [0..' + hist.allBins.length + ')'); |
| } |
| |
| hist.allBins[i] = new HistogramBin(hist.allBins[i].range); |
| hist.allBins[i].fromDict(binDict); |
| } |
| } |
| } |
| if (dict.running) { |
| hist.running_ = tr.b.math.RunningStatistics.fromDict(dict.running); |
| } |
| if (dict.summaryOptions) { |
| if (dict.summaryOptions.iprs) { |
| // Range.fromDict() requires isEmpty, which is unnecessarily verbose |
| // for this use case. |
| dict.summaryOptions.iprs = dict.summaryOptions.iprs.map( |
| r => tr.b.math.Range.fromExplicitRange(r[0], r[1])); |
| } |
| 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 (const 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'); |
| } |
| |
| const 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 undefined; |
| if (this.allBins.length === 1) { |
| // Copy sampleValues, don't sort them in place, in order to preserve |
| // insertion order. |
| const 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 (const 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; |
| } |
| |
| getBinIndexForValue(value) { |
| // Don't use subtraction to avoid arithmetic overflow. |
| const i = tr.b.findFirstTrueIndexInSortedArray( |
| this.allBins, b => value < b.range.max); |
| if (0 <= i && i < this.allBins.length) return i; |
| return this.allBins.length - 1; |
| } |
| |
| getBinForValue(value) { |
| return this.allBins[this.getBinIndexForValue(value)]; |
| } |
| |
| /** |
| * @param {number|*} value |
| * @param {(!Object|!tr.v.d.DiagnosticMap)=} opt_diagnostics |
| */ |
| addSample(value, opt_diagnostics) { |
| if (opt_diagnostics) { |
| if (!(opt_diagnostics instanceof tr.v.d.DiagnosticMap)) { |
| opt_diagnostics = tr.v.d.DiagnosticMap.fromObject(opt_diagnostics); |
| } |
| for (const [name, diag] of opt_diagnostics) { |
| if (diag instanceof tr.v.d.Breakdown) { |
| diag.truncate(this.unit); |
| } |
| } |
| } |
| |
| 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.sampleMeans_ = []; |
| this.running_.add(value); |
| |
| value = this.unit.truncate(value); |
| |
| const binIndex = this.getBinIndexForValue(value); |
| let bin = this.allBins[binIndex]; |
| if (bin.count === 0) { |
| // Copy HistogramBin on write, share the rest with the other |
| // Histograms that use the same HistogramBinBoundaries. |
| bin = new HistogramBin(bin.range); |
| this.allBins[binIndex] = bin; |
| } |
| bin.addSample(value); |
| if (opt_diagnostics) { |
| bin.addDiagnosticMap(opt_diagnostics); |
| } |
| } |
| |
| tr.b.math.Statistics.uniformlySampleStream(this.sampleValues_, |
| this.numValues + this.numNans, value, this.maxNumSampleValues); |
| } |
| |
| resampleMean_(percent) { |
| const filteredSamples = this.sampleValues_. |
| filter(value => typeof(value) === 'number' && !isNaN(value)); |
| const sampleCount = filteredSamples.length; |
| if (sampleCount === 0 || percent <= 0.0 || percent >= 1.0) { |
| return [undefined, undefined]; |
| } else if (sampleCount === 1) { |
| return [filteredSamples[0], filteredSamples[0]]; |
| } |
| |
| const iterations = DEFAULT_ITERATION_FOR_BOOTSTRAP_RESAMPLING; |
| if (this.sampleMeans_.length !== iterations) { |
| this.sampleMeans_ = []; |
| for (let i = 0; i < iterations; i++) { |
| let tempSum = 0.0; |
| for (let j = 0; j < sampleCount; j++) { |
| tempSum += filteredSamples[Math.floor( |
| Math.random() * sampleCount)]; |
| } |
| |
| this.sampleMeans_.push(tempSum / sampleCount); |
| } |
| this.sampleMeans_.sort((a, b) => a - b); |
| } |
| |
| return [ |
| this.sampleMeans_[Math.floor((iterations - 1) * (0.5 - percent / 2))], |
| this.sampleMeans_[Math.ceil((iterations - 1) * (0.5 + percent / 2))], |
| ]; |
| } |
| |
| sampleValuesInto(samples) { |
| for (const 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; |
| } |
| 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) { |
| const slice = this.binBoundariesDict_[i]; |
| const 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'); |
| } |
| |
| if (!!this.binBoundariesDict_ === !!other.binBoundariesDict_) { |
| for (let i = 0; i < this.allBins.length; ++i) { |
| let bin = this.allBins[i]; |
| if (bin.count === 0) { |
| bin = new HistogramBin(bin.range); |
| this.allBins[i] = bin; |
| } |
| bin.addBin(other.allBins[i]); |
| } |
| } else { |
| const [multiBin, singleBin] = this.binBoundariesDict_ ? |
| [this, other] : [other, this]; |
| // TODO(benjhayden) This can't propagate sample diagnostics until |
| // sampleValues are merged into bins alongside their diagnostics. |
| for (const value of singleBin.sampleValues) { |
| if (typeof(value) !== 'number' || isNaN(value)) { |
| continue; |
| } |
| const binIndex = multiBin.getBinIndexForValue(value); |
| let bin = multiBin.allBins[binIndex]; |
| if (bin.count === 0) { |
| // Copy HistogramBin on write, share the rest with the other |
| // Histograms that use the same HistogramBinBoundaries. |
| bin = new HistogramBin(bin.range); |
| multiBin.allBins[binIndex] = bin; |
| } |
| bin.addSample(value); |
| } |
| } |
| |
| 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_); |
| } |
| |
| this.sampleMeans_ = []; |
| this.diagnostics.addDiagnostics(other.diagnostics); |
| |
| for (const [stat, option] of other.summaryOptions) { |
| if (stat === 'percentile') { |
| const percentiles = this.summaryOptions.get(stat); |
| for (const percent of option) { |
| if (!percentiles.includes(percent)) percentiles.push(percent); |
| } |
| } else if (stat === 'iprs') { |
| const thisIprs = this.summaryOptions.get(stat); |
| for (const ipr of option) { |
| let found = false; |
| for (const thisIpr of thisIprs) { |
| found = ipr.equals(thisIpr); |
| if (found) break; |
| } |
| if (!found) thisIprs.push(ipr); |
| } |
| } else if (stat === 'ci') { |
| const CIs = this.summaryOptions.get(stat); |
| for (const CI of option) { |
| if (!CIs.includes(CI)) CIs.push(CI); |
| } |
| } 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) |
| * @param {!Array.<!tr.b.Range>=} summaryOptions.iprs Ranges of numbers in |
| * (0,1). |
| * @param {!Array.<number>=} summaryOptions.ci Numbers in (0,1) |
| */ |
| customizeSummaryOptions(summaryOptions) { |
| for (const [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 (typeof(this.average) !== 'number') return undefined; |
| return new tr.b.Scalar(this.unit, this.average); |
| } |
| if (statName === 'std') { |
| if (typeof(this.standardDeviation) !== 'number') return undefined; |
| return new tr.b.Scalar(this.unit, this.standardDeviation); |
| } |
| if (statName === 'geometricMean') { |
| if (typeof(this.geometricMean) !== 'number') return undefined; |
| 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(); |
| } |
| if (typeof(this.running_[statName]) !== 'number') return undefined; |
| 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_') { |
| if (this.numValues === 0) return undefined; |
| const percent = percentFromString(statName.substr(4)); |
| const percentile = this.getApproximatePercentile(percent); |
| if (typeof(percentile) !== 'number') return undefined; |
| return new tr.b.Scalar(this.unit, percentile); |
| } |
| if (statName.substr(0, 3) === 'ci_') { |
| const percent = percentFromString(statName.substr(3, 3)); |
| const [lowCI, highCI] = this.resampleMean_(percent); |
| if (statName.substr(7) === 'lower') { |
| if (typeof(lowCI) !== 'number') return undefined; |
| return new tr.b.Scalar(this.unit, lowCI); |
| } else if (statName.substr(7) === 'upper') { |
| if (typeof(highCI) !== 'number') return undefined; |
| return new tr.b.Scalar(this.unit, highCI); |
| } |
| |
| if (typeof(highCI) !== 'number' || typeof(lowCI) !== 'number') { |
| return undefined; |
| } |
| return new tr.b.Scalar(this.unit, highCI - lowCI); |
| } |
| if (statName.substr(0, 4) === 'ipr_') { |
| let lower = percentFromString(statName.substr(4, 3)); |
| let upper = percentFromString(statName.substr(8)); |
| if (lower >= upper) { |
| throw new Error('Invalid inter-percentile range: ' + statName); |
| } |
| lower = this.getApproximatePercentile(lower); |
| upper = this.getApproximatePercentile(upper); |
| const ipr = upper - lower; |
| if (typeof(ipr) !== 'number') return undefined; |
| return new tr.b.Scalar(this.unit, ipr); |
| } |
| |
| 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); |
| } |
| |
| const 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() { |
| const statisticsNames = new Set(); |
| for (const [statName, option] of this.summaryOptions) { |
| if (statName === 'percentile') { |
| for (const pctile of option) { |
| statisticsNames.add('pct_' + tr.v.percentToString(pctile)); |
| } |
| } else if (statName === 'iprs') { |
| for (const range of option) { |
| statisticsNames.add( |
| 'ipr_' + tr.v.percentToString(range.min, true) + |
| '_' + tr.v.percentToString(range.max, true)); |
| } |
| } else if (statName === 'ci') { |
| for (const CIpctile of option) { |
| const CIpctStr = tr.v.percentToString(CIpctile); |
| statisticsNames.add('ci_' + CIpctStr + '_lower'); |
| statisticsNames.add('ci_' + CIpctStr + '_upper'); |
| statisticsNames.add('ci_' + CIpctStr); |
| } |
| } 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() { |
| const results = new Map(); |
| for (const statName of this.statisticsNames) { |
| const 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|. This is useful |
| * when merging Histograms. |
| * @return {!tr.v.Histogram} |
| */ |
| clone() { |
| const binBoundaries = HistogramBinBoundaries.fromDict( |
| this.binBoundariesDict_); |
| const hist = new Histogram(this.name, this.unit, binBoundaries); |
| for (const [stat, option] of this.summaryOptions) { |
| // Copy arrays, but not ipr Ranges. |
| if (stat === 'percentile' || stat === 'iprs' || stat === 'ci') { |
| hist.summaryOptions.set(stat, Array.from(option)); |
| } else { |
| 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|. |
| * 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 (const [stat, option] of this.summaryOptions) { |
| // Copy the array of percentiles. |
| if (stat === 'percentile' || stat === 'ci') { |
| rebinned.summaryOptions.set(stat, Array.from(option)); |
| } else { |
| rebinned.summaryOptions.set(stat, option); |
| } |
| } |
| return rebinned; |
| } |
| |
| serialize(serializer) { |
| let nanBin = this.numNans; |
| if (this.nanDiagnosticMaps.length) { |
| nanBin = [nanBin, ...this.nanDiagnosticMaps.map(dm => [ |
| undefined, ...dm.serialize(serializer)])]; |
| } |
| this.diagnostics.set(tr.v.d.RESERVED_NAMES.STATISTICS_NAMES, |
| new tr.v.d.GenericSet([...this.statisticsNames].sort())); |
| this.diagnostics.set(tr.v.d.RESERVED_NAMES.DESCRIPTION, |
| new tr.v.d.GenericSet([this.description].sort())); |
| return [ |
| serializer.getOrAllocateId(this.name), |
| this.unit.asJSON2(), |
| serializer.getOrAllocateId(this.binBoundariesDict_), |
| this.diagnostics.serialize(serializer), |
| this.running_ ? this.running_.asDict() : 0, |
| this.serializeBins_(serializer), |
| nanBin, |
| ]; |
| } |
| |
| asDict() { |
| const dict = {}; |
| dict.name = this.name; |
| dict.unit = this.unit.asJSON(); |
| if (this.binBoundariesDict_ !== undefined) { |
| dict.binBoundaries = this.binBoundariesDict_; |
| } |
| 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(); |
| this.running.truncate(this.unit); |
| dict.running = this.running_.asDict(); |
| dict.allBins = this.allBinsAsDict_(); |
| } |
| |
| const summaryOptions = {}; |
| let anyOverriddenSummaryOptions = false; |
| for (const [name, value] of this.summaryOptions) { |
| let option; |
| if (name === 'percentile') { |
| if (value.length === 0) continue; |
| option = Array.from(value); |
| } else if (name === 'iprs') { |
| // Use a more compact JSON format than Range supports. |
| if (value.length === 0) continue; |
| option = value.map(r => [r.min, r.max]); |
| } else if (name === 'ci') { |
| if (value.length === 0) continue; |
| option = Array.from(value); |
| } else if (value === DEFAULT_SUMMARY_OPTIONS.get(name)) { |
| continue; |
| } else { |
| option = value; |
| } |
| summaryOptions[name] = option; |
| anyOverriddenSummaryOptions = true; |
| } |
| if (anyOverriddenSummaryOptions) { |
| dict.summaryOptions = summaryOptions; |
| } |
| |
| return dict; |
| } |
| |
| serializeBins_(serializer) { |
| // 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. |
| |
| const 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 0; |
| } |
| |
| if (emptyBins > (numBins / 2)) { |
| const allBinsDict = {}; |
| for (let i = 0; i < numBins; ++i) { |
| const bin = this.allBins[i]; |
| if (bin.count > 0) { |
| allBinsDict[i] = bin.serialize(serializer); |
| } |
| } |
| return allBinsDict; |
| } |
| |
| const allBinsArray = []; |
| for (let i = 0; i < numBins; ++i) { |
| allBinsArray.push(this.allBins[i].serialize(serializer)); |
| } |
| return allBinsArray; |
| } |
| |
| 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. |
| |
| const 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)) { |
| const allBinsDict = {}; |
| for (let i = 0; i < numBins; ++i) { |
| const bin = this.allBins[i]; |
| if (bin.count > 0) { |
| allBinsDict[i] = bin.asDict(); |
| } |
| } |
| return allBinsDict; |
| } |
| |
| const 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); |
| } |
| } |
| |
| // Some metrics only want to report average. This dictionary is provided to |
| // facilitate disabling all other statistics. |
| Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS = { |
| count: false, |
| max: false, |
| min: false, |
| std: false, |
| sum: false, |
| }; |
| |
| 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) { |
| const builder = new HistogramBinBoundaries(binBoundaries[0]); |
| for (const 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; |
| this.bins_ = 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 results.html with many Histograms with the same bin |
| // boundaries, caching the HistogramBinBoundaries not only speeds up |
| // loading, but also prevents a bug where buildBinRanges_ is occasionally |
| // non-deterministic, which causes similar Histograms to be unmergeable. |
| const cacheKey = JSON.stringify(dict); |
| if (HISTOGRAM_BIN_BOUNDARIES_CACHE.has(cacheKey)) { |
| return HISTOGRAM_BIN_BOUNDARIES_CACHE.get(cacheKey); |
| } |
| |
| const binBoundaries = new HistogramBinBoundaries(dict[0]); |
| for (const 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; |
| } |
| |
| get bins() { |
| if (this.bins_ === undefined) { |
| this.buildBins_(); |
| } |
| return this.bins_; |
| } |
| |
| buildBins_() { |
| this.bins_ = this.binRanges.map(r => new HistogramBin(r)); |
| // It would be nice to Object.freeze() the bins in order to catch bugs |
| // when we forget to copy a bin before writing to it, but that would slow |
| // down buildBins_ by 55%: https://jsperf.com/new-vs-new-freeze/1 |
| } |
| |
| /** |
| * @return {!Array.<!tr.b.math.Range>} |
| */ |
| get binRanges() { |
| if (this.binRanges_ === undefined) { |
| this.buildBinRanges_(); |
| } |
| return this.binRanges_; |
| } |
| |
| buildBinRanges_() { |
| 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 (const slice of this.builder_.slice(1)) { |
| if (!(slice instanceof Array)) { |
| this.binRanges_.push( |
| tr.b.math.Range.fromExplicitRange(prevBoundary, slice)); |
| prevBoundary = slice; |
| continue; |
| } |
| const nextMaxBinBoundary = slice[1]; |
| const binCount = slice[2]; |
| const sliceMinBinBoundary = prevBoundary; |
| |
| switch (slice[0]) { |
| case HistogramBinBoundaries.SLICE_TYPE.LINEAR: |
| { |
| const binWidth = (nextMaxBinBoundary - prevBoundary) / binCount; |
| for (let i = 1; i < binCount; i++) { |
| const boundary = sliceMinBinBoundary + i * binWidth; |
| this.binRanges_.push(tr.b.math.Range.fromExplicitRange( |
| prevBoundary, boundary)); |
| prevBoundary = boundary; |
| } |
| break; |
| } |
| |
| case HistogramBinBoundaries.SLICE_TYPE.EXPONENTIAL: |
| { |
| const binExponentWidth = |
| Math.log(nextMaxBinBoundary / prevBoundary) / binCount; |
| for (let i = 1; i < binCount; i++) { |
| const 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.bins_ = 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.bins_ = 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.bins_ = 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.buildBinRanges_() 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.timeInMsAutoFormat.unitName, |
| new HistogramBinBoundaries(0) |
| .addBinBoundary(1).addExponentialBins(1e3, 3) |
| .addBinBoundary(tr.b.convertUnit( |
| 2, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| 5, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| 10, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| 30, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| tr.b.UnitScale.TIME.MINUTE.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(2 * tr.b.convertUnit( |
| tr.b.UnitScale.TIME.MINUTE.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(5 * tr.b.convertUnit( |
| tr.b.UnitScale.TIME.MINUTE.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(10 * tr.b.convertUnit( |
| tr.b.UnitScale.TIME.MINUTE.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(30 * tr.b.convertUnit( |
| tr.b.UnitScale.TIME.MINUTE.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| tr.b.UnitScale.TIME.HOUR.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(2 * tr.b.convertUnit( |
| tr.b.UnitScale.TIME.HOUR.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(6 * tr.b.convertUnit( |
| tr.b.UnitScale.TIME.HOUR.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(12 * tr.b.convertUnit( |
| tr.b.UnitScale.TIME.HOUR.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| tr.b.UnitScale.TIME.DAY.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| tr.b.UnitScale.TIME.WEEK.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| tr.b.UnitScale.TIME.MONTH.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)) |
| .addBinBoundary(tr.b.convertUnit( |
| tr.b.UnitScale.TIME.YEAR.value, |
| tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))); |
| |
| 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> |