Chromium Code Reviews| Index: tracing/tracing/metrics/compare_samples.html |
| diff --git a/tracing/tracing/metrics/compare_samples.html b/tracing/tracing/metrics/compare_samples.html |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..0d5f50a90fd20d277d2686f95bfc78d85a56fd97 |
| --- /dev/null |
| +++ b/tracing/tracing/metrics/compare_samples.html |
| @@ -0,0 +1,275 @@ |
| +<!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/statistics.html"> |
| +<link rel="import" href="/tracing/base/xhr.html"> |
| +<link rel="import" href="/tracing/value/value_set.html"> |
| + |
| +<script> |
| +'use strict'; |
| + |
| +tr.exportTo('tr.metrics', function() { |
|
nednguyen
2016/09/12 17:13:32
You also don't need this export.
RobertoCN
2016/09/19 20:07:43
Done.
nednguyen
2016/09/19 20:14:41
Did you forget to upload your new patch?
|
| + var escapeChars = s => s.replace(/[\:|=\/#&,]/g, '_'); |
| + |
| + function findUnescapedKey(escaped, d) { |
| + for (var k of tr.b.dictionaryKeys(d)) |
| + if (escapeChars(k) === escaped) |
| + return k; |
| + throw new Error('did not find key ' + escaped + ' in ' + |
| + tr.b.dictionaryKeys(d)); |
| + } |
| + |
| + function geoMeanFromHistogram(h) { |
| + if (!h.hasOwnProperty('buckets')) |
| + return 0.0; |
| + var count = 0; |
| + var sumOfLogs = 0; |
| + for (var bucket of h.buckets) { |
| + if (bucket.hasOwnProperty('high')) |
| + bucket.mean = (bucket.low + bucket.high) / 2.0; |
| + else |
| + bucket.mean = bucket.low; |
| + |
| + if (bucket.mean > 0) { |
| + sumOfLogs += Math.log(bucket.mean) * bucket.count; |
| + count += bucket.count; |
| + } |
| + } |
| + if (count === 0) |
| + return 0.0; |
| + return Math.exp(sumOfLogs / count); |
| + } |
| + |
| + function splitMetric(metricName) { |
| + var parts = metricName.split('/'); |
| + var interactionName; |
| + var traceName = 'summary'; |
| + var chartName = parts[0]; |
| + if (parts.length === 3) { |
| + // parts[1] is the interactionName |
| + if (parts[1]) |
| + chartName = parts[1] + '@@' + chartName; |
| + traceName = parts[2]; |
| + } else if (parts.length === 2) { |
| + if (chartName !== parts[1]) |
| + traceName = parts[1]; |
| + } else |
| + throw new Error('Could not parse metric name.'); |
| + return [chartName, traceName]; |
| + } |
| + |
| + function valuesFromCharts(listOfCharts, metricName) { |
| + var all_values = []; |
| + var chartAndTrace = splitMetric(metricName); |
| + for (var charts of listOfCharts) { |
| + var chartName = findUnescapedKey(chartAndTrace[0], charts.charts); |
| + if (chartName) { |
| + var traceName = findUnescapedKey( |
| + chartAndTrace[1], charts.charts[chartName]); |
| + if (traceName) { |
| + if (charts.charts[chartName][traceName].type === |
| + 'list_of_scalar_values') |
| + all_values.push(...charts.charts[chartName][traceName].values); |
| + if (charts.charts[chartName][traceName].type === 'histogram') |
| + all_values.push( |
| + geoMeanFromHistogram(charts.charts[chartName][traceName])); |
| + } |
| + } |
| + } |
| + return all_values; |
| + } |
| + |
| + function rawValuesByMetricName(valueSet, metricName) { |
| + var interactionRecord, valueName, story; |
| + var metricNameParts = metricName.split('/'); |
| + if (metricNameParts[0] === metricNameParts[1]) |
| + story = 'summary'; |
| + else |
| + story = metricNameParts[1]; |
| + var chartNameParts = metricNameParts[0].split('-'); |
| + valueName = chartNameParts[1]; |
| + if (chartNameParts.length === 2) |
| + interactionRecord = chartNameParts[0]; |
| + var values = valueSet.getValuesWithName(valueName); |
| + if (!values || values.length === 0) { |
| + // If there was a dash in the chart name, but it wasn't an |
| + // interaction record. |
| + valueName = metricNameParts[0]; |
| + values = valueSet.getValuesWithName(valueName); |
| + interactionRecord = undefined; |
| + if (!values || values.length === 0) |
| + throw new Error('No values with name ' + valueName); |
| + } |
| + var filtered = []; |
| + for (var value of values) { |
| + if (value.name !== valueName) |
| + continue; |
| + var ii = tr.v.d.IterationInfo.getFromValue(value); |
| + if (interactionRecord) { |
| + var IRParts = []; |
| + var keys = Object.keys(ii.storyGroupingKeys); |
| + keys.sort(); |
| + for (var key of keys) |
| + IRParts.push(ii.storyGroupingKeys[key]); |
| + if (interactionRecord === IRParts.join('_') && |
| + escapeChars(ii.storyDisplayName) === |
| + escapeChars(story)) |
| + filtered.push(value); |
| + } else if (escapeChars(ii.storyDisplayName) === |
| + escapeChars(story)) |
| + filtered.push(value); |
| + } |
| + |
| + var rawValues = []; |
| + for (var val of filtered) { |
| + if (val.numeric instanceof tr.v.Numeric) |
| + rawValues = rawValues.concat(val.numeric.sampleValues); |
| + else if (val.numeric instanceof tr.v.ScalarNumeric) |
| + rawValues.push(val.numeric.value); |
| + } |
| + return rawValues; |
| + } |
| + |
| + function parseFiles(files) { |
| + var results = []; |
| + for (var path of files) { |
| + try { |
| + var current = tr.b.getSync('file://' + path); |
| + } catch (ex) { |
| + var err = new Error('Could not open' + path); |
| + err.name = 'File loading error'; |
| + throw err; |
| + } |
| + results.push(JSON.parse(current)); |
| + } |
| + return results; |
| + } |
| + |
| + var escapeForRegExp = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); |
| + |
| + var strFromRE = re => re.toString().split('/')[1]; |
| + |
| + function valuesFromBuildbotOutput(out, metric) { |
| + if (!out) |
| + return []; |
| + var stringVals = []; |
| + var floatVals = []; |
| + var chartAndTrace = splitMetric(metric); |
| + var metricRE = escapeForRegExp( |
| + 'RESULT ' + chartAndTrace[0] + ': ' + chartAndTrace[1] + '='); |
| + var singleResultRE = new RegExp(metricRE + |
| + strFromRE(/\s*([-]?[\d\.]+)/), 'g'); |
| + var multiResultsRE = new RegExp(metricRE + |
| + strFromRE(/\s*\[\s*([\d\., -]+)\s*\]/), 'g'); |
| + var meanStdDevRE = new RegExp(metricRE + |
| + strFromRE(/\s*\{\s*([-]?\d*(?:\.\d*)?),\s*([-]?\d*(?:\.\d*)?)\}/), 'g'); |
| + for (var line of out.split(/\r?\n/)) { |
| + var singleResultMatch = singleResultRE.exec(line); |
| + var multiResultsMatch = multiResultsRE.exec(line); |
| + var meanStdDevMatch = meanStdDevRE.exec(line); |
| + if (singleResultMatch && singleResultMatch.length > 1) |
| + stringVals.push(singleResultMatch[1]); |
| + else if (multiResultsMatch && multiResultsMatch.length > 1) { |
| + var values = multiResultsMatch[1].split(','); |
| + stringVals = stringVals.concat(values); |
| + } else if (meanStdDevMatch && meanStdDevMatch.length > 1) |
| + stringVals.push(meanStdDevMatch[1]); |
| + } |
| + for (var val of stringVals) { |
| + var f = parseFloat(val); |
| + if (!isNaN(f)) |
| + floatVals.push(f); |
| + } |
| + return floatVals; |
| + } |
| + |
| + function parseMultipleBuildbotStreams(files, metric) { |
| + var allValues = []; |
| + for (var path of files) { |
| + try { |
| + var contents = tr.b.getSync('file://' + path); |
| + } |
| + catch (ex) { |
| + var err = new Error('Could not open' + path); |
| + err.name = 'File loading error'; |
| + throw err; |
| + } |
| + allValues = allValues.concat(valuesFromBuildbotOutput(contents, metric)); |
| + } |
| + return allValues; |
| + } |
| + |
| + var BisectComparison = { |
|
dtu
2016/09/15 21:31:56
The word "bisect" isn't used anywhere else. Probab
RobertoCN
2016/09/19 20:07:42
Done.
|
| + ENOUGH_SAMPLES: 18, |
| + SIGNIFICANCE_LEVEL: 0.05, |
|
dtu
2016/09/15 21:31:56
Seems like bisect is currently using 0.01, so I gu
RobertoCN
2016/09/19 20:07:43
Done.
|
| + |
| + compareBuildbotOutputs: function( |
|
dtu
2016/09/15 21:31:56
I like these composed functions. Very easy to read
RobertoCN
2016/09/19 20:07:43
Acknowledged.
|
| + buildbotOutputAPathList, buildbotOutputBPathList, metric) { |
| + var aPaths = buildbotOutputAPathList.split(','); |
| + var bPaths = buildbotOutputBPathList.split(','); |
| + var sampleA = parseMultipleBuildbotStreams(aPaths, metric); |
| + var sampleB = parseMultipleBuildbotStreams(bPaths, metric); |
| + return this.compareSamples(sampleA, sampleB); |
| + }, |
| + |
| + compareValuesets: function(valueSetAPathList, valueSetBPathList, metric) { |
| + var aPaths = valueSetAPathList.split(','); |
| + var bPaths = valueSetBPathList.split(','); |
| + var valueSetA = new tr.v.ValueSet(); |
| + var valueSetB = new tr.v.ValueSet(); |
| + var dictsA = parseFiles(aPaths); |
| + var dictsB = parseFiles(bPaths); |
| + for (var d of dictsA) |
| + valueSetA.addValuesFromDicts(d); |
| + for (var d of dictsB) |
| + valueSetB.addValuesFromDicts(d); |
| + |
| + var sampleA = rawValuesByMetricName(valueSetA, metric); |
| + var sampleB = rawValuesByMetricName(valueSetB, metric); |
| + return this.compareSamples(sampleA, sampleB); |
| + }, |
| + |
| + compareCharts: function(chartPathListA, chartPathListB, metric) { |
| + var aPaths = chartPathListA.split(','); |
| + var bPaths = chartPathListB.split(','); |
| + var chartsA = parseFiles(aPaths); |
| + var chartsB = parseFiles(bPaths); |
| + var sampleA = valuesFromCharts(chartsA, metric); |
| + var sampleB = valuesFromCharts(chartsB, metric); |
| + return this.compareSamples(sampleA, sampleB); |
| + }, |
| + |
| + compareSamples: function(sampleA, sampleB) { |
| + var pValue = tr.b.Statistics.mwu.test(sampleA, sampleB); |
| + // Diagnostics |
| + var summaryStats = sample => ({ |
| + std_dev: tr.b.Statistics.stddev(sample), |
| + mean: tr.b.Statistics.mean(sample), |
|
dtu
2016/09/15 21:35:41
Question for Ben and Ethan: are you going to be us
benjhayden
2016/09/21 06:33:25
No, I don't think that results2.html will be using
|
| + debug_values: sample |
| + }); |
| + var result = { |
| + sample_a: summaryStats(sampleA), |
| + sample_b: summaryStats(sampleB), |
| + pValue: pValue.p, |
| + UStatistic: pValue.U, |
|
dtu
2016/09/15 21:31:56
Why is the U-statistic useful?
RobertoCN
2016/09/19 20:07:42
Maybe it's not. I decided to surface it because it
|
| + result: 'needMoreData', |
| + }; |
| + if (pValue.p < this.SIGNIFICANCE_LEVEL) |
| + result.result = true; // Reject the null |
|
dtu
2016/09/15 21:31:56
Don't mix types. Make them all string constants, I
RobertoCN
2016/09/19 20:07:43
Done.
|
| + else if (sampleA.length > this.ENOUGH_SAMPLES && |
| + sampleB.length > this.ENOUGH_SAMPLES) |
| + result.result = false; // Fail to reject the null. |
| + return result; |
| + } |
| + }; |
| + |
| + return { |
| + BisectComparison: BisectComparison |
| + }; |
| +}); |
| +</script> |