| <!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/statistics.html"> |
| <link rel="import" href="/tracing/base/utils.html"> |
| <link rel="import" href="/tracing/base/xhr.html"> |
| |
| <script> |
| 'use strict'; |
| /* eslint-disable no-console */ |
| |
| const escapeChars = s => s.replace(/[\:|=\/#&,]/g, '_'); |
| |
| function findUnescapedKey(escaped, d) { |
| if (!d) { |
| return undefined; |
| } |
| |
| for (const k of Object.keys(d)) { |
| if (escapeChars(k) === escapeChars(escaped)) { |
| return k; |
| } |
| } |
| } |
| |
| function geoMeanFromHistogram(h) { |
| if (!h.hasOwnProperty('buckets')) return 0.0; |
| let count = 0; |
| let sumOfLogs = 0; |
| for (const 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 guessFullMetricName(metricName) { |
| const parts = metricName.split('/'); |
| if (parts.length === 2) { |
| return metricName + '/summary'; |
| } |
| return undefined; |
| } |
| |
| function splitMetric(metricName) { |
| const parts = metricName.split('/'); |
| let interactionName; |
| let traceName = 'summary'; |
| let 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) { |
| const allValues = []; |
| const chartAndTrace = splitMetric(metricName); |
| for (const charts of listOfCharts) { |
| const chartName = findUnescapedKey(chartAndTrace[0], charts.charts); |
| if (chartName) { |
| const traceName = findUnescapedKey( |
| chartAndTrace[1], charts.charts[chartName]); |
| if (traceName) { |
| if (charts.charts[chartName][traceName].type === |
| 'list_of_scalar_values') { |
| if (charts.charts[chartName][traceName].values === null) continue; |
| allValues.push(tr.b.math.Statistics.mean( |
| charts.charts[chartName][traceName].values)); |
| } |
| if (charts.charts[chartName][traceName].type === 'histogram') { |
| allValues.push( |
| geoMeanFromHistogram(charts.charts[chartName][traceName])); |
| } |
| if (charts.charts[chartName][traceName].type === 'scalar') { |
| allValues.push(charts.charts[chartName][traceName].value); |
| } |
| } |
| } |
| } |
| return allValues; |
| } |
| |
| function valuesFromChartsWithFallback(listOfCharts, metricName) { |
| const allValues = valuesFromCharts(listOfCharts, metricName); |
| if (allValues.length > 0) return allValues; |
| |
| // If this had a grouping_label, the "summary" part may have been stripped by |
| // the dashboard during upload. We can re-add it here. |
| const fullMetricName = guessFullMetricName(metricName); |
| if (!fullMetricName) return []; |
| |
| return valuesFromCharts(listOfCharts, fullMetricName); |
| } |
| |
| function parseFiles(files) { |
| const results = []; |
| for (const path of files) { |
| const current = tr.b.getSync('file://' + path); |
| results.push(JSON.parse(current)); |
| } |
| return results; |
| } |
| |
| const escapeForRegExp = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); |
| |
| const strFromRE = re => re.toString().split('/')[1]; |
| |
| function valuesFromBuildbotOutput(out, metric) { |
| if (!out) return []; |
| |
| let stringVals = []; |
| const floatVals = []; |
| const chartAndTrace = splitMetric(metric); |
| const metricRE = escapeForRegExp( |
| 'RESULT ' + chartAndTrace[0] + ': ' + chartAndTrace[1] + '='); |
| const singleResultRE = new RegExp(metricRE + |
| strFromRE(/\s*([-]?[\d\.]+)/), 'g'); |
| const multiResultsRE = new RegExp(metricRE + |
| strFromRE(/\s*\[\s*([\d\., -]+)\s*\]/), 'g'); |
| const meanStdDevRE = new RegExp(metricRE + |
| strFromRE(/\s*\{\s*([-]?\d*(?:\.\d*)?),\s*([-]?\d*(?:\.\d*)?)\}/), 'g'); |
| for (const line of out.split(/\r?\n/)) { |
| const singleResultMatch = singleResultRE.exec(line); |
| const multiResultsMatch = multiResultsRE.exec(line); |
| const meanStdDevMatch = meanStdDevRE.exec(line); |
| if (singleResultMatch && singleResultMatch.length > 1) { |
| stringVals.push(singleResultMatch[1]); |
| } else if (multiResultsMatch && multiResultsMatch.length > 1) { |
| const values = multiResultsMatch[1].split(','); |
| stringVals = stringVals.concat(values); |
| } else if (meanStdDevMatch && meanStdDevMatch.length > 1) { |
| stringVals.push(meanStdDevMatch[1]); |
| } |
| } |
| for (const val of stringVals) { |
| const f = parseFloat(val); |
| if (!isNaN(f)) floatVals.push(f); |
| } |
| return floatVals; |
| } |
| |
| function parseMultipleBuildbotStreams(files, metric) { |
| let allValues = []; |
| for (const path of files) { |
| let contents; |
| try { |
| contents = tr.b.getSync('file://' + path); |
| } catch (ex) { |
| const err = new Error('Could not open' + path); |
| err.name = 'File loading error'; |
| throw err; |
| } |
| allValues = allValues.concat(valuesFromBuildbotOutput(contents, metric)); |
| } |
| return allValues; |
| } |
| |
| const buildComparisonResultOutput = function(a, b) { |
| let comparisonResult; |
| if (!a.length || !b.length) { |
| comparisonResult = { |
| significance: tr.b.math.Statistics.Significance.NEED_MORE_DATA |
| }; |
| } else { |
| comparisonResult = tr.b.math.Statistics.mwu( |
| a, b, tr.b.math.Statistics.DEFAULT_ALPHA, |
| tr.b.math.Statistics.MAX_SUGGESTED_SAMPLE_SIZE).asDict(); |
| } |
| return { |
| sampleA: a, |
| sampleB: b, |
| result: comparisonResult |
| }; |
| }; |
| |
| const SampleComparison = { |
| |
| compareBuildbotOutputs( |
| buildbotOutputAPathList, buildbotOutputBPathList, metric) { |
| const aPaths = buildbotOutputAPathList.split(','); |
| const bPaths = buildbotOutputBPathList.split(','); |
| const sampleA = parseMultipleBuildbotStreams(aPaths, metric); |
| const sampleB = parseMultipleBuildbotStreams(bPaths, metric); |
| return buildComparisonResultOutput(sampleA, sampleB); |
| }, |
| |
| compareCharts(chartPathListA, chartPathListB, metric) { |
| const aPaths = chartPathListA.split(','); |
| const bPaths = chartPathListB.split(','); |
| const chartsA = parseFiles(aPaths); |
| const chartsB = parseFiles(bPaths); |
| const sampleA = valuesFromChartsWithFallback(chartsA, metric); |
| const sampleB = valuesFromChartsWithFallback(chartsB, metric); |
| return buildComparisonResultOutput(sampleA, sampleB); |
| } |
| |
| }; |
| |
| if (tr.isHeadless) { |
| const [method, ...rest] = sys.argv.slice(1); |
| if (SampleComparison[method]) { |
| console.log(JSON.stringify(SampleComparison[method](...rest))); |
| } |
| } |
| </script> |