diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/value')
125 files changed, 22468 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/value/__init__.py b/chromium/third_party/catapult/tracing/tracing/value/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/__init__.py diff --git a/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html new file mode 100644 index 00000000000..fb30056f60b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html @@ -0,0 +1,154 @@ +<!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/value/histogram.html"> +<link rel="import" href="/tracing/value/legacy_unit_info.html"> + +<script> +'use strict'; +tr.exportTo('tr.v', function() { + class ChartJsonConverter { + /** + * Parses Values from |charts|, converts them to Histograms, and adds those + * to |histograms|. + * + * @param {!Array.<!Object>} charts + * @param {!tr.v.HistogramSet} histograms + */ + static convertChartJson(charts, histograms) { + const traceValues = charts.charts.trace; + + // The chromeperf dashboard requires some Diagnostics to be shared, even + // if there is only a single Histogram in the HistogramSet. + const diagnosticCaches = new Map(); + function addSharedDiagnostic(hist, name, diagnostic) { + if (!diagnosticCaches.has(name)) { + diagnosticCaches.set(name, new Map()); + } + const cache = diagnosticCaches.get(name); + const cacheKey = diagnostic instanceof tr.v.d.GenericSet ? + [...diagnostic].sort().join(',') : diagnostic.minDate; + if (!cache.has(cacheKey)) { + cache.set(cacheKey, diagnostic); + histograms.addSharedDiagnostic(diagnostic); + } + hist.diagnostics.set(name, cache.get(cacheKey)); + } + + for (const [name, pageValues] of Object.entries(charts.charts)) { + if (name === 'trace') continue; + + const pageValuesCount = Object.keys(pageValues).length; + for (const [storyName, value] of Object.entries(pageValues)) { + if (pageValuesCount > 1 && storyName === 'summary') continue; + + const unitInfo = tr.v.LEGACY_UNIT_INFO.get(value.units) || {}; + const unitName = unitInfo.name || 'unitlessNumber'; + const conversionFactor = unitInfo.conversionFactor || 1; + + let improvementDirection = tr.b.ImprovementDirection.DONT_CARE; + if (unitInfo.defaultImprovementDirection !== undefined) { + improvementDirection = unitInfo.defaultImprovementDirection; + } + // Metrics have the final say. + if (value.improvement_direction !== undefined) { + improvementDirection = + ChartJsonConverter.convertImprovementDirection( + value.improvement_direction); + } + const unitNameSuffix = tr.b.Unit.nameSuffixForImprovementDirection( + improvementDirection); + + const hist = histograms.createHistogram( + value.name || name, + tr.b.Unit.byName[unitName + unitNameSuffix], [], { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + description: value.description || '', + }); + + if (traceValues) { + const traceValue = traceValues[storyName] || {}; + let traceUrl = traceValue.cloud_url; + if (!traceUrl && traceValue.file_path) { + traceUrl = 'file://' + traceValue.file_path; + } + if (traceUrl) { + addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.TRACE_URLS, + new tr.v.d.GenericSet([traceUrl])); + } + } + + if (pageValuesCount > 1) { + addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet([storyName])); + } + + const storyTags = []; + if (value.tir_label) { + storyTags.push(`tir_label:${value.tir_label}`); + } + if (value.story_tags) { + storyTags.push(...value.story_tags); + } + if (storyTags.length) { + addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.STORY_TAGS, + new tr.v.d.GenericSet(storyTags)); + } + + if (charts.benchmark_name) { + addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.BENCHMARKS, + new tr.v.d.GenericSet([charts.benchmark_name])); + } + + if (charts.label) { + addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet([charts.label])); + } + + if (charts.benchmarkStartMs) { + addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(charts.benchmarkStartMs)); + } + + if (value.type === 'histogram' || value.buckets) { + for (const bucket of value.buckets) { + // Take the center of the bin. This coarse granularity can amplify + // noise when a measurement moves from one bin to the next. + const sample = conversionFactor * (bucket.high + bucket.low) / 2; + for (let i = 0; i < bucket.count; ++i) { + hist.addSample(sample); + } + } + } else if (value.type === 'list_of_scalar_values' || value.values) { + // |value.values| is undefined if the list_of_scalar_values is + // empty. + if (value.values) { + for (const sample of value.values) { + hist.addSample(sample * conversionFactor); + } + } + } else if (value.type === 'scalar' || value.value !== undefined) { + hist.addSample(value.value * conversionFactor); + } + } + } + } + + static convertImprovementDirection(improvementDirection) { + switch (improvementDirection) { + case 'down': return tr.b.ImprovementDirection.SMALLER_IS_BETTER; + case 'up': return tr.b.ImprovementDirection.BIGGER_IS_BETTER; + default: return tr.b.ImprovementDirection.DONT_CARE; + } + } + } + + return { + ChartJsonConverter, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html new file mode 100644 index 00000000000..6b0bb9e49ec --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html @@ -0,0 +1,401 @@ +<!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/value/chart_json_converter.html"> +<link rel="import" href="/tracing/value/histogram_grouping.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('emptyListOfScalarValues', function() { + const charts = { + benchmark_name: 'the.benchmark', + label: 'the_label', + charts: { + mean_frame_time: { + 'http://games.yahoo.com': { + std: 0.0, + name: 'mean_frame_time', + type: 'list_of_scalar_values', + improvement_direction: 'down', + units: 'ms', + page_id: 16, + description: 'Arithmetic mean of frame times.' + }, + 'summary': { + std: 0.0, + name: 'mean_frame_time', + improvement_direction: 'down', + units: 'ms', + type: 'list_of_scalar_values', + description: 'Arithmetic mean of frame times.' + }, + } + } + }; + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + assert.lengthOf(histograms, 1); + const hist = [...histograms][0]; + assert.strictEqual('mean_frame_time', hist.name); + assert.strictEqual('http://games.yahoo.com', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(hist)); + assert.strictEqual('the.benchmark', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BENCHMARKS).callback(hist)); + assert.strictEqual('the_label', + tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist)); + assert.strictEqual(0, hist.numValues); + assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + hist.unit); + + const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES); + assert.isTrue(stories.hasGuid); + assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid)); + + const benchmarks = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS); + assert.isTrue(benchmarks.hasGuid); + assert.strictEqual(benchmarks, histograms.lookupDiagnostic( + benchmarks.guid)); + + const labels = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.LABELS); + assert.isTrue(labels.hasGuid); + assert.strictEqual(labels, histograms.lookupDiagnostic(labels.guid)); + }); + + test('convertWithoutName', function() { + const charts = { + benchmark_name: 'the.benchmark', + label: 'the_label', + charts: { + mean_frame_time: { + 'http://games.yahoo.com': { + std: 0.0, + type: 'list_of_scalar_values', + improvement_direction: 'down', + units: 'ms', + page_id: 16, + description: 'Arithmetic mean of frame times.' + }, + 'summary': { + std: 0.0, + improvement_direction: 'down', + units: 'ms', + type: 'list_of_scalar_values', + description: 'Arithmetic mean of frame times.' + }, + } + } + }; + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + assert.lengthOf(histograms, 1); + const hist = [...histograms][0]; + assert.strictEqual('mean_frame_time', hist.name); + assert.strictEqual('http://games.yahoo.com', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(hist)); + assert.strictEqual('the.benchmark', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BENCHMARKS).callback(hist)); + assert.strictEqual('the_label', + tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist)); + assert.strictEqual(0, hist.numValues); + assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + hist.unit); + + const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES); + assert.isTrue(stories.hasGuid); + assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid)); + + const benchmarks = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS); + assert.isTrue(benchmarks.hasGuid); + assert.strictEqual(benchmarks, histograms.lookupDiagnostic( + benchmarks.guid)); + + const labels = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.LABELS); + assert.isTrue(labels.hasGuid); + assert.strictEqual(labels, histograms.lookupDiagnostic(labels.guid)); + }); + test('convertWithoutTIRLabel', function() { + const charts = { + charts: { + mean_frame_time: { + 'http://games.yahoo.com': { + std: 0.0, + name: 'mean_frame_time', + type: 'list_of_scalar_values', + improvement_direction: 'down', + values: [42], + units: 'ms', + page_id: 16, + description: 'Arithmetic mean of frame times.' + }, + 'summary': { + std: 0.0, + name: 'mean_frame_time', + improvement_direction: 'down', + values: [ + 16.693, + 16.646, + 16.918, + 16.681 + ], + units: 'ms', + type: 'list_of_scalar_values', + description: 'Arithmetic mean of frame times.' + }, + } + } + }; + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + assert.lengthOf(histograms, 1); + const hist = [...histograms][0]; + assert.strictEqual('mean_frame_time', hist.name); + assert.strictEqual('http://games.yahoo.com', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(hist)); + assert.strictEqual(42, hist.average); + assert.strictEqual(1, hist.numValues); + assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + hist.unit); + + const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES); + assert.isTrue(stories.hasGuid); + assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid)); + }); + + test('convertWithoutType', function() { + const charts = { + charts: { + mean_frame_time: { + 'http://games.yahoo.com': { + std: 0.0, + name: 'mean_frame_time', + improvement_direction: 'down', + value: 42, + units: 'ms', + page_id: 16, + description: 'Arithmetic mean of frame times.' + }, + 'summary': { + std: 0.0, + name: 'mean_frame_time', + improvement_direction: 'down', + values: [ + 16.693, + 16.646, + 16.918, + 16.681 + ], + units: 'ms', + type: 'list_of_scalar_values', + description: 'Arithmetic mean of frame times.' + }, + } + } + }; + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + assert.lengthOf(histograms, 1); + const hist = [...histograms][0]; + assert.strictEqual('mean_frame_time', hist.name); + assert.strictEqual('http://games.yahoo.com', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(hist)); + assert.strictEqual(42, hist.average); + assert.strictEqual(1, hist.numValues); + assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + hist.unit); + + const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES); + assert.isTrue(stories.hasGuid); + assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid)); + }); + + test('convertWithTIRLabel', function() { + const charts = { + charts: { + 'TIR-A@@value-name': { + 'story-name': { + name: 'value-name', + page_id: 7, + improvement_direction: 'down', + values: [42], + units: 'ms', + tir_label: 'TIR-A', + type: 'list_of_scalar_values', + }, + 'summary': { + name: 'value-name', + improvement_direction: 'down', + values: [42], + units: 'ms', + tir_label: 'TIR-A', + type: 'list_of_scalar_values', + }, + }, + }, + }; + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + const hist = tr.b.getOnlyElement(histograms); + assert.strictEqual('value-name', hist.name); + assert.strictEqual(tr.b.getOnlyElement(hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.STORY_TAGS)), 'tir_label:TIR-A'); + assert.strictEqual('story-name', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(hist)); + assert.strictEqual(42, hist.average); + assert.strictEqual(1, hist.numValues); + assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + hist.unit); + assert.isTrue(hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES).hasGuid); + assert.isTrue(hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.STORY_TAGS).hasGuid); + }); + + test('convertWithStoryTags', function() { + const charts = { + charts: { + 'TIR-A@@value-name': { + 'story-name': { + name: 'value-name', + page_id: 7, + improvement_direction: 'down', + values: [42], + units: 'ms', + story_tags: ['foo', 'bar'], + type: 'list_of_scalar_values', + }, + 'summary': { + name: 'value-name', + improvement_direction: 'down', + values: [42], + units: 'ms', + story_tags: ['foo', 'bar'], + type: 'list_of_scalar_values', + }, + }, + }, + }; + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + const hist = tr.b.getOnlyElement(histograms); + assert.strictEqual('value-name', hist.name); + const tags = [...hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.STORY_TAGS)]; + assert.lengthOf(tags, 2); + assert.strictEqual(tags[0], 'foo'); + assert.strictEqual(tags[1], 'bar'); + assert.strictEqual('story-name', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(hist)); + assert.strictEqual(42, hist.average); + assert.strictEqual(1, hist.numValues); + assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + hist.unit); + assert.isTrue(hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES).hasGuid); + assert.isTrue(hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.STORY_TAGS).hasGuid); + }); + + test('convertHistogram', function() { + const charts = { + charts: { + MPArch_RWH_TabSwitchPaintDuration: { + summary: { + units: 'ms', + buckets: [ + { + high: 20, + count: 2, + low: 16, + }, + { + high: 24, + count: 2, + low: 20, + } + ], + important: false, + type: 'histogram', + name: 'MPArch_RWH_TabSwitchPaintDuration', + } + } + } + }; + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + assert.lengthOf(histograms, 1); + const hist = [...histograms][0]; + assert.strictEqual('MPArch_RWH_TabSwitchPaintDuration', hist.name); + assert.strictEqual('', + tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(hist)); + assert.strictEqual(20, hist.average); + assert.strictEqual(4, hist.numValues); + assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + hist.unit); + }); + + test('traceUrls', function() { + const charts = { + charts: { + measurementA: { + storyA: { + units: 'ms', + type: 'list_of_scalar_values', + values: [100], + name: 'measurementA', + }, + storyB: { + units: 'ms', + type: 'list_of_scalar_values', + values: [200], + name: 'measurementA', + }, + }, + trace: { + storyA: { + name: 'trace', + type: 'trace', + file_path: '/home/user/storyA_1900-01-01_00-00-00.html', + }, + storyB: { + name: 'trace', + type: 'trace', + cloud_url: 'https://console.developers.google.com/m/cloudstorage/chromium-telemetry/o/storyB_1900-01-01_00-00-00.html', + }, + }, + }, + }; + let histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + histograms = [...histograms]; + assert.lengthOf(histograms, 2); + assert.strictEqual(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(histograms[0]), 'storyA'); + assert.strictEqual(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES).callback(histograms[1]), 'storyB'); + assert.strictEqual(tr.b.getOnlyElement(histograms[0].diagnostics.get( + tr.v.d.RESERVED_NAMES.TRACE_URLS)), + 'file:///home/user/storyA_1900-01-01_00-00-00.html'); + assert.strictEqual(tr.b.getOnlyElement(histograms[1].diagnostics.get( + tr.v.d.RESERVED_NAMES.TRACE_URLS)), + 'https://console.developers.google.com/m/cloudstorage/chromium-telemetry/o/storyB_1900-01-01_00-00-00.html'); + assert.isTrue(histograms[0].diagnostics.get( + tr.v.d.RESERVED_NAMES.TRACE_URLS).hasGuid); + assert.isTrue(histograms[1].diagnostics.get( + tr.v.d.RESERVED_NAMES.TRACE_URLS).hasGuid); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py new file mode 100755 index 00000000000..10dabee5ccf --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py @@ -0,0 +1,24 @@ +# 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. + +import os +import tracing_project +import vinn + + +def ConvertChartJson(chart_json): + """Convert chart_json to Histograms. + + Args: + chart_json: path to a file containing chart-json + + Returns: + a Vinn result object whose 'returncode' indicates whether there was an + exception, and whose 'stdout' contains HistogramSet json. + """ + return vinn.RunFile( + os.path.join(os.path.dirname(__file__), + 'convert_chart_json_cmdline.html'), + source_paths=tracing_project.TracingProject().source_paths, + js_args=[os.path.abspath(chart_json)]) diff --git a/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html new file mode 100644 index 00000000000..3503b1db685 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html @@ -0,0 +1,22 @@ +<!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/xhr.html"> +<link rel="import" href="/tracing/value/chart_json_converter.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +/* eslint-disable no-console */ + +if (tr.isHeadless) { + const charts = JSON.parse(tr.b.getSync('file://' + sys.argv[1])); + const histograms = new tr.v.HistogramSet(); + tr.v.ChartJsonConverter.convertChartJson(charts, histograms); + console.log(JSON.stringify(histograms.asDicts())); +} +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/csv_builder.html b/chromium/third_party/catapult/tracing/tracing/value/csv_builder.html new file mode 100644 index 00000000000..1627c592669 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/csv_builder.html @@ -0,0 +1,111 @@ +<!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/value/histogram_grouping.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +tr.exportTo('tr.v', function() { + const IGNORE_GROUPING_KEYS = [ + 'name', + 'storyTags', + 'testPath', + ]; + + class CSVBuilder { + /** + * @param {!tr.v.HistogramSet} histograms + */ + constructor(histograms) { + this.histograms_ = histograms; + this.table_ = []; + this.statisticsNames_ = new Set(); + this.groupings_ = []; + } + + build() { + this.prepare_(); + this.buildHeader_(); + this.buildTable_(); + } + + prepare_() { + for (const [key, grouping] of tr.v.HistogramGrouping.BY_KEY) { + if (IGNORE_GROUPING_KEYS.includes(key)) continue; + this.groupings_.push(grouping); + } + this.groupings_.push(new tr.v.GenericSetGrouping( + tr.v.d.RESERVED_NAMES.TRACE_URLS)); + + this.groupings_.sort((a, b) => a.key.localeCompare(b.key)); + + for (const hist of this.histograms_) { + for (const name of hist.statisticsNames) { + this.statisticsNames_.add(name); + } + } + this.statisticsNames_ = Array.from(this.statisticsNames_); + this.statisticsNames_.sort(); + } + + buildHeader_() { + const header = ['name', 'unit']; + for (const name of this.statisticsNames_) { + header.push(name); + } + for (const grouping of this.groupings_) { + header.push(grouping.key); + } + this.table_.push(header); + } + + buildTable_() { + for (const hist of this.histograms_) { + const row = [hist.name, hist.unit.unitString]; + this.table_.push(row); + + for (const name of this.statisticsNames_) { + const stat = hist.getStatisticScalar(name); + if (stat) { + row.push(stat.value); + } else { + row.push(''); + } + } + + for (const grouping of this.groupings_) { + row.push(grouping.callback(hist)); + } + } + } + + toString() { + let str = ''; + for (const row of this.table_) { + for (let i = 0; i < row.length; ++i) { + if (i > 0) { + str += ','; + } + let cell = '' + row[i]; + cell = cell.replace(/\n/g, ' '); + if (cell.indexOf(',') >= 0 || cell.indexOf('"') >= 0) { + cell = '"' + cell.replace(/"/g, '""') + '"'; + } + str += cell; + } + str += '\n'; + } + return str; + } + } + + return { + CSVBuilder, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html b/chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html new file mode 100644 index 00000000000..b34faf35c7d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html @@ -0,0 +1,75 @@ +<!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/value/csv_builder.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('csvBuilder', function() { + const hist0 = new tr.v.Histogram('hist0', tr.b.Unit.byName.sizeInBytes, + tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 10)); + hist0.customizeSummaryOptions({ + nans: true, + percentile: [0.1, 0.9], + }); + hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARKS, + new tr.v.d.GenericSet(['benchmark A'])); + hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['label A'])); + hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['story A'])); + hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.TRACE_URLS, + new tr.v.d.GenericSet(['file://a/b.html'])); + hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(0)); + hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, + new tr.v.d.GenericSet([0])); + for (let i = 0; i <= 1e3; i += 10) { + hist0.addSample(i); + } + hist0.addSample(NaN); + + const hist1 = new tr.v.Histogram('hist1', tr.b.Unit.byName.sigma); + hist0.customizeSummaryOptions({ + std: false, + count: false, + sum: false, + min: false, + max: false, + }); + + const hist2 = new tr.v.Histogram('hist2', tr.b.Unit.byName.count); + hist2.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARKS, + new tr.v.d.GenericSet(['benchmark A'])); + hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(1499726648646)); + + const histograms = new tr.v.HistogramSet([hist0, hist1, hist2]); + let csv = new tr.v.CSVBuilder(histograms); + csv.build(); + csv = csv.toString().split('\n'); + assert.lengthOf(csv, histograms.length + 2); + assert.strictEqual(csv[0], + 'name,unit,avg,count,max,min,nans,pct_010,pct_090,std,sum,' + + 'architectures,benchmarks,benchmarkStart,bots,builds,deviceIds,' + + 'displayLabel,masters,memoryAmounts,osNames,osVersions,' + + 'productVersions,stories,storysetRepeats,traceStart,traceUrls'); + assert.strictEqual(csv[1], + 'hist0,B,500,101,1000,0,1,150,950,293.00170647967224,50500,,' + + 'benchmark A,2017-07-10 22:44:08,,,,label A,,,,,,story A,0,,' + + 'file://a/b.html'); + assert.strictEqual(csv[2], + 'hist1,σ,,0,-Infinity,Infinity,0,,,,0,,,,,,,Value,,,,,,,,,'); + assert.strictEqual(csv[3], + 'hist2,,,0,-Infinity,Infinity,0,,,,0,,benchmark A,,,,,' + + 'benchmark A,,,,,,,,,'); + assert.strictEqual(csv[4], ''); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py new file mode 100644 index 00000000000..a22a6ee39a9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2017 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. diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py new file mode 100644 index 00000000000..e947e4d45cd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py @@ -0,0 +1,180 @@ +# Copyright 2018 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. + +import contextlib +import json +import os +import tempfile + +from tracing.value import histogram_set +from tracing.value import merge_histograms +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import reserved_infos + + +ALL_NAMES = list(reserved_infos.AllNames()) + + +def _LoadHistogramSet(dicts): + hs = histogram_set.HistogramSet() + hs.ImportDicts(dicts) + return hs + + +@contextlib.contextmanager +def TempFile(): + try: + temp = tempfile.NamedTemporaryFile(delete=False) + yield temp + finally: + os.unlink(temp.name) + + +def GetTIRLabelFromHistogram(hist): + tags = hist.diagnostics.get(reserved_infos.STORY_TAGS.name) or [] + + tags_to_use = [t.split(':') for t in tags if ':' in t] + + return '_'.join(v for _, v in sorted(tags_to_use)) + + +def ComputeTestPath(hist): + path = hist.name + + # If a Histogram represents a summary across multiple stories, then its + # 'stories' diagnostic will contain the names of all of the stories. + # If a Histogram is not a summary, then its 'stories' diagnostic will contain + # the singular name of its story. + is_summary = list( + hist.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, [])) + + tir_label = GetTIRLabelFromHistogram(hist) + if tir_label and ( + not is_summary or reserved_infos.STORY_TAGS.name in is_summary): + path += '/' + tir_label + + is_ref = hist.diagnostics.get(reserved_infos.IS_REFERENCE_BUILD.name) + if is_ref and len(is_ref) == 1: + is_ref = is_ref.GetOnlyElement() + + story_name = hist.diagnostics.get(reserved_infos.STORIES.name) + if story_name and len(story_name) == 1 and not is_summary: + escaped_story_name = story_name.GetOnlyElement() + path += '/' + escaped_story_name + if is_ref: + path += '_ref' + elif is_ref: + path += '/ref' + + return path + + +def _MergeHistogramSetByPath(hs): + with TempFile() as temp: + temp.write(json.dumps(hs.AsDicts()).encode('utf-8')) + temp.close() + + return merge_histograms.MergeHistograms(temp.name, ( + reserved_infos.TEST_PATH.name,)) + + +def _GetAndDeleteHadFailures(hs): + had_failures = False + for h in hs: + had_failures_diag = h.diagnostics.get(reserved_infos.HAD_FAILURES.name) + if had_failures_diag: + del h.diagnostics[reserved_infos.HAD_FAILURES.name] + had_failures = True + return had_failures + + +def _MergeAndReplaceSharedDiagnostics(diagnostic_name, hs): + merged = None + for h in hs: + d = h.diagnostics.get(diagnostic_name) + if not d: + continue + + if not merged: + merged = d + else: + merged.AddDiagnostic(d) + h.diagnostics[diagnostic_name] = merged + + +def AddReservedDiagnostics(histogram_dicts, names_to_values): + # We need to generate summary statistics for anything that had a story, so + # filter out every histogram with no stories, then merge. If you keep the + # histograms with no story, you end up with duplicates. + hs_with_stories = _LoadHistogramSet(histogram_dicts) + hs_with_stories.FilterHistograms( + lambda h: not h.diagnostics.get(reserved_infos.STORIES.name, [])) + + hs_with_no_stories = _LoadHistogramSet(histogram_dicts) + hs_with_no_stories.FilterHistograms( + lambda h: h.diagnostics.get(reserved_infos.STORIES.name, [])) + + # TODO(#3987): Refactor recipes to call merge_histograms separately. + # This call combines all repetitions of a metric for a given story into a + # single histogram. + hs = histogram_set.HistogramSet() + hs.ImportDicts(hs_with_stories.AsDicts()) + + for h in hs: + h.diagnostics[reserved_infos.TEST_PATH.name] = ( + generic_set.GenericSet([ComputeTestPath(h)])) + + _GetAndDeleteHadFailures(hs) + dicts_across_repeats = _MergeHistogramSetByPath(hs) + + had_failures = _GetAndDeleteHadFailures(hs_with_stories) + + if not had_failures: + # This call creates summary metrics across each tag set of stories. + hs = histogram_set.HistogramSet() + hs.ImportDicts(hs_with_stories.AsDicts()) + hs.FilterHistograms(lambda h: not GetTIRLabelFromHistogram(h)) + + for h in hs: + h.diagnostics[reserved_infos.SUMMARY_KEYS.name] = ( + generic_set.GenericSet(['name', 'storyTags'])) + h.diagnostics[reserved_infos.TEST_PATH.name] = ( + generic_set.GenericSet([ComputeTestPath(h)])) + + dicts_across_stories = _MergeHistogramSetByPath(hs) + + # This call creates summary metrics across the entire story set. + hs = histogram_set.HistogramSet() + hs.ImportDicts(hs_with_stories.AsDicts()) + + for h in hs: + h.diagnostics[reserved_infos.SUMMARY_KEYS.name] = ( + generic_set.GenericSet(['name'])) + h.diagnostics[reserved_infos.TEST_PATH.name] = ( + generic_set.GenericSet([ComputeTestPath(h)])) + + dicts_across_names = _MergeHistogramSetByPath(hs) + else: + dicts_across_stories = [] + dicts_across_names = [] + + # Now load everything into one histogram set. First we load the summary + # histograms, since we need to mark them with SUMMARY_KEYS. + # After that we load the rest, and then apply all the diagnostics specified + # on the command line. Finally, since we end up with a lot of diagnostics + # that no histograms refer to, we make sure to prune those. + histograms = histogram_set.HistogramSet() + histograms.ImportDicts(dicts_across_names) + histograms.ImportDicts(dicts_across_stories) + histograms.ImportDicts(dicts_across_repeats) + histograms.ImportDicts(hs_with_no_stories.AsDicts()) + + histograms.DeduplicateDiagnostics() + for name, value in names_to_values.items(): + assert name in ALL_NAMES + histograms.AddSharedDiagnosticToAllHistograms( + name, generic_set.GenericSet([value])) + histograms.RemoveOrphanedDiagnostics() + + return json.dumps(histograms.AsDicts()) diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py new file mode 100644 index 00000000000..e94589c2cb9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py @@ -0,0 +1,237 @@ +# Copyright 2018 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. + +import json +import unittest + +from tracing.value import histogram +from tracing.value import histogram_set +from tracing.value.diagnostics import add_reserved_diagnostics +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import reserved_infos + +class AddReservedDiagnosticsUnittest(unittest.TestCase): + + def _CreateHistogram(self, name, stories=None, tags=None, had_failures=False): + h = histogram.Histogram(name, 'count') + if stories: + h.diagnostics[reserved_infos.STORIES.name] = generic_set.GenericSet( + stories) + if tags: + h.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + tags) + if had_failures: + h.diagnostics[reserved_infos.HAD_FAILURES.name] = generic_set.GenericSet( + [True]) + return h + + def testAddReservedDiagnostics_InvalidDiagnostic_Raises(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo')]) + + with self.assertRaises(AssertionError): + add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'SOME INVALID DIAGNOSTIC': 'bar'}) + + def testAddReservedDiagnostics_DiagnosticsAdded(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo1', stories=['foo1']), + self._CreateHistogram('foo1', stories=['foo1']), + self._CreateHistogram('bar', stories=['bar1']), + self._CreateHistogram('bar', stories=['bar2']), + self._CreateHistogram('blah')]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + for h in new_hs: + self.assertIn('benchmarks', h.diagnostics) + benchmarks = list(h.diagnostics['benchmarks']) + self.assertEqual(['bar'], benchmarks) + + def testAddReservedDiagnostics_SummaryAddedToMerged(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo1', stories=['foo1']), + self._CreateHistogram('foo1', stories=['foo1']), + self._CreateHistogram('bar', stories=['bar1']), + self._CreateHistogram('bar', stories=['bar2']), + self._CreateHistogram('blah')]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + expected = [ + [u'foo1', [], [u'foo1']], + [u'bar', [], [u'bar1']], + [u'blah', [], []], + [u'bar', [u'name'], [u'bar1', u'bar2']], + [u'foo1', [u'name'], [u'foo1']], + [u'bar', [], [u'bar2']], + ] + + for h in new_hs: + is_summary = sorted( + list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, []))) + stories = sorted(list(h.diagnostics.get(reserved_infos.STORIES.name, []))) + self.assertIn([h.name, is_summary, stories], expected) + expected.remove([h.name, is_summary, stories]) + + self.assertEqual(0, len(expected)) + + def testAddReservedDiagnostics_Repeats_Merged(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo1', stories=['foo1']), + self._CreateHistogram('foo1', stories=['foo1']), + self._CreateHistogram('foo2', stories=['foo2'])]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + expected = [ + [u'foo2', [u'name']], + [u'foo1', [u'name']], + [u'foo2', []], + [u'foo1', []], + ] + + for h in new_hs: + is_summary = sorted( + list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, []))) + self.assertIn([h.name, is_summary], expected) + expected.remove([h.name, is_summary]) + + self.assertEqual(0, len(expected)) + + def testAddReservedDiagnostics_Stories_Merged(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo', stories=['foo1']), + self._CreateHistogram('foo', stories=['foo2']), + self._CreateHistogram('bar', stories=['bar'])]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + expected = [ + [u'foo', [], [u'foo2']], + [u'foo', [u'name'], [u'foo1', u'foo2']], + [u'bar', [u'name'], [u'bar']], + [u'foo', [], [u'foo1']], + [u'bar', [], [u'bar']], + ] + + for h in new_hs: + is_summary = sorted( + list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, []))) + stories = sorted(list(h.diagnostics[reserved_infos.STORIES.name])) + self.assertIn([h.name, is_summary, stories], expected) + expected.remove([h.name, is_summary, stories]) + + self.assertEqual(0, len(expected)) + + def testAddReservedDiagnostics_NoStories_Unmerged(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo'), + self._CreateHistogram('foo'), + self._CreateHistogram('bar')]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + for h in new_hs: + self.assertNotIn(reserved_infos.SUMMARY_KEYS.name, h.diagnostics) + + self.assertEqual(2, len(new_hs.GetHistogramsNamed('foo'))) + self.assertEqual(1, len(new_hs.GetHistogramsNamed('bar'))) + + def testAddReservedDiagnostics_WithTags(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo', ['bar'], ['t:1']), + self._CreateHistogram('foo', ['bar'], ['t:2']) + ]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + expected = [ + [u'foo', [u'name'], [u'bar'], [u't:1', u't:2']], + [u'foo', [], [u'bar'], [u't:1']], + [u'foo', [], [u'bar'], [u't:2']], + [u'foo', [u'name', u'storyTags'], [u'bar'], [u't:1']], + [u'foo', [u'name', u'storyTags'], [u'bar'], [u't:2']], + ] + + for h in new_hs: + is_summary = sorted( + list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, []))) + stories = sorted(list(h.diagnostics[reserved_infos.STORIES.name])) + tags = sorted(list(h.diagnostics[reserved_infos.STORY_TAGS.name])) + self.assertIn([h.name, is_summary, stories, tags], expected) + expected.remove([h.name, is_summary, stories, tags]) + + self.assertEqual(0, len(expected)) + + def testAddReservedDiagnostics_WithTags_SomeIgnored(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram( + 'foo', stories=['story1'], tags=['t:1', 'ignored']), + self._CreateHistogram( + 'foo', stories=['story1'], tags=['t:1']), + ]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + expected = [ + [u'foo', [u'name', u'storyTags'], [u'story1'], [u'ignored', u't:1']], + [u'foo', [], [u'story1'], [u'ignored', u't:1']], + [u'foo', [u'name'], [u'story1'], [u'ignored', u't:1']], + ] + + for h in new_hs: + is_summary = sorted( + list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, []))) + stories = sorted(list(h.diagnostics[reserved_infos.STORIES.name])) + tags = sorted(list(h.diagnostics[reserved_infos.STORY_TAGS.name])) + self.assertIn([h.name, is_summary, stories, tags], expected) + expected.remove([h.name, is_summary, stories, tags]) + + self.assertEqual(0, len(expected)) + + def testAddReservedDiagnostics_OmitsSummariesIfHadFailures(self): + hs = histogram_set.HistogramSet([ + self._CreateHistogram('foo', ['bar'], had_failures=True)]) + + new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics( + hs.AsDicts(), {'benchmarks': 'bar'}) + + new_hs = histogram_set.HistogramSet() + new_hs.ImportDicts(json.loads(new_hs_json)) + + self.assertEqual(len(new_hs), 1) + + h = new_hs.GetFirstHistogram() + self.assertEqual(h.name, 'foo') + self.assertNotIn(reserved_infos.SUMMARY_KEYS.name, h.diagnostics) + self.assertNotIn(reserved_infos.HAD_FAILURES.name, h.diagnostics) diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html new file mode 100644 index 00000000000..48ec199d6ca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/diagnostics/breakdown.html"> +<link rel="import" href="/tracing/value/diagnostics/collected_related_event_set.html"> +<link rel="import" href="/tracing/value/diagnostics/date_range.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic_ref.html"> +<link rel="import" href="/tracing/value/diagnostics/generic_set.html"> +<link rel="import" href="/tracing/value/diagnostics/related_event_set.html"> +<link rel="import" href="/tracing/value/diagnostics/related_name_map.html"> +<link rel="import" href="/tracing/value/diagnostics/scalar.html"> +<link rel="import" href="/tracing/value/diagnostics/unmergeable_diagnostic_set.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py new file mode 100644 index 00000000000..95b18c65e7c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py @@ -0,0 +1,42 @@ +# Copyright 2017 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. + +import importlib +import sys + + +# TODO(#3613): Flatten this to a list once diagnostics are in their own modules. +_MODULES_BY_DIAGNOSTIC_NAME = { + 'Breakdown': 'diagnostics.breakdown', + 'GenericSet': 'diagnostics.generic_set', + 'UnmergeableDiagnosticSet': 'diagnostics.unmergeable_diagnostic_set', + 'RelatedEventSet': 'diagnostics.related_event_set', + 'DateRange': 'diagnostics.date_range', + 'RelatedNameMap': 'diagnostics.related_name_map', +} + + +_CLASSES_BY_NAME = {} + + +def IsDiagnosticTypename(name): + return name in _MODULES_BY_DIAGNOSTIC_NAME + + +def GetDiagnosticTypenames(): + return _MODULES_BY_DIAGNOSTIC_NAME.keys() + + +def GetDiagnosticClassForName(name): + assert IsDiagnosticTypename(name) + + if name in _CLASSES_BY_NAME: + return _CLASSES_BY_NAME[name] + + module_name = 'tracing.value.%s' % _MODULES_BY_DIAGNOSTIC_NAME[name] + importlib.import_module(module_name) + + cls = getattr(sys.modules[module_name], name) + _CLASSES_BY_NAME[name] = cls + return cls diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py new file mode 100644 index 00000000000..9004114c3d9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py @@ -0,0 +1,27 @@ +# Copyright 2018 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. + +import unittest + +from tracing.value.diagnostics import all_diagnostics + + +class AllDiagnosticsUnittest(unittest.TestCase): + + def testGetDiagnosticClassForName(self): + cls0 = all_diagnostics.GetDiagnosticClassForName('GenericSet') + gs0 = cls0(['foo']) + gs0_dict = gs0.AsDict() + + # Run twice to ensure that the memoization isn't broken. + cls1 = all_diagnostics.GetDiagnosticClassForName('GenericSet') + gs1 = cls1(['foo']) + gs1_dict = gs1.AsDict() + + self.assertEqual(gs0_dict['type'], 'GenericSet') + self.assertEqual(gs1_dict['type'], 'GenericSet') + + def testGetDiagnosticClassForName_Bogus(self): + self.assertRaises( + AssertionError, all_diagnostics.GetDiagnosticClassForName, 'BogusDiag') diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html new file mode 100644 index 00000000000..bf19b381170 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html @@ -0,0 +1,116 @@ +<!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/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + class Breakdown extends tr.v.d.Diagnostic { + constructor() { + super(); + this.values_ = new Map(); + this.colorScheme = undefined; + } + + truncate(unit) { + for (const [name, value] of this) { + this.values_.set(name, unit.truncate(value)); + } + } + + clone() { + const clone = new Breakdown(); + clone.colorScheme = this.colorScheme; + clone.addDiagnostic(this); + return clone; + } + + canAddDiagnostic(otherDiagnostic) { + return ((otherDiagnostic instanceof Breakdown) && + (otherDiagnostic.colorScheme === this.colorScheme)); + } + + addDiagnostic(otherDiagnostic) { + for (const [name, value] of otherDiagnostic) { + this.set(name, this.get(name) + value); + } + return this; + } + + /** + * Add a Value by an explicit name to this map. + * + * @param {string} name + * @param {number} value + */ + set(name, value) { + if (typeof name !== 'string' || + typeof value !== 'number') { + throw new Error('Breakdown maps from strings to numbers'); + } + this.values_.set(name, value); + } + + /** + * @param {string} name + * @return {number} + */ + get(name) { + return this.values_.get(name) || 0; + } + + * [Symbol.iterator]() { + for (const pair of this.values_) { + yield pair; + } + } + + get size() { + return this.values_.size; + } + + asDictInto_(d) { + d.values = {}; + for (const [name, value] of this) { + d.values[name] = tr.b.numberToJson(value); + } + if (this.colorScheme) { + d.colorScheme = this.colorScheme; + } + } + + static fromEntries(entries) { + const breakdown = new Breakdown(); + for (const [name, value] of entries) { + breakdown.set(name, value); + } + return breakdown; + } + + static fromDict(d) { + const breakdown = new Breakdown(); + for (const [name, value] of Object.entries(d.values)) { + breakdown.set(name, tr.b.numberFromJson(value)); + } + if (d.colorScheme) { + breakdown.colorScheme = d.colorScheme; + } + return breakdown; + } + } + + tr.v.d.Diagnostic.register(Breakdown, { + elementName: 'tr-v-ui-breakdown-span' + }); + + return { + Breakdown, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py new file mode 100644 index 00000000000..5272734f469 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py @@ -0,0 +1,73 @@ +# Copyright 2018 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. + +import math +import numbers + +from tracing.value.diagnostics import diagnostic + + +try: + StringTypes = basestring +except NameError: + StringTypes = str + + +class Breakdown(diagnostic.Diagnostic): + __slots__ = '_values', '_color_scheme' + + def __init__(self): + super(Breakdown, self).__init__() + self._values = {} + self._color_scheme = None + + @property + def color_scheme(self): + return self._color_scheme + + @staticmethod + def FromDict(d): + result = Breakdown() + result._color_scheme = d.get('colorScheme') + for name, value in d['values'].items(): + if value in ['NaN', 'Infinity', '-Infinity']: + value = float(value) + result.Set(name, value) + return result + + def _AsDictInto(self, d): + d['values'] = {} + for name, value in self: + # JSON serializes NaN and the infinities as 'null', preventing + # distinguishing between them. Override that behavior by serializing them + # as their Javascript string names, not their python string names since + # the reference implementation is in Javascript. + if math.isnan(value): + value = 'NaN' + elif math.isinf(value): + if value > 0: + value = 'Infinity' + else: + value = '-Infinity' + d['values'][name] = value + if self._color_scheme: + d['colorScheme'] = self._color_scheme + + def Set(self, name, value): + assert isinstance(name, StringTypes), ( + 'Expected basestring, found %s: "%r"' % (type(name).__name__, name)) + assert isinstance(value, numbers.Number), ( + 'Expected number, found %s: "%r"', (type(value).__name__, value)) + self._values[name] = value + + def Get(self, name): + return self._values.get(name, 0) + + def __iter__(self): + for name, value in self._values.items(): + yield name, value + + def __len__(self): + return len(self._values) + diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html new file mode 100644 index 00000000000..23b4e54359d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/breakdown.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('merge', function() { + const a = new tr.v.d.Breakdown(); + a.set('x', 1); + a.set('y', 2); + + const b = new tr.v.d.Breakdown(); + b.set('y', 3); + b.set('z', 4); + + assert.isTrue(a.canAddDiagnostic(b)); + assert.isTrue(b.canAddDiagnostic(a)); + + a.addDiagnostic(b); + assert.strictEqual(a.get('x'), 1); + assert.strictEqual(a.get('y'), 5); + assert.strictEqual(a.get('z'), 4); + + a.colorScheme = 'fake color scheme'; + assert.isFalse(a.canAddDiagnostic(b)); + assert.isFalse(b.canAddDiagnostic(a)); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py new file mode 100644 index 00000000000..ab9c6733771 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py @@ -0,0 +1,31 @@ +# Copyright 2018 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. + +import json +import math +import unittest + +from tracing.value.diagnostics import breakdown +from tracing.value.diagnostics import diagnostic + + +class BreakdownUnittest(unittest.TestCase): + + def testRoundtrip(self): + bd = breakdown.Breakdown() + bd.Set('one', 1) + bd.Set('m1', -1) + bd.Set('inf', float('inf')) + bd.Set('nun', float('nan')) + bd.Set('ninf', float('-inf')) + bd.Set('long', 2**65) + d = bd.AsDict() + clone = diagnostic.Diagnostic.FromDict(d) + self.assertEqual(json.dumps(d), json.dumps(clone.AsDict())) + self.assertEqual(clone.Get('one'), 1) + self.assertEqual(clone.Get('m1'), -1) + self.assertEqual(clone.Get('inf'), float('inf')) + self.assertTrue(math.isnan(clone.Get('nun'))) + self.assertEqual(clone.Get('ninf'), float('-inf')) + self.assertEqual(clone.Get('long'), 2**65) diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html new file mode 100644 index 00000000000..b88a8bf3349 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + class CollectedRelatedEventSet extends tr.v.d.Diagnostic { + constructor() { + super(); + this.eventSetsByCanonicalUrl_ = new Map(); + } + + asDictInto_(d) { + d.events = {}; + for (const [canonicalUrl, eventSet] of this) { + d.events[canonicalUrl] = []; + for (const event of eventSet) { + d.events[canonicalUrl].push({ + stableId: event.stableId, + title: event.title, + start: event.start, + duration: event.duration + }); + } + } + } + + static fromDict(d) { + const result = new CollectedRelatedEventSet(); + for (const [canonicalUrl, events] of Object.entries(d.events)) { + result.eventSetsByCanonicalUrl_.set(canonicalUrl, events.map( + e => new tr.v.d.EventRef(e))); + } + return result; + } + + get size() { + return this.eventSetsByCanonicalUrl_.size; + } + + get(canonicalUrl) { + return this.eventSetsByCanonicalUrl_.get(canonicalUrl); + } + + * [Symbol.iterator]() { + for (const [canonicalUrl, eventSet] of this.eventSetsByCanonicalUrl_) { + yield [canonicalUrl, eventSet]; + } + } + + canAddDiagnostic(otherDiagnostic) { + return otherDiagnostic instanceof tr.v.d.RelatedEventSet || + otherDiagnostic instanceof tr.v.d.CollectedRelatedEventSet; + } + + addEventSetForCanonicalUrl(canonicalUrl, events) { + let myEventSet = this.eventSetsByCanonicalUrl_.get(canonicalUrl); + if (myEventSet === undefined) { + myEventSet = new Set(); + this.eventSetsByCanonicalUrl_.set(canonicalUrl, myEventSet); + } + for (const event of events) { + myEventSet.add(event); + } + } + + addDiagnostic(otherDiagnostic) { + if (otherDiagnostic instanceof tr.v.d.CollectedRelatedEventSet) { + // Merge Maps of Sets. + for (const [canonicalUrl, otherEventSet] of otherDiagnostic) { + this.addEventSetForCanonicalUrl(canonicalUrl, otherEventSet); + } + return; + } + + if (!otherDiagnostic.canonicalUrl) return; + this.addEventSetForCanonicalUrl( + otherDiagnostic.canonicalUrl, otherDiagnostic); + } + } + + tr.v.d.Diagnostic.register(CollectedRelatedEventSet, { + elementName: 'tr-v-ui-collected-related-event-set-span' + }); + + return { + CollectedRelatedEventSet, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html new file mode 100644 index 00000000000..f519326b553 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + /** + * This class represents a mergeable range of Date objects. + * This range can contain 1 or 2 Dates. + */ + class DateRange extends tr.v.d.Diagnostic { + /** + * @param {number} ms + */ + constructor(ms) { + super(); + this.range_ = new tr.b.math.Range(); + this.range_.addValue(ms); + } + + get minDate() { + return new Date(this.range_.min); + } + + get maxDate() { + return new Date(this.range_.max); + } + + get durationMs() { + return this.range_.duration; + } + + clone() { + const clone = new tr.v.d.DateRange(this.range_.min); + clone.addDiagnostic(this); + return clone; + } + + equals(other) { + if (!(other instanceof DateRange)) return false; + return this.range_.equals(other.range_); + } + + canAddDiagnostic(otherDiagnostic) { + return otherDiagnostic instanceof DateRange; + } + + addDiagnostic(other) { + this.range_.addRange(other.range_); + } + + toString() { + const minDate = tr.b.formatDate(this.minDate); + if (this.durationMs === 0) return minDate; + const maxDate = tr.b.formatDate(this.maxDate); + return `${minDate} - ${maxDate}`; + } + + asDictInto_(d) { + d.min = this.range_.min; + if (this.durationMs === 0) return; + d.max = this.range_.max; + } + + static fromDict(d) { + const dateRange = new DateRange(d.min); + if (d.max !== undefined) dateRange.range_.addValue(d.max); + return dateRange; + } + } + + tr.v.d.Diagnostic.register(DateRange, { + elementName: 'tr-v-ui-date-range-span' + }); + + return { + DateRange, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py new file mode 100644 index 00000000000..3cdce45d49c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py @@ -0,0 +1,71 @@ +# Copyright 2018 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. +import datetime + +from tracing.value.diagnostics import diagnostic + + +class DateRange(diagnostic.Diagnostic): + __slots__ = '_range', + + def __init__(self, ms): + from tracing.value import histogram + super(DateRange, self).__init__() + self._range = histogram.Range() + self._range.AddValue(ms) + + def __eq__(self, other): + if not isinstance(other, DateRange): + return False + return self._range == other._range + + def __hash__(self): + return id(self) + + @property + def min_date(self): + return datetime.datetime.utcfromtimestamp(self._range.min / 1000) + + @property + def max_date(self): + return datetime.datetime.utcfromtimestamp(self._range.max / 1000) + + @property + def min_timestamp(self): + return self._range.min + + @property + def max_timestamp(self): + return self._range.max + + @property + def duration_ms(self): + return self._range.duration + + def __str__(self): + min_date = self.min_date.isoformat().replace('T', ' ')[:19] + if self.duration_ms == 0: + return min_date + max_date = self.max_date.isoformat().replace('T', ' ')[:19] + return min_date + ' - ' + max_date + + def _AsDictInto(self, dct): + dct['min'] = self._range.min + if self.duration_ms == 0: + return + dct['max'] = self._range.max + + @staticmethod + def FromDict(dct): + dr = DateRange(dct['min']) + if 'max' in dct: + dr._range.AddValue(dct['max']) + return dr + + def CanAddDiagnostic(self, other_diagnostic): + return isinstance(other_diagnostic, DateRange) + + def AddDiagnostic(self, other_diagnostic): + self._range.AddRange(other_diagnostic._range) + diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py new file mode 100644 index 00000000000..4636045da1c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py @@ -0,0 +1,31 @@ +# Copyright 2018 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. + +import calendar +import unittest + +from tracing.value.diagnostics import date_range +from tracing.value.diagnostics import diagnostic + + +class DateRangeUnittest(unittest.TestCase): + + def testRoundtrip(self): + dr = date_range.DateRange(1496693745000) + dr.AddDiagnostic(date_range.DateRange(1496693746000)) + self.assertEqual(calendar.timegm(dr.min_date.timetuple()), 1496693745) + self.assertEqual(calendar.timegm(dr.max_date.timetuple()), 1496693746) + clone = diagnostic.Diagnostic.FromDict(dr.AsDict()) + self.assertEqual(clone.min_date, dr.min_date) + self.assertEqual(clone.max_date, dr.max_date) + + def testMinTimestamp(self): + dr = date_range.DateRange(1496693745123) + dr.AddDiagnostic(date_range.DateRange(1496693746123)) + self.assertEqual(dr.min_timestamp, 1496693745123) + + def testMaxTimestamp(self): + dr = date_range.DateRange(1496693745123) + dr.AddDiagnostic(date_range.DateRange(1496693746123)) + self.assertEqual(dr.max_timestamp, 1496693746123) diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html new file mode 100644 index 00000000000..cb3b4907563 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html @@ -0,0 +1,133 @@ +<!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/extension_registry.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + class Diagnostic { + constructor() { + this.guid_ = undefined; + } + + /** + * Returns a new Diagnostic that is like this one but distinct. + * This is useful when merging Diagnostics. + * @return {!tr.v.d.Diagnostic} + */ + clone() { + return new this.constructor(); + } + + /** + * Return true if |this| can be merged with |otherDiagnostic|. + * + * @param {!tr.v.d.Diagnostic} otherDiagnostic + * @return {!boolean} + */ + canAddDiagnostic(otherDiagnostic) { + return false; + } + + /** + * If subclasses override canAddDiagnostic() then they must also override + * this method to modify |this| to include information from + * |otherDiagnostic|. + * + * @param {!tr.v.d.Diagnostic} otherDiagnostic + */ + addDiagnostic(otherDiagnostic) { + throw new Error('Abstract virtual method: subclasses must override ' + + 'this method if they override canAddDiagnostic'); + } + + 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; + } + + get hasGuid() { + return this.guid_ !== undefined; + } + + /** + * If this Diagnostic is shared between multiple Histograms, then return its + * |guid|. Otherwise, serialize this Diagnostic to a dictionary. + * + * @return {string|!Object} + */ + asDictOrReference() { + if (this.guid_ !== undefined) { + return this.guid_; + } + return this.asDict(); + } + + /** + * Serialize all of the information in this Diagnostic to a dictionary, + * regardless of whether it is shared between multiple Histograms. + * + * @return {!Object} + */ + asDict() { + const result = {type: this.constructor.name}; + if (this.guid_ !== undefined) { + result.guid = this.guid_; + } + this.asDictInto_(result); + return result; + } + + asDictInto_(d) { + throw new Error('Abstract virtual method: subclasses must override ' + + 'this method if they override canAddDiagnostic'); + } + + static fromDict(d) { + const typeInfo = Diagnostic.findTypeInfoWithName(d.type); + if (!typeInfo) { + throw new Error('Unrecognized diagnostic type: ' + d.type); + } + + const diagnostic = typeInfo.constructor.fromDict(d); + if (d.guid !== undefined) diagnostic.guid = d.guid; + return diagnostic; + } + } + + const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE); + options.defaultMetadata = {}; + options.mandatoryBaseClass = Diagnostic; + tr.b.decorateExtensionRegistry(Diagnostic, options); + + Diagnostic.addEventListener('will-register', function(e) { + const constructor = e.typeInfo.constructor; + if (!(constructor.fromDict instanceof Function) || + (constructor.fromDict === Diagnostic.fromDict) || + (constructor.fromDict.length !== 1)) { + throw new Error('Diagnostics must define fromDict(d)'); + } + }); + + return { + Diagnostic, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py new file mode 100644 index 00000000000..d2decda8f12 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py @@ -0,0 +1,95 @@ +# Copyright 2017 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. + +import uuid + +try: + from py_utils import slots_metaclass + SlotsMetaclass = slots_metaclass.SlotsMetaclass # pylint: disable=invalid-name +except ImportError: + # TODO(benjhayden): Figure out why py_utils doesn't work in dev_appserver.py + SlotsMetaclass = None # pylint: disable=invalid-name + +from tracing.value.diagnostics import all_diagnostics + + +class Diagnostic(object): + __slots__ = '_guid', + + # Ensure that new subclasses remember to specify __slots__ in order to prevent + # regressing memory consumption: + if SlotsMetaclass: + __metaclass__ = SlotsMetaclass + + def __init__(self): + self._guid = None + + def __ne__(self, other): + return not self == other + + @property + def guid(self): + if self._guid is None: + self._guid = str(uuid.uuid4()) + return self._guid + + @guid.setter + def guid(self, g): + assert self._guid is None + self._guid = g + + @property + def has_guid(self): + return self._guid is not None + + def AsDictOrReference(self): + if self._guid: + return self._guid + return self.AsDict() + + def AsDict(self): + dct = {'type': self.__class__.__name__} + if self._guid: + dct['guid'] = self._guid + self._AsDictInto(dct) + return dct + + def _AsDictInto(self, unused_dct): + raise NotImplementedError + + @staticmethod + def FromDict(dct): + cls = all_diagnostics.GetDiagnosticClassForName(dct['type']) + if not cls: + raise ValueError('Unrecognized diagnostic type: ' + dct['type']) + diagnostic = cls.FromDict(dct) + if 'guid' in dct: + diagnostic.guid = dct['guid'] + return diagnostic + + def ResetGuid(self, guid=None): + if guid: + self._guid = guid + else: + self._guid = str(uuid.uuid4()) + + def Inline(self): + """Inlines a shared diagnostic. + + Any diagnostic that has a guid will be serialized as a reference, because it + is assumed that diagnostics with guids are shared. This method removes the + guid so that the diagnostic will be serialized by value. + + Inling is used for example in the dashboard, where certain types of shared + diagnostics that vary on a per-upload basis are inlined for efficiency + reasons. + """ + self._guid = None + + def CanAddDiagnostic(self, unused_other_diagnostic): + return False + + def AddDiagnostic(self, unused_other_diagnostic): + raise Exception('Abstract virtual method: subclasses must override ' + 'this method if they override canAddDiagnostic') diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html new file mode 100644 index 00000000000..79af5824b7e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html @@ -0,0 +1,177 @@ +<!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. +--> + +<!-- + Include all Diagnostic subclasses here so that DiagnosticMap.addDicts() and + DiagnosticMap.fromDict() always have access to all subclasses in the + Diagnostic registry. +--> +<link rel="import" href="/tracing/value/diagnostics/all_diagnostics.html"> +<link rel="import" href="/tracing/value/diagnostics/reserved_names.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + class DiagnosticMap extends Map { + /** + * @param {boolean=} opt_allowReservedNames defaults to true + */ + constructor(opt_allowReservedNames) { + super(); + if (opt_allowReservedNames === undefined) { + opt_allowReservedNames = true; + } + this.allowReservedNames_ = opt_allowReservedNames; + } + + /** + * Add a new Diagnostic to this map. + * + * @param {string} name + * @param {!tr.v.d.Diagnostic} diagnostic + */ + set(name, diagnostic) { + if (typeof(name) !== 'string') { + throw new Error(`name must be string, not ${name}`); + } + + if (!(diagnostic instanceof tr.v.d.Diagnostic) && + !(diagnostic instanceof tr.v.d.DiagnosticRef)) { + throw new Error(`Must be instanceof Diagnostic: ${diagnostic}`); + } + + // TODO(#3507): Reserved names should never be UnmergeableDiagnosticSet. + if (!this.allowReservedNames_ && + tr.v.d.RESERVED_NAMES_SET.has(name) && + !(diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet) && + !(diagnostic instanceof tr.v.d.DiagnosticRef)) { + const type = tr.v.d.RESERVED_NAMES_TO_TYPES.get(name); + if (type && !(diagnostic instanceof type)) { + throw new Error( + `Diagnostics named "${name}" must be ${type.name}, ` + + `not ${diagnostic.constructor.name}`); + } + } + + Map.prototype.set.call(this, name, diagnostic); + } + + delete(name) { + if (name === undefined) throw new Error('missing name'); + Map.prototype.delete.call(this, name); + } + + /** + * Add Diagnostics from a dictionary of dictionaries. + * + * @param {Object} dict + */ + addDicts(dict) { + for (const [name, diagnosticDict] of Object.entries(dict)) { + if (name === 'tagmap') continue; + if (typeof diagnosticDict === 'string') { + this.set(name, new tr.v.d.DiagnosticRef(diagnosticDict)); + } else if (diagnosticDict.type !== 'RelatedHistogramMap' && + diagnosticDict.type !== 'RelatedHistogramBreakdown' && + diagnosticDict.type !== 'TagMap') { + // Ignore RelatedHistograms and TagMaps. + // TODO(benjhayden): Forget about them in 2019 Q2. + this.set(name, tr.v.d.Diagnostic.fromDict(diagnosticDict)); + } + } + } + + resolveSharedDiagnostics(histograms, opt_required) { + for (const [name, value] of this) { + if (!(value instanceof tr.v.d.DiagnosticRef)) { + continue; + } + + const guid = value.guid; + const diagnostic = histograms.lookupDiagnostic(guid); + if (diagnostic instanceof tr.v.d.Diagnostic) { + this.set(name, diagnostic); + } else if (opt_required) { + throw new Error('Unable to find shared Diagnostic ' + guid); + } + } + } + + asDict() { + const dict = {}; + for (const [name, diagnostic] of this) { + dict[name] = diagnostic.asDictOrReference(); + } + return dict; + } + + static fromDict(d) { + const diagnostics = new DiagnosticMap(); + diagnostics.addDicts(d); + return diagnostics; + } + + /** + * Convert dictionary or ES6 Map to DiagnosticMap. + * @param {!Object|!Map.<string, !tr.v.d.Diagnostic>} obj + * @return {tr.v.d.DiagnosticMap} + */ + static fromObject(obj) { + const diagnostics = new DiagnosticMap(); + if (!(obj instanceof Map)) obj = Object.entries(obj); + for (const [name, diagnostic] of obj) { + if (!diagnostic) continue; + diagnostics.set(name, diagnostic); + } + return diagnostics; + } + + addDiagnostics(other) { + for (const [name, otherDiagnostic] of other) { + const myDiagnostic = this.get(name); + + if (myDiagnostic !== undefined && + myDiagnostic.canAddDiagnostic(otherDiagnostic)) { + myDiagnostic.addDiagnostic(otherDiagnostic); + continue; + } + + // We need to avoid storing references to |otherDiagnostic| in both + // |this| and |other| because future merge()s may add yet other + // Diagnostics to |this|, and they shouldn't accidentally modify + // anything in |other|. + // Now, either |this| doesn't already have a Diagnostic named |name| + // (myDiagnostic is undefined), or + // |this| already has a Diagnostic named |name| that can't be merged + // with |otherDiagnostic|. + // Either way, we need to clone |otherDiagnostic|. + // However, clones produced via fromDict/toDict cannot necessarily be + // merged with yet other Diagnostics, either because of semantics (as in + // the case of TelemtryInfo and the like) or because guids must not be + // shared by distinct Diagnostics. Therefore, Diagnostics support + // another way of cloning that is specifically targeted at supporting + // merging: clone(). + + const clone = otherDiagnostic.clone(); + + if (myDiagnostic === undefined) { + this.set(name, clone); + continue; + } + + // Now, |myDiagnostic| exists and it is unmergeable with |clone|, which + // is safe to store in |this|. + this.set(name, new tr.v.d.UnmergeableDiagnosticSet( + [myDiagnostic, clone])); + } + } + } + + return {DiagnosticMap}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html new file mode 100644 index 00000000000..8d0781fb22d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html @@ -0,0 +1,130 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('clone', function() { + const diagnostics = new tr.v.d.DiagnosticMap(); + diagnostics.set('generic', new tr.v.d.GenericSet([{a: ['b', 3]}])); + diagnostics.set('breakdown', new tr.v.d.Breakdown()); + diagnostics.set('events', new tr.v.d.RelatedEventSet()); + + const clone = tr.v.d.DiagnosticMap.fromDict(diagnostics.asDict()); + assert.instanceOf(clone.get('generic'), tr.v.d.GenericSet); + assert.deepEqual(tr.b.getOnlyElement(clone.get('generic')), + tr.b.getOnlyElement(diagnostics.get('generic'))); + assert.instanceOf(clone.get('breakdown'), tr.v.d.Breakdown); + assert.instanceOf(clone.get('events'), tr.v.d.RelatedEventSet); + }); + + test('fromObject', function() { + assert.strictEqual(tr.v.d.DiagnosticMap.fromObject( + {a: new tr.v.d.GenericSet([])}).size, 1); + assert.strictEqual(tr.v.d.DiagnosticMap.fromObject( + new Map([['a', new tr.v.d.GenericSet([])]])).size, 1); + }); + + test('cloneWithRef', function() { + const diagnostics = new tr.v.d.DiagnosticMap(); + diagnostics.set('ref', new tr.v.d.DiagnosticRef('abc')); + + const clone = tr.v.d.DiagnosticMap.fromDict(diagnostics.asDict()); + assert.instanceOf(clone.get('ref'), tr.v.d.DiagnosticRef); + assert.strictEqual(clone.get('ref').guid, 'abc'); + }); + + test('requireFromDict', function() { + class MissingFromDict extends tr.v.d.Diagnostic { } + assert.throws(() => tr.v.d.Diagnostic.register(MissingFromDict)); + + class InvalidFromDict extends tr.v.d.Diagnostic { + static fromDict() { + } + } + assert.throws(() => tr.v.d.Diagnostic.register(InvalidFromDict)); + }); + + test('merge', function() { + const event = tr.c.TestUtils.newSliceEx({ + title: 'event', + start: 0, + duration: 1, + }); + event.parentContainer = { + sliceGroup: { + stableId: 'fake_thread', + slices: [event], + }, + }; + const generic = new tr.v.d.GenericSet(['generic diagnostic']); + const generic2 = new tr.v.d.GenericSet(['generic diagnostic 2']); + const events = new tr.v.d.RelatedEventSet([event]); + + // When Histograms are merged, first an empty clone is created with an empty + // DiagnosticMap. + const hist = new tr.v.Histogram('', tr.b.Unit.byName.count); + + const hist2 = new tr.v.Histogram('', tr.b.Unit.byName.count); + hist2.diagnostics.set('a', generic); + hist.diagnostics.addDiagnostics(hist2.diagnostics); + assert.strictEqual(tr.b.getOnlyElement(generic), + tr.b.getOnlyElement(hist.diagnostics.get('a'))); + + // Separate keys are not merged. + const hist3 = new tr.v.Histogram('', tr.b.Unit.byName.count); + hist3.diagnostics.set('b', generic2); + hist.diagnostics.addDiagnostics(hist3.diagnostics); + assert.strictEqual( + tr.b.getOnlyElement(generic), + tr.b.getOnlyElement(hist.diagnostics.get('a'))); + assert.strictEqual( + tr.b.getOnlyElement(generic2), + tr.b.getOnlyElement(hist.diagnostics.get('b'))); + + // Merging unmergeable diagnostics should produce an + // UnmergeableDiagnosticSet. + const hist4 = new tr.v.Histogram('', tr.b.Unit.byName.count); + hist4.diagnostics.set('a', new tr.v.d.RelatedNameMap()); + hist.diagnostics.addDiagnostics(hist4.diagnostics); + assert.instanceOf(hist.diagnostics.get('a'), + tr.v.d.UnmergeableDiagnosticSet); + let diagnostics = Array.from(hist.diagnostics.get('a')); + assert.strictEqual( + tr.b.getOnlyElement(generic), tr.b.getOnlyElement(diagnostics[0])); + + // UnmergeableDiagnosticSets are mergeable. + const hist5 = new tr.v.Histogram('', tr.b.Unit.byName.count); + hist5.diagnostics.set('a', new tr.v.d.UnmergeableDiagnosticSet([ + events, generic2])); + hist.diagnostics.addDiagnostics(hist5.diagnostics); + assert.instanceOf(hist.diagnostics.get('a'), + tr.v.d.UnmergeableDiagnosticSet); + diagnostics = Array.from(hist.diagnostics.get('a')); + assert.lengthOf(diagnostics, 3); + assert.instanceOf(diagnostics[0], tr.v.d.GenericSet); + assert.deepEqual(Array.from(diagnostics[0]), [...generic, ...generic2]); + assert.instanceOf(diagnostics[2], tr.v.d.CollectedRelatedEventSet); + }); + + test('validateDiagnosticTypes', function() { + const hist = new tr.v.Histogram('', tr.b.Unit.byName.count); + function addInvalidDiagnosticType() { + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.TRACE_START, new tr.v.d.GenericSet(['foo'])); + } + assert.throw(addInvalidDiagnosticType, Error, + `Diagnostics named "${tr.v.d.RESERVED_NAMES.TRACE_START}" must be ` + + 'DateRange, not GenericSet'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html new file mode 100644 index 00000000000..ac30d88cd65 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html @@ -0,0 +1,39 @@ +<!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/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + /** + * This is a placeholder to allow many DiagnosticMaps to contain references to + * the same Diagnostic. + */ + class DiagnosticRef { + /** + * @param {string} guid + */ + constructor(guid) { + this.guid = guid; + } + + asDict() { + return this.guid; + } + + asDictOrReference() { + return this.asDict(); + } + } + + return { + DiagnosticRef, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py new file mode 100644 index 00000000000..fe86099af13 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py @@ -0,0 +1,22 @@ +# Copyright 2017 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. + + +class DiagnosticRef(object): + def __init__(self, guid): + self._guid = guid + + @property + def guid(self): + return self._guid + + @property + def has_guid(self): + return True + + def AsDict(self): + return self.guid + + def AsDictOrReference(self): + return self.AsDict() diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html new file mode 100644 index 00000000000..55a49aa65b6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/all_diagnostics.html"> +<link rel="import" href="/tracing/value/diagnostics/generic_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('roundtripPreservesGuid', function() { + const diagnostic = new tr.v.d.GenericSet(['generic']); + diagnostic.guid = 'foo'; + const clone = tr.v.d.Diagnostic.fromDict(diagnostic.asDict()); + assert.strictEqual('foo', clone.guid); + }); + + test('equalitySmokeTest', function() { + const infos = tr.v.d.Diagnostic.getAllRegisteredTypeInfos(); + + for (const info of infos) { + assert.hasOwnProperty(info, 'equals'); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py new file mode 100644 index 00000000000..f3c1a1f9b64 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py @@ -0,0 +1,15 @@ +# Copyright 2018 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. + +import unittest + +from tracing.value.diagnostics import all_diagnostics + + +class DiagnosticUnittest(unittest.TestCase): + + def testEqualityForSmoke(self): + for name in all_diagnostics.GetDiagnosticTypenames(): + ctor = all_diagnostics.GetDiagnosticClassForName(name) + self.assertTrue(hasattr(ctor, '__eq__')) diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html new file mode 100644 index 00000000000..293a5086508 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; +/* eslint-disable no-console */ + +function isDiagnosticSubclass(cls) { + cls = cls.__proto__; + while (cls) { + if (cls === tr.v.d.Diagnostic) return true; + cls = cls.__proto__; + } + return false; +} + +function discoverDiagnostics(args) { + const discoveryMode = args.shift(); + for (const arg of args) HTMLImportsLoader.loadHTML(arg); + + const results = []; + if (discoveryMode === 'registry') { + for (const typeInfo of tr.v.d.Diagnostic.getAllRegisteredTypeInfos()) { + results.push(typeInfo.constructor.name); + } + } else if (discoveryMode === 'namespace') { + for (const cls of Object.values(tr.v.d)) { + if (isDiagnosticSubclass(cls)) results.push(cls.name); + } + } else { + console.log('First argument must be either "registry" or "namespace".'); + return 1; + } + console.log(JSON.stringify(results)); + return 0; +} + +if (tr.isHeadless) { + quit(discoverDiagnostics(sys.argv.slice(1))); +} +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html new file mode 100644 index 00000000000..d582272f0e6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html @@ -0,0 +1,43 @@ +<!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/guid.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + /** + * This is a placeholder in case the referenced Event isn't available in + * memory to point to directly. + */ + class EventRef { + /** + * @param {!Object} event + * @param {string} event.stableId + * @param {string} event.title + * @param {number} event.start + * @param {number} event.duration + */ + constructor(event) { + this.stableId = event.stableId; + this.title = event.title; + this.start = event.start; + this.duration = event.duration; + this.end = this.start + this.duration; + + // tr.v.d.RelatedEventSet identifies events using stableId, but + // tr.model.EventSet uses guid. + this.guid = tr.b.GUID.allocateSimple(); + } + } + + return { + EventRef, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html new file mode 100644 index 00000000000..e0c0664b958 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html @@ -0,0 +1,145 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + /** + * Stringify Arrays or dictionaries. Sorts dictionaries keys. Non-recursive. + * + * @param {!Object} obj + * @return {string} + */ + function stableStringify(obj) { + let replacer; + if (!(obj instanceof Array) && obj !== null) { + replacer = Object.keys(obj).sort(); + } + return JSON.stringify(obj, replacer); + } + + /** + * @typedef {(null|number|string|boolean|Array.<!PlainOldData>|!Object)} + * PlainOldData + */ + + class GenericSet extends tr.v.d.Diagnostic { + /** + * @param {!Iterable.<!PlainOldData>} values + */ + constructor(values) { + super(); + + if (typeof values[Symbol.iterator] !== 'function') { + throw new Error('GenericSet must be constructed from an interable.'); + } + + this.values_ = new Set(values); + this.has_objects_ = false; + + for (const value of values) { + if (typeof value === 'object') { + this.has_objects_ = true; + } + } + } + + get size() { + return this.values_.size; + } + + get length() { + return this.values_.size; + } + + * [Symbol.iterator]() { + for (const value of this.values_) { + yield value; + } + } + + has(value) { + if (typeof value !== 'object') return this.values_.has(value); + const json = JSON.stringify(value); + for (const x of this) { + if (typeof x !== 'object') continue; + if (json === JSON.stringify(x)) return true; + } + return false; + } + + equals(other) { + if (!(other instanceof GenericSet)) return false; + if (this.size !== other.size) return false; + for (const value of this) { + if (!other.has(value)) return false; + } + return true; + } + + get hashKey() { + if (this.has_objects_) return undefined; + + if (this.hash_key_ !== undefined) { + return this.hash_key_; + } + + let key = ''; + for (const value of Array.from(this.values_.values()).sort()) { + key += value; + } + this.hash_key_ = key; + return key; + } + + asDictInto_(d) { + d.values = Array.from(this); + } + + static fromDict(d) { + return new GenericSet(d.values); + } + + clone() { + return new GenericSet(this.values_); + } + + canAddDiagnostic(otherDiagnostic) { + return otherDiagnostic instanceof GenericSet; + } + + addDiagnostic(otherDiagnostic) { + const jsons = new Set(); + for (const value of this) { + if (typeof value !== 'object') continue; + jsons.add(stableStringify(value)); + } + + for (const value of otherDiagnostic) { + if (typeof value === 'object') { + if (jsons.has(stableStringify(value))) { + continue; + } + this.has_objects_ = true; + } + this.values_.add(value); + } + } + } + + tr.v.d.Diagnostic.register(GenericSet, { + elementName: 'tr-v-ui-generic-set-span' + }); + + return { + GenericSet, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py new file mode 100644 index 00000000000..e4a2b19f4c7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py @@ -0,0 +1,82 @@ +# Copyright 2018 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. + +import json + +from tracing.value.diagnostics import diagnostic + + +class GenericSet(diagnostic.Diagnostic): + """Contains any Plain-Ol'-Data objects. + + Contents are serialized using json.dumps(): None, boolean, number, string, + list, dict. Dicts, lists, and booleans are deduplicated by their JSON + representation. Dicts and lists are not hashable. (1 == True) and (0 == + False) in Python, but not in JSON. + """ + __slots__ = '_values', '_comparable_set' + + def __init__(self, values): + super(GenericSet, self).__init__() + + self._values = list(values) + self._comparable_set = None + + def __iter__(self): + for value in self._values: + yield value + + def __len__(self): + return len(self._values) + + def __eq__(self, other): + return self._GetComparableSet() == other._GetComparableSet() + + def __hash__(self): + return id(self) + + def SetValues(self, values): + # Use a list because Python sets cannot store dicts or lists because they + # are not hashable. + self._values = list(values) + + # Cache a set to facilitate comparing and merging GenericSets. + # Dicts, lists, and booleans are serialized; other types are not. + self._comparable_set = None + + def _GetComparableSet(self): + if self._comparable_set is None: + self._comparable_set = set() + for value in self: + if isinstance(value, (dict, list, bool)): + self._comparable_set.add(json.dumps(value, sort_keys=True)) + else: + self._comparable_set.add(value) + return self._comparable_set + + def CanAddDiagnostic(self, other_diagnostic): + return isinstance(other_diagnostic, GenericSet) + + def AddDiagnostic(self, other_diagnostic): + comparable_set = self._GetComparableSet() + for value in other_diagnostic: + if isinstance(value, (dict, list, bool)): + json_value = json.dumps(value, sort_keys=True) + if json_value not in comparable_set: + self._values.append(value) + self._comparable_set.add(json_value) + elif value not in comparable_set: + self._values.append(value) + self._comparable_set.add(value) + + def _AsDictInto(self, dct): + dct['values'] = list(self) + + @staticmethod + def FromDict(dct): + return GenericSet(dct['values']) + + def GetOnlyElement(self): + assert len(self) == 1 + return self._values[0] diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html new file mode 100644 index 00000000000..8ce850e220b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/generic_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('merge', function() { + const a = new tr.v.d.GenericSet(['a']); + const b = new tr.v.d.GenericSet(['b']); + + assert.isTrue(a.canAddDiagnostic(b)); + assert.isTrue(b.canAddDiagnostic(a)); + + const ab = a.clone(); + ab.addDiagnostic(b); + assert.deepEqual(Array.from(ab), ['a', 'b']); + + const bab = b.clone(); + bab.addDiagnostic(ab); + assert.deepEqual(Array.from(bab), ['b', 'a']); + }); + + test('mergeDictionaries', function() { + const a = new tr.v.d.GenericSet([{a: 1, b: 2}]); + const b = new tr.v.d.GenericSet([{b: 2, a: 1}]); + const ab = a.clone(); + assert.strictEqual(tr.b.getOnlyElement(a), tr.b.getOnlyElement(ab)); + ab.addDiagnostic(b); + assert.lengthOf(ab, 1); + assert.strictEqual(tr.b.getOnlyElement(a), tr.b.getOnlyElement(ab)); + }); + + test('addDiagnosticWithNull', function() { + const a = new tr.v.d.GenericSet([]); + const b = new tr.v.d.GenericSet([null]); + a.addDiagnostic(b); + assert.lengthOf(a, 1); + assert.isTrue(a.has(null)); + }); + + test('hashKey', function() { + const a = new tr.v.d.GenericSet(['a', 'b']); + assert.strictEqual(a.hashKey, 'ab'); + }); + + test('setsWithoutObjectsSupportFastPath', function() { + const a = new tr.v.d.GenericSet(['a', 'b']); + assert.isDefined(a.hashKey); + }); + + test('setsWithObjectsDoNotSupportFastPath', function() { + const a = new tr.v.d.GenericSet([{foo: 'bar'}]); + assert.isUndefined(a.hashKey); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py new file mode 100644 index 00000000000..00ba42e10a0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py @@ -0,0 +1,100 @@ +# Copyright 2018 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. + +import unittest + +from tracing.value.diagnostics import diagnostic +from tracing.value.diagnostics import generic_set + + +class GenericSetUnittest(unittest.TestCase): + + def testRoundtrip(self): + a_set = generic_set.GenericSet([ + None, + True, + False, + 0, + 1, + 42, + [], + {}, + [0, False], + {'a': 1, 'b': True}, + ]) + self.assertEqual(a_set, diagnostic.Diagnostic.FromDict(a_set.AsDict())) + + def testEq(self): + a_set = generic_set.GenericSet([ + None, + True, + False, + 0, + 1, + 42, + [], + {}, + [0, False], + {'a': 1, 'b': True}, + ]) + b_set = generic_set.GenericSet([ + {'b': True, 'a': 1}, + [0, False], + {}, + [], + 42, + 1, + 0, + False, + True, + None, + ]) + self.assertEqual(a_set, b_set) + + def testMerge(self): + a_set = generic_set.GenericSet([ + None, + True, + False, + 0, + 1, + 42, + [], + {}, + [0, False], + {'a': 1, 'b': True}, + ]) + b_set = generic_set.GenericSet([ + {'b': True, 'a': 1}, + [0, False], + {}, + [], + 42, + 1, + 0, + False, + True, + None, + ]) + self.assertTrue(a_set.CanAddDiagnostic(b_set)) + self.assertTrue(b_set.CanAddDiagnostic(a_set)) + a_set.AddDiagnostic(b_set) + self.assertEqual(a_set, b_set) + b_set.AddDiagnostic(a_set) + self.assertEqual(a_set, b_set) + + c_dict = {'a': 1, 'b': 1} + c_set = generic_set.GenericSet([c_dict]) + a_set.AddDiagnostic(c_set) + self.assertEqual(len(a_set), 1 + len(b_set)) + self.assertIn(c_dict, a_set) + + def testGetOnlyElement(self): + gs = generic_set.GenericSet(['foo']) + self.assertEqual(gs.GetOnlyElement(), 'foo') + + def testGetOnlyElementRaises(self): + gs = generic_set.GenericSet([]) + with self.assertRaises(AssertionError): + gs.GetOnlyElement() diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html new file mode 100644 index 00000000000..8021e661159 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html @@ -0,0 +1,129 @@ +<!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/model/event_set.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic.html"> +<link rel="import" href="/tracing/value/diagnostics/event_ref.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + /** + * @typedef {!(tr.v.d.EventRef|tr.model.Event)} EventLike + */ + + /** + * A RelatedEventSet diagnostic contains references to Events + */ + class RelatedEventSet extends tr.v.d.Diagnostic { + /** + * @param {!(tr.model.EventSet|Array.<EventLike>|EventLike)=} opt_events + */ + constructor(opt_events) { + super(); + this.eventsByStableId_ = new Map(); + // TODO(#2431) Plumb canonicalUrl from event.model. + this.canonicalUrl_ = undefined; + + if (opt_events) { + if (opt_events instanceof tr.model.EventSet || + opt_events instanceof Array) { + for (const event of opt_events) { + this.add(event); + } + } else { + this.add(opt_events); + } + } + } + + clone() { + const clone = new tr.v.d.CollectedRelatedEventSet(); + clone.addDiagnostic(this); + return clone; + } + + /** + * @param {!(tr.v.d.EventRef|tr.model.Event)} event + */ + add(event) { + this.eventsByStableId_.set(event.stableId, event); + } + + /** + * @param {!(tr.v.d.EventRef|tr.model.Event)} event + * @return {boolean} + */ + has(event) { + return this.eventsByStableId_.has(event.stableId); + } + + get length() { + return this.eventsByStableId_.size; + } + + * [Symbol.iterator]() { + for (const event of this.eventsByStableId_.values()) { + yield event; + } + } + + get canonicalUrl() { + return this.canonicalUrl_; + } + + /** + * Resolve all EventRefs into Events by finding their stableIds in |model|. + * If a stableId cannot be found and |opt_required| is true, then throw an + * Error. + * If a stableId cannot be found and |opt_required| is false, then the + * EventRef will remain an EventRef. + * + * @param {!tr.model.Model} model + * @param {boolean=} opt_required + */ + resolve(model, opt_required) { + for (const [stableId, value] of this.eventsByStableId_) { + if (!(value instanceof tr.v.d.EventRef)) continue; + + const event = model.getEventByStableId(stableId); + if (event instanceof tr.model.Event) { + this.eventsByStableId_.set(stableId, event); + } else if (opt_required) { + throw new Error('Unable to find Event ' + stableId); + } + } + } + + asDictInto_(d) { + d.events = []; + for (const event of this) { + d.events.push({ + stableId: event.stableId, + title: event.title, + start: tr.b.Unit.byName.timeStampInMs.truncate(event.start), + duration: tr.b.Unit.byName.timeDurationInMs.truncate(event.duration), + }); + } + } + + static fromDict(d) { + return new RelatedEventSet(d.events.map( + event => new tr.v.d.EventRef(event))); + } + } + + tr.v.d.Diagnostic.register(RelatedEventSet, { + elementName: 'tr-v-ui-related-event-set-span' + }); + + return { + RelatedEventSet, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py new file mode 100644 index 00000000000..f4bbad3c596 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py @@ -0,0 +1,34 @@ +# Copyright 2018 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. + + +from tracing.value.diagnostics import diagnostic + + +class RelatedEventSet(diagnostic.Diagnostic): + __slots__ = '_events_by_stable_id', + + def __init__(self): + super(RelatedEventSet, self).__init__() + self._events_by_stable_id = {} + + def Add(self, event): + self._events_by_stable_id[event['stableId']] = event + + def __len__(self): + return len(self._events_by_stable_id) + + def __iter__(self): + for event in self._events_by_stable_id.values(): + yield event + + @staticmethod + def FromDict(d): + result = RelatedEventSet() + for event in d['events']: + result.Add(event) + return result + + def _AsDictInto(self, d): + d['events'] = [event for event in self] diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html new file mode 100644 index 00000000000..8019f67172f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html @@ -0,0 +1,92 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('eventSet', function() { + let slice = undefined; + const model = tr.c.TestUtils.newModel(function(model) { + slice = tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + title: 'foo', + start: 0, + duration: 10 + }); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + thread.sliceGroup.pushSlice(slice); + }); + + let d = new tr.v.d.RelatedEventSet(slice); + assert.strictEqual(tr.b.getOnlyElement([...d]), slice); + + d = new tr.v.d.RelatedEventSet([slice]); + assert.strictEqual(tr.b.getOnlyElement([...d]), slice); + + d = new tr.v.d.RelatedEventSet(new tr.model.EventSet([slice])); + assert.strictEqual(tr.b.getOnlyElement([...d]), slice); + + const d2 = tr.v.d.Diagnostic.fromDict(d.asDict()); + assert.instanceOf(d2, tr.v.d.RelatedEventSet); + + assert.instanceOf(tr.b.getOnlyElement([...d2]), tr.v.d.EventRef); + + d2.resolve(model, true); + + assert.strictEqual(tr.b.getOnlyElement([...d2]), slice); + }); + + test('merge', function() { + let aSlice; + let bSlice; + const model = tr.c.TestUtils.newModel(function(model) { + aSlice = tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + title: 'a', + start: 0, + duration: 10 + }); + bSlice = tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + title: 'b', + start: 1, + duration: 10 + }); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + thread.sliceGroup.pushSlice(aSlice); + thread.sliceGroup.pushSlice(bSlice); + }); + assert.notEqual(aSlice.stableId, bSlice.stableId); + + const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.count); + const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.count); + + const aEvents = new tr.v.d.RelatedEventSet(aSlice); + const bEvents = new tr.v.d.RelatedEventSet(bSlice); + aEvents.canonicalUrl_ = 'http://a'; + bEvents.canonicalUrl_ = 'http://b'; + + aHist.diagnostics.set('events', aEvents); + bHist.diagnostics.set('events', bEvents); + + let mergedHist = aHist.clone(); + mergedHist.addHistogram(bHist); + mergedHist = tr.v.Histogram.fromDict(mergedHist.asDict()); + + const mergedEvents = mergedHist.diagnostics.get('events'); + const aSlice2 = tr.b.getOnlyElement(mergedEvents.get('http://a')); + assert.strictEqual(aSlice.stableId, aSlice2.stableId); + const bSlice2 = tr.b.getOnlyElement(mergedEvents.get('http://b')); + assert.strictEqual(bSlice.stableId, bSlice2.stableId); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py new file mode 100644 index 00000000000..b0a55e090a0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py @@ -0,0 +1,29 @@ +# Copyright 2018 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. +import unittest + +from tracing.value import histogram_unittest +from tracing.value.diagnostics import related_event_set +from tracing.value.diagnostics import diagnostic + + +class RelatedEventSetUnittest(unittest.TestCase): + def testRoundtrip(self): + events = related_event_set.RelatedEventSet() + events.Add({ + 'stableId': '0.0', + 'title': 'foo', + 'start': 0, + 'duration': 1, + }) + d = events.AsDict() + clone = diagnostic.Diagnostic.FromDict(d) + self.assertEqual( + histogram_unittest.ToJSON(d), histogram_unittest.ToJSON(clone.AsDict())) + self.assertEqual(len(events), 1) + event = list(events)[0] + self.assertEqual(event['stableId'], '0.0') + self.assertEqual(event['title'], 'foo') + self.assertEqual(event['start'], 0) + self.assertEqual(event['duration'], 1) diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html new file mode 100644 index 00000000000..3004d6bf3e5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/utils.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + class RelatedNameMap extends tr.v.d.Diagnostic { + constructor(opt_info) { + super(); + this.map_ = new Map(); + } + + clone() { + const clone = new RelatedNameMap(); + clone.addDiagnostic(this); + return clone; + } + + equals(other) { + if (!(other instanceof RelatedNameMap)) return false; + + const keys1 = new Set(this.map_.keys()); + const keys2 = new Set(other.map_.keys()); + if (!tr.b.setsEqual(keys1, keys2)) return false; + + for (const [key, name] of this) { + if (name !== other.get(key)) return false; + } + + return true; + } + + canAddDiagnostic(otherDiagnostic) { + return otherDiagnostic instanceof RelatedNameMap; + } + + addDiagnostic(otherDiagnostic) { + for (const [key, name] of otherDiagnostic) { + const existing = this.get(key); + if (existing === undefined) { + this.set(key, name); + } else if (existing !== name) { + throw new Error('Histogram names differ: ' + + `"${existing}" != "${name}"`); + } + } + } + + asDictInto_(d) { + d.names = {}; + for (const [key, name] of this) d.names[key] = name; + } + + set(key, name) { + this.map_.set(key, name); + } + + get(key) { + return this.map_.get(key); + } + + * [Symbol.iterator]() { + for (const pair of this.map_) yield pair; + } + + * values() { + for (const value of this.map_.values()) yield value; + } + + static fromEntries(entries) { + const names = new RelatedNameMap(); + for (const [key, name] of entries) { + names.set(key, name); + } + return names; + } + + static fromDict(d) { + return RelatedNameMap.fromEntries(Object.entries(d.names || {})); + } + } + + tr.v.d.Diagnostic.register(RelatedNameMap, { + elementName: 'tr-v-ui-related-name-map-span', + }); + + return { + RelatedNameMap, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py new file mode 100644 index 00000000000..c2584fa1b60 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py @@ -0,0 +1,64 @@ +# Copyright 2018 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. + +from tracing.value.diagnostics import diagnostic + + +class RelatedNameMap(diagnostic.Diagnostic): + __slots__ = '_map', + + def __init__(self): + super(RelatedNameMap, self).__init__() + self._map = {} + + def __len__(self): + return len(self._map) + + def __eq__(self, other): + if not isinstance(other, RelatedNameMap): + return False + if set(self._map) != set(other._map): + return False + for key, name in self._map.items(): + if name != other.Get(key): + return False + return True + + def __hash__(self): + return id(self) + + def CanAddDiagnostic(self, other): + return isinstance(other, RelatedNameMap) + + def AddDiagnostic(self, other): + for key, name in other._map.items(): + existing = self.Get(key) + if existing is None: + self.Set(key, name) + elif existing != name: + raise ValueError('Histogram names differ: "%s" != "%s"' % ( + existing, name)) + + def Get(self, key): + return self._map.get(key) + + def Set(self, key, name): + self._map[key] = name + + def __iter__(self): + for key, name in self._map.items(): + yield key, name + + def Values(self): + return self._map.values() + + def _AsDictInto(self, dct): + dct['names'] = dict(self._map) + + @staticmethod + def FromDict(dct): + names = RelatedNameMap() + for key, name in dct['names'].items(): + names.Set(key, name) + return names diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py new file mode 100644 index 00000000000..13fae26d20e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py @@ -0,0 +1,50 @@ +# Copyright 2018 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. + +import unittest + +from tracing.value import histogram_unittest +from tracing.value.diagnostics import diagnostic +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import related_name_map + + +class RelatedNameMapUnittest(unittest.TestCase): + def testRoundtrip(self): + names = related_name_map.RelatedNameMap() + names.Set('a', 'A') + d = names.AsDict() + clone = diagnostic.Diagnostic.FromDict(d) + self.assertEqual( + histogram_unittest.ToJSON(d), histogram_unittest.ToJSON(clone.AsDict())) + self.assertEqual(clone.Get('a'), 'A') + + def testMerge(self): + a_names = related_name_map.RelatedNameMap() + a_names.Set('a', 'A') + b_names = related_name_map.RelatedNameMap() + b_names.Set('b', 'B') + self.assertTrue(a_names.CanAddDiagnostic(b_names)) + self.assertTrue(b_names.CanAddDiagnostic(a_names)) + self.assertFalse(a_names.CanAddDiagnostic(generic_set.GenericSet([]))) + + a_names.AddDiagnostic(b_names) + self.assertEqual(a_names.Get('b'), 'B') + a_names.AddDiagnostic(b_names) + self.assertEqual(a_names.Get('b'), 'B') + + b_names.Set('a', 'C') + with self.assertRaises(ValueError): + a_names.AddDiagnostic(b_names) + + def testEquals(self): + a_names = related_name_map.RelatedNameMap() + a_names.Set('a', 'A') + self.assertNotEqual(a_names, generic_set.GenericSet([])) + b_names = related_name_map.RelatedNameMap() + self.assertNotEqual(a_names, b_names) + b_names.Set('a', 'B') + self.assertNotEqual(a_names, b_names) + b_names.Set('a', 'A') + self.assertEqual(a_names, b_names) diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py new file mode 100644 index 00000000000..54615b7a6fb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py @@ -0,0 +1,92 @@ +# Copyright 2017 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. + +class _Info(object): + + def __init__(self, name, _type=None, entry_type=None): + self._name = name + self._type = _type + if entry_type is not None and self._type != 'GenericSet': + raise ValueError( + 'entry_type should only be specified if _type is GenericSet') + self._entry_type = entry_type + + @property + def name(self): + return self._name + + @property + def type(self): + return self._type + + @property + def entry_type(self): + return self._entry_type + + +ANGLE_REVISIONS = _Info('angleRevisions', 'GenericSet', str) +ARCHITECTURES = _Info('architectures', 'GenericSet', str) +BENCHMARKS = _Info('benchmarks', 'GenericSet', str) +BENCHMARK_START = _Info('benchmarkStart', 'DateRange') +BENCHMARK_DESCRIPTIONS = _Info('benchmarkDescriptions', 'GenericSet', str) +BOTS = _Info('bots', 'GenericSet', str) +BUG_COMPONENTS = _Info('bugComponents', 'GenericSet', str) +BUILD_URLS = _Info('buildUrls', 'GenericSet', str) +BUILDS = _Info('builds', 'GenericSet', int) +CATAPULT_REVISIONS = _Info('catapultRevisions', 'GenericSet', str) +CHROMIUM_COMMIT_POSITIONS = _Info('chromiumCommitPositions', 'GenericSet', int) +CHROMIUM_REVISIONS = _Info('chromiumRevisions', 'GenericSet', str) +DEVICE_IDS = _Info('deviceIds', 'GenericSet', str) +DOCUMENTATION_URLS = _Info('documentationLinks', 'GenericSet', str) +FUCHSIA_GARNET_REVISIONS = _Info('fuchsiaGarnetRevisions', 'GenericSet', str) +FUCHSIA_PERIDOT_REVISIONS = _Info('fuchsiaPeridotRevisions', 'GenericSet', str) +FUCHSIA_TOPAZ_REVISIONS = _Info('fuchsiaTopazRevisions', 'GenericSet', str) +FUCHSIA_ZIRCON_REVISIONS = _Info('fuchsiaZirconRevisions', 'GenericSet', str) +GPUS = _Info('gpus', 'GenericSet', str) +HAD_FAILURES = _Info('hadFailures', 'GenericSet', bool) +IS_REFERENCE_BUILD = _Info('isReferenceBuild', 'GenericSet', bool) +LABELS = _Info('labels', 'GenericSet', str) +LOG_URLS = _Info('logUrls', 'GenericSet', str) +MASTERS = _Info('masters', 'GenericSet', str) +MEMORY_AMOUNTS = _Info('memoryAmounts', 'GenericSet', int) +OS_NAMES = _Info('osNames', 'GenericSet', str) +OS_VERSIONS = _Info('osVersions', 'GenericSet', str) +OWNERS = _Info('owners', 'GenericSet', str) +POINT_ID = _Info('pointId', 'GenericSet', int) +PRODUCT_VERSIONS = _Info('productVersions', 'GenericSet', str) +REVISION_TIMESTAMPS = _Info('revisionTimestamps', 'DateRange') +SKIA_REVISIONS = _Info('skiaRevisions', 'GenericSet', str) +STORIES = _Info('stories', 'GenericSet', str) +STORYSET_REPEATS = _Info('storysetRepeats', 'GenericSet', int) +STORY_TAGS = _Info('storyTags', 'GenericSet', str) +SUMMARY_KEYS = _Info('summaryKeys', 'GenericSet', str) +TEST_PATH = _Info('testPath', 'GenericSet', str) +TRACE_START = _Info('traceStart', 'DateRange') +TRACE_URLS = _Info('traceUrls', 'GenericSet', str) +V8_COMMIT_POSITIONS = _Info('v8CommitPositions', 'DateRange') +V8_REVISIONS = _Info('v8Revisions', 'GenericSet', str) +WEBRTC_REVISIONS = _Info('webrtcRevisions', 'GenericSet', str) + + +def _CreateCachedInfoTypes(): + info_types = {} + for info in globals().values(): + if isinstance(info, _Info): + info_types[info.name] = info + return info_types + +_CACHED_INFO_TYPES = _CreateCachedInfoTypes() + +def GetTypeForName(name): + info = _CACHED_INFO_TYPES.get(name) + if info: + return info.type + +def AllInfos(): + for info in _CACHED_INFO_TYPES.values(): + yield info + +def AllNames(): + for info in AllInfos(): + yield info.name diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html new file mode 100644 index 00000000000..1321815f0c5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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. +--> + +<!-- + Include all Diagnostic subclasses here so that RESERVED_INFOS always has + access to all subclasses. +--> +<link rel="import" href="/tracing/value/diagnostics/all_diagnostics.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.d', function() { + // Diagnostics that are produced outside of metrics (e.g. by telemetry) use + // reserved names. + const RESERVED_INFOS = { + ANGLE_REVISIONS: {name: 'angleRevisions', type: tr.v.d.GenericSet}, + ARCHITECTURES: {name: 'architectures', type: tr.v.d.GenericSet}, + BENCHMARKS: {name: 'benchmarks', type: tr.v.d.GenericSet}, + BENCHMARK_START: {name: 'benchmarkStart', type: tr.v.d.DateRange}, + BENCHMARK_DESCRIPTIONS: {name: 'benchmarkDescriptions', + type: tr.v.d.GenericSet}, + BOTS: {name: 'bots', type: tr.v.d.GenericSet}, + BUG_COMPONENTS: {name: 'bugComponents', type: tr.v.d.GenericSet}, + BUILDS: {name: 'builds', type: tr.v.d.GenericSet}, + CATAPULT_REVISIONS: {name: 'catapultRevisions', type: tr.v.d.GenericSet}, + CHROMIUM_COMMIT_POSITIONS: { + name: 'chromiumCommitPositions', type: tr.v.d.GenericSet}, + CHROMIUM_REVISIONS: {name: 'chromiumRevisions', type: tr.v.d.GenericSet}, + DEVICE_IDS: {name: 'deviceIds', type: tr.v.d.GenericSet}, + DOCUMENTATION_URLS: {name: 'documentationUrls', type: tr.v.d.GenericSet}, + FUCHSIA_GARNET_REVISIONS: { + name: 'fuchsiaGarnetRevisions', type: tr.v.d.GenericSet}, + FUCHSIA_PERIDOT_REVISIONS: { + name: 'fuchsiaPeridotRevisions', type: tr.v.d.GenericSet}, + FUCHSIA_TOPAZ_REVISIONS: { + name: 'fuchsiaTopazRevisions', type: tr.v.d.GenericSet}, + FUCHSIA_ZIRCON_REVISIONS: { + name: 'fuchsiaZirconRevisions', type: tr.v.d.GenericSet}, + GPUS: {name: 'gpus', type: tr.v.d.GenericSet}, + IS_REFERENCE_BUILD: {name: 'isReferenceBuild', type: tr.v.d.GenericSet}, + LABELS: {name: 'labels', type: tr.v.d.GenericSet}, + LOG_URLS: {name: 'logUrls', type: tr.v.d.GenericSet}, + MASTERS: {name: 'masters', type: tr.v.d.GenericSet}, + MEMORY_AMOUNTS: {name: 'memoryAmounts', type: tr.v.d.GenericSet}, + OS_NAMES: {name: 'osNames', type: tr.v.d.GenericSet}, + OS_VERSIONS: {name: 'osVersions', type: tr.v.d.GenericSet}, + OWNERS: {name: 'owners', type: tr.v.d.GenericSet}, + POINT_ID: {name: 'pointId', type: tr.v.d.GenericSet}, + PRODUCT_VERSIONS: {name: 'productVersions', type: tr.v.d.GenericSet}, + REVISION_TIMESTAMPS: {name: 'revisionTimestamps', type: tr.v.d.DateRange}, + SKIA_REVISIONS: {name: 'skiaRevisions', type: tr.v.d.GenericSet}, + STORIES: {name: 'stories', type: tr.v.d.GenericSet}, + STORYSET_REPEATS: {name: 'storysetRepeats', type: tr.v.d.GenericSet}, + STORY_TAGS: {name: 'storyTags', type: tr.v.d.GenericSet}, + SUMMARY_KEYS: {name: 'summaryKeys', type: tr.v.d.GenericSet}, + TEST_PATH: {name: 'testPath', type: tr.v.d.GenericSet}, + TRACE_START: {name: 'traceStart', type: tr.v.d.DateRange}, + TRACE_URLS: {name: 'traceUrls', type: tr.v.d.GenericSet}, + V8_COMMIT_POSITIONS: {name: 'v8CommitPositions', type: tr.v.d.DateRange}, + V8_REVISIONS: {name: 'v8Revisions', type: tr.v.d.GenericSet}, + WEBRTC_REVISIONS: {name: 'webrtcRevisions', type: tr.v.d.GenericSet}, + }; + + const RESERVED_NAMES = {}; + + const RESERVED_NAMES_TO_TYPES = new Map(); + + for (const [codename, info] of Object.entries(RESERVED_INFOS)) { + RESERVED_NAMES[codename] = info.name; + if (RESERVED_NAMES_TO_TYPES.has(info.name)) { + throw new Error(`Duplicate reserved name "${info.name}"`); + } + RESERVED_NAMES_TO_TYPES.set(info.name, info.type); + } + + const RESERVED_NAMES_SET = new Set(Object.values(RESERVED_NAMES)); + + return { + RESERVED_INFOS, + RESERVED_NAMES, + RESERVED_NAMES_SET, + RESERVED_NAMES_TO_TYPES, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html new file mode 100644 index 00000000000..397ec7d31b5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html @@ -0,0 +1,48 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + class Scalar extends tr.v.d.Diagnostic { + /** + * @param {!tr.b.Scalar} value + */ + constructor(value) { + super(); + if (!(value instanceof tr.b.Scalar)) { + throw new Error('expected Scalar'); + } + this.value = value; + } + + clone() { + return new Scalar(this.value); + } + + asDictInto_(d) { + d.value = this.value.asDict(); + } + + static fromDict(d) { + return new Scalar(tr.b.Scalar.fromDict(d.value)); + } + } + + tr.v.d.Diagnostic.register(Scalar, { + elementName: 'tr-v-ui-scalar-diagnostic-span' + }); + + return { + Scalar, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html new file mode 100644 index 00000000000..3bffc75caf0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/diagnostics/diagnostic.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v.d', function() { + class UnmergeableDiagnosticSet extends tr.v.d.Diagnostic { + /** + * @param {!Array.<!tr.v.d.Diagnostic>} diagnostics + */ + constructor(diagnostics) { + super(); + this._diagnostics = diagnostics; + } + + clone() { + const clone = new tr.v.d.UnmergeableDiagnosticSet(); + clone.addDiagnostic(this); + return clone; + } + + canAddDiagnostic(otherDiagnostic) { + return true; + } + + /** + * If |otherDiagnostic| is an UnmergeableDiagnosticSet, then add clones of + * its diagnostics to |this|. Otherwise, try to add |otherDiagnostic| to one + * of the diagnostics already in this set. If that fails, add a clone of + * |otherDiagnostic| to this set. + * + * @param {!tr.v.d.Diagnostic} otherDiagnostic + * @return {!tr.v.d.UnmergeableDiagnostic} this + */ + addDiagnostic(otherDiagnostic) { + if (otherDiagnostic instanceof UnmergeableDiagnosticSet) { + for (const subOtherDiagnostic of otherDiagnostic) { + const clone = subOtherDiagnostic.clone(); + this.addDiagnostic(clone); + } + return; + } + + for (let i = 0; i < this._diagnostics.length; ++i) { + if (this._diagnostics[i].canAddDiagnostic(otherDiagnostic)) { + this._diagnostics[i].addDiagnostic(otherDiagnostic); + return; + } + } + + const clone = otherDiagnostic.clone(); + this._diagnostics.push(clone); + } + + get length() { + return this._diagnostics.length; + } + + * [Symbol.iterator]() { + for (const diagnostic of this._diagnostics) yield diagnostic; + } + + asDictInto_(d) { + d.diagnostics = this._diagnostics.map(d => d.asDictOrReference()); + } + + static fromDict(d) { + return new UnmergeableDiagnosticSet(d.diagnostics.map( + d => ((typeof d === 'string') ? + new tr.v.d.DiagnosticRef(d) : tr.v.d.Diagnostic.fromDict(d)))); + } + } + + tr.v.d.Diagnostic.register(UnmergeableDiagnosticSet, { + elementName: 'tr-v-ui-unmergeable-diagnostic-set-span' + }); + + return { + UnmergeableDiagnosticSet, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py new file mode 100644 index 00000000000..5387c3b9614 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py @@ -0,0 +1,52 @@ +# Copyright 2018 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. + +from tracing.value.diagnostics import diagnostic +from tracing.value.diagnostics import diagnostic_ref + +try: + StringTypes = basestring +except NameError: + StringTypes = str + + +class UnmergeableDiagnosticSet(diagnostic.Diagnostic): + __slots__ = '_diagnostics', + + def __init__(self, diagnostics): + super(UnmergeableDiagnosticSet, self).__init__() + self._diagnostics = diagnostics + + def __len__(self): + return len(self._diagnostics) + + def __iter__(self): + for diag in self._diagnostics: + yield diag + + def CanAddDiagnostic(self, unused_other_diagnostic): + return True + + def AddDiagnostic(self, other_diagnostic): + if isinstance(other_diagnostic, UnmergeableDiagnosticSet): + self._diagnostics.extend(other_diagnostic._diagnostics) + return + for diag in self: + if diag.CanAddDiagnostic(other_diagnostic): + diag.AddDiagnostic(other_diagnostic) + return + self._diagnostics.append(other_diagnostic) + + def _AsDictInto(self, d): + d['diagnostics'] = [d.AsDictOrReference() for d in self] + + @staticmethod + def FromDict(dct): + def RefOrDiagnostic(d): + if isinstance(d, StringTypes): + return diagnostic_ref.DiagnosticRef(d) + return diagnostic.Diagnostic.FromDict(d) + + return UnmergeableDiagnosticSet( + [RefOrDiagnostic(d) for d in dct['diagnostics']]) diff --git a/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py new file mode 100644 index 00000000000..82b0ce3ceb8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py @@ -0,0 +1,112 @@ +# Copyright 2018 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. + +import json + +from tracing.value import histogram +from tracing.value import histogram_set +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import reserved_infos + + +def ConvertGtestJson(gtest_json): + """Convert JSON from a gtest perf test to Histograms. + + Incoming data is in the following format: + { + 'metric1': { + 'units': 'unit1', + 'traces': { + 'story1': ['mean', 'std_dev'], + 'story2': ['mean', 'std_dev'], + }, + 'important': ['testcase1', 'testcase2'], + }, + 'metric2': { + 'units': 'unit2', + 'traces': { + 'story1': ['mean', 'std_dev'], + 'story2': ['mean', 'std_dev'], + }, + 'important': ['testcase1', 'testcase2'], + }, + ... + } + We ignore the 'important' fields and just assume everything should be + considered important. + + We also don't bother adding any reserved diagnostics like mastername in this + script since that should be handled by the upload script. + + Args: + gtest_json: A JSON dict containing perf output from a gtest + + Returns: + A HistogramSet containing equivalent histograms and diagnostics + """ + + hs = histogram_set.HistogramSet() + + for metric, metric_data in gtest_json.iteritems(): + # Maintain the same unit if we're able to find an exact match, converting + # time units if possible. Otherwise use 'unitless'. + unit, multiplier = _ConvertUnit(metric_data.get('units')) + + for story, story_data in metric_data['traces'].iteritems(): + # We should only ever have the mean and standard deviation here. + assert len(story_data) == 2 + h = histogram.Histogram(metric, unit) + h.diagnostics[reserved_infos.STORIES.name] = generic_set.GenericSet( + [story]) + mean = float(story_data[0]) * multiplier + std_dev = float(story_data[1]) * multiplier + h.AddSample(mean) + + # Synthesize the running statistics since we only have the mean + standard + # deviation instead of the actual data points. + h._running = histogram.RunningStatistics.FromDict([ + 2, # count, we need this to be >1 in order for variance to work + mean, # max + 0, # meanlogs + mean, # mean + mean, # min + 2 * mean, # sum, this must be count * mean otherwise the reported mean + # is incorrect after merging statistics when reserved + # diagnostics are added. + std_dev * std_dev, # variance + ]) + + hs.AddHistogram(h) + + return hs + +def ConvertGtestJsonFile(filepath): + """Convert JSON in a file from a gtest perf test to Histograms. + + Contents of the given file will be overwritten with the new Histograms data. + + Args: + filepath: The filepath to the JSON file to read/write from/to. + """ + with open(filepath, 'r') as f: + data = json.load(f) + histograms = ConvertGtestJson(data) + with open(filepath, 'w') as f: + json.dump(histograms.AsDicts(), f) + + +def _ConvertUnit(unit): + # We assume that smaller is better since we don't have an actual way to + # determine what the improvement direction is and most or all metrics from + # gtest perf tests have a downward improvement direction. + if unit in histogram.UNIT_NAMES: + return unit + '_smallerIsBetter', 1 + # A number of existing gtest perf tests report time in units like + # microseconds, but histograms only support milliseconds. So, convert here if + # we can. + if unit == 'us': + return 'msBestFitFormat_smallerIsBetter', 0.001 + if unit == 'ns': + return 'msBestFitFormat_smallerIsBetter', 0.000001 + return 'unitless_smallerIsBetter', 1 diff --git a/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py new file mode 100644 index 00000000000..b784143ee97 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py @@ -0,0 +1,114 @@ +# Copyright 2018 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. + +import unittest + +from tracing.value import gtest_json_converter +from tracing.value.diagnostics import reserved_infos + + +NANO_TO_MILLISECONDS = 0.000001 + + +class GtestJsonConverterUnittest(unittest.TestCase): + + def testConvertBasic(self): + data = { + 'metric1': { + 'units': 'ms', + 'traces': { + 'story1': ['10.12345', '0.54321'], + 'story2': ['30', '0'], + } + }, + 'metric2': { + 'units': 'ns', + 'traces': { + 'story1': ['100000.0', '2543.543'], + 'story2': ['12345.6789', '301.2'], + }, + } + } + histograms = gtest_json_converter.ConvertGtestJson(data) + self.assertEqual(len(histograms), 4) + + metric_histograms = histograms.GetHistogramsNamed('metric1') + self.assertEqual(len(metric_histograms), 2) + story1 = None + story2 = None + if metric_histograms[0].diagnostics[ + reserved_infos.STORIES.name].GetOnlyElement() == 'story1': + story1 = metric_histograms[0] + story2 = metric_histograms[1] + else: + story2 = metric_histograms[0] + story1 = metric_histograms[1] + + # assertAlmostEqual necessary to avoid floating point precision issues. + self.assertAlmostEqual(story1.average, 10.12345) + self.assertAlmostEqual(story1.standard_deviation, 0.54321) + self.assertAlmostEqual(story1.sum, story1.num_values * story1.average) + self.assertEqual(story2.average, 30) + self.assertEqual(story2.standard_deviation, 0) + self.assertEqual(story2.sum, story2.num_values * story2.average) + self.assertEqual(story1.unit, story2.unit) + self.assertEqual(story1.unit, 'ms_smallerIsBetter') + + metric_histograms = histograms.GetHistogramsNamed('metric2') + self.assertEqual(len(metric_histograms), 2) + if metric_histograms[0].diagnostics[ + reserved_infos.STORIES.name].GetOnlyElement() == 'story1': + story1 = metric_histograms[0] + story2 = metric_histograms[1] + else: + story2 = metric_histograms[0] + story1 = metric_histograms[1] + + # assertAlmostEqual necessary to avoid floating point precision issues. + # We expect the numbers to be different than what was initially provided + # since this should be converted to milliseconds. + self.assertAlmostEqual(story1.average, 100000 * NANO_TO_MILLISECONDS) + self.assertAlmostEqual(story1.standard_deviation, + 2543.543 * NANO_TO_MILLISECONDS) + self.assertAlmostEqual(story1.sum, story1.num_values * story1.average) + self.assertAlmostEqual(story2.average, 12345.6789 * NANO_TO_MILLISECONDS) + self.assertAlmostEqual(story2.standard_deviation, + 301.2 * NANO_TO_MILLISECONDS) + self.assertAlmostEqual(story2.sum, story2.num_values * story2.average) + self.assertEqual(story1.unit, story2.unit) + self.assertEqual(story1.unit, 'msBestFitFormat_smallerIsBetter') + + def testConvertUnknownUnit(self): + data = { + 'metric1': { + 'units': 'SomeUnknownUnit', + 'traces': { + 'story1': ['10', '1'], + 'story2': ['123.4', '7.89'], + } + } + } + histograms = gtest_json_converter.ConvertGtestJson(data) + self.assertEqual(len(histograms), 2) + + metric_histograms = histograms.GetHistogramsNamed('metric1') + self.assertEqual(len(metric_histograms), 2) + story1 = None + story2 = None + if metric_histograms[0].diagnostics[ + reserved_infos.STORIES.name].GetOnlyElement() == 'story1': + story1 = metric_histograms[0] + story2 = metric_histograms[1] + else: + story2 = metric_histograms[0] + story1 = metric_histograms[1] + + self.assertEqual(story1.average, 10) + self.assertEqual(story1.standard_deviation, 1) + self.assertEqual(story1.sum, story1.num_values * story1.average) + self.assertAlmostEqual(story2.average, 123.4) + self.assertAlmostEqual(story2.standard_deviation, 7.89) + self.assertAlmostEqual(story2.sum, story2.num_values * story2.average) + self.assertEqual(story1.unit, story2.unit) + self.assertEqual(story1.unit, 'unitless_smallerIsBetter') diff --git a/chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py new file mode 100644 index 00000000000..9ca47d2a709 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py @@ -0,0 +1,201 @@ +# Copyright 2017 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. + +import codecs +import collections +import sys +import time + +from tracing.value import histogram +from tracing.value import histogram_set +from tracing.value.diagnostics import breakdown +from tracing.value.diagnostics import date_range +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import related_name_map +from tracing.value.diagnostics import reserved_infos +from tracing_build import render_histograms_viewer + + +def _IsUserDefinedInstance(obj): + return str(type(obj)).startswith('<class ') + + +class _HeapProfiler(object): + __slots__ = '_diagnostics_callback', '_histograms', '_seen' + + def __init__(self, diagnostics_callback=None): + self._diagnostics_callback = diagnostics_callback + self._histograms = None + self._seen = set() + + def Profile(self, root): + self._histograms = histogram_set.HistogramSet() + total_hist = self._GetOrCreateHistogram('heap') + total_hist.diagnostics['types'] = related_name_map.RelatedNameMap() + total_breakdown = breakdown.Breakdown() + total_size = self._Recurse( + root, total_hist.diagnostics['types'], total_breakdown) + builtins_size = total_size - sum(subsize for _, subsize in total_breakdown) + + if builtins_size: + total_breakdown.Set('(builtin types)', builtins_size) + total_hist.AddSample(total_size, dict(types=total_breakdown)) + + self._histograms.AddSharedDiagnosticToAllHistograms( + reserved_infos.TRACE_START.name, + date_range.DateRange(time.time() * 1000)) + + return self._histograms + + def _GetOrCreateHistogram(self, name): + hs = self._histograms.GetHistogramsNamed(name) + if len(hs) > 1: + raise Exception('Too many Histograms named %s' % name) + + if len(hs) == 1: + return hs[0] + + hist = histogram.Histogram(name, 'sizeInBytes_smallerIsBetter') + hist.CustomizeSummaryOptions(dict(std=False, min=False, max=False)) + self._histograms.AddHistogram(hist) + return hist + + def _Recurse(self, obj, parent_related_names, parent_breakdown): + if id(obj) in self._seen: + return 0 + self._seen.add(id(obj)) + + size = sys.getsizeof(obj) + + related_names = parent_related_names + types_breakdown = parent_breakdown + + hist = None + if _IsUserDefinedInstance(obj): + type_name = type(obj).__name__ + hist = self._GetOrCreateHistogram('heap:' + type_name) + + related_names = hist.diagnostics.get('types') + if related_names is None: + related_names = related_name_map.RelatedNameMap() + types_breakdown = breakdown.Breakdown() + + if isinstance(obj, dict): + for objkey, objvalue in obj.iteritems(): + size += self._Recurse(objkey, related_names, types_breakdown) + size += self._Recurse(objvalue, related_names, types_breakdown) + elif isinstance(obj, (tuple, list, set, frozenset, collections.deque)): + # Can't use collections.Iterable because strings are iterable, but + # sys.getsizeof() already handles strings, we don't need to iterate over + # them. + for elem in obj: + size += self._Recurse(elem, related_names, types_breakdown) + + # It is possible to subclass builtin types like dict and add properties to + # them, so handle __dict__ and __slots__ even if obj is a dict/list/etc. + + properties_breakdown = breakdown.Breakdown() + if hasattr(obj, '__dict__'): + size += sys.getsizeof(obj.__dict__) + for dkey, dvalue in obj.__dict__.iteritems(): + size += self._Recurse(dkey, related_names, types_breakdown) + dsize = self._Recurse(dvalue, related_names, types_breakdown) + properties_breakdown.Set(dkey, dsize) + size += dsize + size += self._Recurse(obj.__dict__, related_names, types_breakdown) + + # It is possible for a class to use both __slots__ and __dict__ by listing + # __dict__ as a slot. + + if hasattr(obj.__class__, '__slots__'): + for slot in obj.__class__.__slots__: + if slot == '__dict__': + # obj.__dict__ was already handled + continue + if not hasattr(obj, slot): + continue + slot_size = self._Recurse( + getattr(obj, slot), related_names, types_breakdown) + properties_breakdown.Set(slot, slot_size) + size += slot_size + + if hist: + if len(related_names): + hist.diagnostics['types'] = related_names + + parent_related_names.Set(type_name, hist.name) + parent_breakdown.Set(type_name, parent_breakdown.Get(type_name) + size) + + builtins_size = size - sum(subsize for _, subsize in types_breakdown) + if builtins_size: + types_breakdown.Set('(builtin types)', builtins_size) + + sample_diagnostics = {'types': types_breakdown} + if len(properties_breakdown): + sample_diagnostics['properties'] = properties_breakdown + if self._diagnostics_callback: + sample_diagnostics.update(self._diagnostics_callback(obj)) + + hist.AddSample(size, sample_diagnostics) + + return size + + +def Profile(root, label=None, html_filename=None, html_stream=None, + vulcanized_viewer=None, reset_results=False, + diagnostics_callback=None): + """Profiles memory consumed by the root object. + + Produces a HistogramSet containing 1 Histogram for each user-defined class + encountered when recursing through the root object's properties. + Each Histogram contains 1 sample for each instance of the class. + Each sample contains 2 Breakdowns: + - 'types' allows drilling down into memory profiles for other classes, and + - 'properties' breaks down the size of an instance by its properties. + + Args: + label: string label to distinguish these results from those produced by + other Profile() calls. + html_filename: string filename to write HTML results. + html_stream: file-like string to write HTML results. + vulcanized_viewer: HTML string + reset_results: whether to delete pre-existing results in + html_filename/html_stream + diagnostics_callback: function that takes an instance of a class, and + returns a dictionary from strings to Diagnostic objects. + + Returns: + HistogramSet + """ + # TODO(4068): Package this and its dependencies and a vulcanized viewer in + # order to remove the vulcanized_viewer parameter and simplify rendering the + # viewer. + + profiler = _HeapProfiler(diagnostics_callback) + histograms = profiler.Profile(root) + + if label: + histograms.AddSharedDiagnosticToAllHistograms( + reserved_infos.LABELS.name, generic_set.GenericSet([label])) + + if html_filename and not html_stream: + open(html_filename, 'a').close() # Create file if it doesn't exist. + html_stream = codecs.open(html_filename, mode='r+', encoding='utf-8') + + if html_stream: + # Vulcanizing the viewer requires a full catapult checkout, which is not + # available in some contexts such as appengine. + # Merely rendering the viewer requires a pre-vulcanized viewer HTML string. + # render_histograms_viewer does not require a full checkout, so it can run + # in restricted contexts such as appengine as long as a pre-vulcanized + # viewer is provided. + if vulcanized_viewer: + render_histograms_viewer.RenderHistogramsViewer( + histograms.AsDicts(), html_stream, reset_results, vulcanized_viewer) + else: + from tracing_build import vulcanize_histograms_viewer + vulcanize_histograms_viewer.VulcanizeAndRenderHistogramsViewer( + histograms.AsDicts(), html_stream) + + return histograms diff --git a/chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py new file mode 100644 index 00000000000..d2904c8a151 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py @@ -0,0 +1,58 @@ +# Copyright 2017 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. + +import unittest + +from tracing.value import heap_profiler +from tracing.value import histogram +from tracing.value import histogram_set + + +class HeapProfilerUnitTest(unittest.TestCase): + + def testHeapProfiler(self): + test_data = histogram_set.HistogramSet() + for i in xrange(10): + test_hist = histogram.Histogram('test', 'n%') + test_hist.AddSample(i / 10.0) + test_data.AddHistogram(test_hist) + + histograms = heap_profiler.Profile(test_data) + + set_size_hist = histograms.GetHistogramNamed('heap:HistogramSet') + self.assertEquals(set_size_hist.num_values, 1) + # The exact sizes of python objects can vary between platforms and versions. + self.assertGreater(set_size_hist.sum, 10000) + + hist_size_hist = histograms.GetHistogramNamed('heap:Histogram') + self.assertEquals(hist_size_hist.num_values, 10) + self.assertGreater(hist_size_hist.sum, 10000) + + related_names = hist_size_hist.diagnostics['types'] + self.assertEquals(related_names.Get('HistogramBin'), 'heap:HistogramBin') + self.assertEquals(related_names.Get('DiagnosticMap'), 'heap:DiagnosticMap') + + properties = hist_size_hist.bins[33].diagnostic_maps[0]['properties'] + types = hist_size_hist.bins[33].diagnostic_maps[0]['types'] + self.assertGreater(len(properties), 3) + self.assertGreater(properties.Get('_bins'), 1000) + self.assertEquals(len(types), 4) + self.assertGreater(types.Get('HistogramBin'), 1000) + self.assertGreater(types.Get('(builtin types)'), 1000) + + bin_size_hist = histograms.GetHistogramNamed('heap:HistogramBin') + self.assertEquals(bin_size_hist.num_values, 32) + self.assertGreater(bin_size_hist.sum, 1000) + + diag_map_size_hist = histograms.GetHistogramNamed('heap:DiagnosticMap') + self.assertEquals(diag_map_size_hist.num_values, 10) + self.assertGreater(diag_map_size_hist.sum, 1000) + + range_size_hist = histograms.GetHistogramNamed('heap:Range') + self.assertEquals(range_size_hist.num_values, 22) + self.assertGreater(range_size_hist.sum, 1000) + + stats_size_hist = histograms.GetHistogramNamed('heap:RunningStatistics') + self.assertEquals(stats_size_hist.num_values, 10) + self.assertGreater(stats_size_hist.sum, 1000) diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram.html b/chromium/third_party/catapult/tracing/tracing/value/histogram.html new file mode 100644 index 00000000000..effc879666d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram.html @@ -0,0 +1,1478 @@ +<!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"> + +<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. + * @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; + } + + 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)); + } + } + } + + 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.summaryOptions = new Map(DEFAULT_SUMMARY_OPTIONS); + this.summaryOptions.set('percentile', []); + this.summaryOptions.set('iprs', []); + 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.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_; + } + + 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_; + } + + 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)) { + 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.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); + } + + 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.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 (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). + */ + 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_') { + const percent = percentFromString(statName.substr(4)); + if (this.numValues === 0) return undefined; + const percentile = this.getApproximatePercentile(percent); + if (typeof(percentile) !== 'number') return undefined; + return new tr.b.Scalar(this.unit, percentile); + } + 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 (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') { + 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') { + rebinned.summaryOptions.set(stat, Array.from(option)); + } else { + rebinned.summaryOptions.set(stat, option); + } + } + return rebinned; + } + + 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 (value === DEFAULT_SUMMARY_OPTIONS.get(name)) { + continue; + } else { + option = value; + } + 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. + + 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> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram.py b/chromium/third_party/catapult/tracing/tracing/value/histogram.py new file mode 100644 index 00000000000..272468f6430 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram.py @@ -0,0 +1,1111 @@ +# Copyright 2017 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. + +import json +import math +import numbers +import random + +from tracing.value.diagnostics import diagnostic +from tracing.value.diagnostics import diagnostic_ref +from tracing.value.diagnostics import reserved_infos +from tracing.value.diagnostics import unmergeable_diagnostic_set + + +try: + StringTypes = basestring +except NameError: + StringTypes = str + + +# pylint: disable=too-many-lines +# TODO(#3613) Split this file. + + +# This should be equal to sys.float_info.max, but that value might differ +# between platforms, whereas ECMA Script specifies this value for all platforms. +# The specific value should not matter in normal practice. +JS_MAX_VALUE = 1.7976931348623157e+308 + + +# Converts the given percent to a string in the following format: +# 0.x produces '0x0', +# 0.xx produces '0xx', +# 0.xxy produces '0xx_y', +# 1.0 produces '100'. +def PercentToString(percent): + if percent < 0 or percent > 1: + raise ValueError('percent must be in [0,1]') + if percent == 0: + return '000' + if percent == 1: + return '100' + s = str(percent) + if s[1] != '.': + raise ValueError('Unexpected percent') + s += '0' * max(4 - len(s), 0) + if len(s) > 4: + s = s[:4] + '_' + s[4:] + return '0' + s[2:] + + +# This variation of binary search returns the index |hi| into |ary| for which +# callback(ary[hi]) < 0 and callback(ary[hi-1]) >= 0 +# This function assumes that map(callback, ary) is sorted descending. +def FindHighIndexInSortedArray(ary, callback): + lo = 0 + hi = len(ary) + while lo < hi: + mid = (lo + hi) >> 1 + if callback(ary[mid]) >= 0: + lo = mid + 1 + else: + hi = mid + return hi + + +# Modifies |samples| in-place to reduce its length to |count|, discarding random +# elements. +def UniformlySampleArray(samples, count): + while len(samples) > count: + samples.pop(int(random.uniform(0, len(samples)))) + return samples + + +# When processing a stream of samples, call this method for each new sample in +# order to decide whether to keep it in |samples|. +# Modifies |samples| in-place such that its length never exceeds |num_samples|. +# After |stream_length| samples have been processed, each sample has equal +# probability of being retained in |samples|. +# The order of samples is not preserved after |stream_length| exceeds +# |num_samples|. +def UniformlySampleStream(samples, stream_length, new_element, num_samples): + if stream_length <= num_samples: + if len(samples) >= stream_length: + samples[stream_length - 1] = new_element + else: + samples.append(new_element) + return + + prob_keep = num_samples / stream_length + if random.random() > prob_keep: + # reject new_sample + return + + # replace a random element + samples[math.floor(random.random() * num_samples)] = new_element + + +# Merge two sets of samples that were assembled using UniformlySampleStream(). +# Modify |a_samples| in-place such that all of the samples in |a_samples| and +# |b_samples| have equal probability of being retained in |a_samples|. +def MergeSampledStreams(a_samples, a_stream_length, + b_samples, b_stream_length, num_samples): + if b_stream_length < num_samples: + for i in range(min(b_stream_length, len(b_samples))): + UniformlySampleStream( + a_samples, a_stream_length + i + 1, b_samples[i], num_samples) + return + + if a_stream_length < num_samples: + temp_samples = list(b_samples) + for i in range(min(a_stream_length, len(a_samples))): + UniformlySampleStream( + temp_samples, b_stream_length + i + 1, a_samples[i], num_samples) + for i, temp_sample in enumerate(temp_samples): + a_samples[i] = temp_sample + return + + prob_swap = b_stream_length / (a_stream_length + b_stream_length) + for i in range(min(num_samples, len(b_samples))): + if random.random() < prob_swap: + a_samples[i] = b_samples[i] + + +def Percentile(ary, percent): + if percent < 0 or percent > 1: + raise ValueError('percent must be in [0,1]') + ary = list(ary) + ary.sort() + return ary[int((len(ary) - 1) * percent)] + + +class Range(object): + __slots__ = '_empty', '_min', '_max' + + def __init__(self): + self._empty = True + self._min = None + self._max = None + + def __eq__(self, other): + if not isinstance(other, Range): + return False + if self.empty and other.empty: + return True + if self.empty != other.empty: + return False + return (self.min == other.min) and (self.max == other.max) + + def __hash__(self): + return id(self) + + @staticmethod + def FromExplicitRange(lower, upper): + r = Range() + r._min = lower + r._max = upper + r._empty = False + return r + + @property + def empty(self): + return self._empty + + @property + def min(self): + return self._min + + @property + def max(self): + return self._max + + @property + def center(self): + return (self._min + self._max) * 0.5 + + @property + def duration(self): + if self.empty: + return 0 + return self._max - self._min + + def AddValue(self, x): + if self._empty: + self._empty = False + self._min = x + self._max = x + return + + self._max = max(x, self._max) + self._min = min(x, self._min) + + def AddRange(self, other): + if other.empty: + return + self.AddValue(other.min) + self.AddValue(other.max) + + +# This class computes statistics online in O(1). +class RunningStatistics(object): + __slots__ = ( + '_count', '_mean', '_max', '_min', '_sum', '_variance', '_meanlogs') + + def __init__(self): + self._count = 0 + self._mean = 0.0 + self._max = -JS_MAX_VALUE + self._min = JS_MAX_VALUE + self._sum = 0.0 + self._variance = 0.0 + # Mean of logarithms of samples, or None if any samples were <= 0. + self._meanlogs = 0.0 + + @property + def count(self): + return self._count + + @property + def geometric_mean(self): + if self._meanlogs is None: + return None + return math.exp(self._meanlogs) + + @property + def mean(self): + if self._count == 0: + return None + return self._mean + + @property + def max(self): + return self._max + + @property + def min(self): + return self._min + + @property + def sum(self): + return self._sum + + # This returns the variance of the samples after Bessel's correction has + # been applied. + @property + def variance(self): + if self.count == 0: + return None + if self.count == 1: + return 0 + return self._variance / (self.count - 1) + + # This returns the standard deviation of the samples after Bessel's + # correction has been applied. + @property + def stddev(self): + if self.count == 0: + return None + return math.sqrt(self.variance) + + def Add(self, x): + self._count += 1 + x = float(x) + self._max = max(self._max, x) + self._min = min(self._min, x) + self._sum += x + + if x <= 0.0: + self._meanlogs = None + elif self._meanlogs is not None: + self._meanlogs += (math.log(abs(x)) - self._meanlogs) / self.count + + # The following uses Welford's algorithm for computing running mean and + # variance. See http://www.johndcook.com/blog/standard_deviation. + if self.count == 1: + self._mean = x + self._variance = 0.0 + else: + old_mean = self._mean + old_variance = self._variance + + # Using the 2nd formula for updating the mean yields better precision but + # it doesn't work for the case oldMean is Infinity. Hence we handle that + # case separately. + if abs(old_mean) == float('inf'): + self._mean = self._sum / self.count + else: + self._mean = old_mean + float(x - old_mean) / self.count + self._variance = old_variance + (x - old_mean) * (x - self._mean) + + def Merge(self, other): + result = RunningStatistics() + result._count = self._count + other._count + result._sum = self._sum + other._sum + result._min = min(self._min, other._min) + result._max = max(self._max, other._max) + if result._count == 0: + result._mean = 0.0 + result._variance = 0.0 + result._meanlogs = 0.0 + else: + # Combine the mean and the variance using the formulas from + # https://goo.gl/ddcAep. + result._mean = float(result._sum) / result._count + delta_mean = (self._mean or 0.0) - (other._mean or 0.0) + result._variance = self._variance + other._variance + ( + self._count * other._count * delta_mean * delta_mean / result._count) + + # Merge the arithmetic means of logarithms of absolute values of samples, + # weighted by counts. + if self._meanlogs is None or other._meanlogs is None: + result._meanlogs = None + else: + result._meanlogs = (self._count * self._meanlogs + + other._count * other._meanlogs) / result._count + return result + + def AsDict(self): + if self._count == 0: + return [] + + # Javascript automatically converts between ints and floats. + # It's more efficient to serialize integers as ints than floats. + def FloatAsFloatOrInt(x): + if x is not None and x.is_integer(): + return int(x) + return x + + # It's more efficient to serialize these 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 [ + self._count, + FloatAsFloatOrInt(self._max), + FloatAsFloatOrInt(self._meanlogs), + FloatAsFloatOrInt(self._mean), + FloatAsFloatOrInt(self._min), + FloatAsFloatOrInt(self._sum), + FloatAsFloatOrInt(self._variance), + ] + + @staticmethod + def FromDict(dct): + result = RunningStatistics() + if len(dct) != 7: + return result + + def AsFloatOrNone(x): + if x is None: + return x + return float(x) + [result._count, result._max, result._meanlogs, result._mean, result._min, + result._sum, result._variance] = [int(dct[0])] + [ + AsFloatOrNone(x) for x in dct[1:]] + return result + + +class DiagnosticMap(dict): + __slots__ = '_allow_reserved_names', + + def __init__(self, *args, **kwargs): + self._allow_reserved_names = True + dict.__init__(self, *args, **kwargs) + + def DisallowReservedNames(self): + self._allow_reserved_names = False + + def __setitem__(self, name, diag): + if not isinstance(name, StringTypes): + raise TypeError('name must be string') + if not isinstance(diag, (diagnostic.Diagnostic, + diagnostic_ref.DiagnosticRef)): + raise TypeError('diag must be Diagnostic or DiagnosticRef') + if (not self._allow_reserved_names and + not isinstance(diag, + unmergeable_diagnostic_set.UnmergeableDiagnosticSet) and + not isinstance(diag, diagnostic_ref.DiagnosticRef)): + expected_type = reserved_infos.GetTypeForName(name) + if expected_type and diag.__class__.__name__ != expected_type: + raise TypeError('Diagnostics names "%s" must be %s, not %s' % + (name, expected_type, diag.__class__.__name__)) + dict.__setitem__(self, name, diag) + + @staticmethod + def FromDict(dct): + dm = DiagnosticMap() + dm.AddDicts(dct) + return dm + + def AddDicts(self, dct): + for name, diagnostic_dict in dct.items(): + if name == 'tagmap': + continue + if isinstance(diagnostic_dict, StringTypes): + self[name] = diagnostic_ref.DiagnosticRef(diagnostic_dict) + elif diagnostic_dict['type'] not in [ + 'RelatedHistogramMap', 'RelatedHistogramBreakdown', 'TagMap']: + # Ignore RelatedHistograms and TagMaps. + # TODO(benjhayden): Forget about them in 2019 Q2. + self[name] = diagnostic.Diagnostic.FromDict(diagnostic_dict) + + def ResolveSharedDiagnostics(self, histograms, required=False): + for name, diag in self.items(): + if not isinstance(diag, diagnostic_ref.DiagnosticRef): + continue + guid = diag.guid + diag = histograms.LookupDiagnostic(guid) + if isinstance(diag, diagnostic.Diagnostic): + self[name] = diag + elif required: + raise ValueError('Unable to find shared Diagnostic ' + guid) + + def AsDict(self): + dct = {} + for name, diag in self.items(): + dct[name] = diag.AsDictOrReference() + return dct + + def Merge(self, other): + for name, other_diagnostic in other.items(): + if name not in self: + self[name] = other_diagnostic + continue + my_diagnostic = self[name] + if my_diagnostic.CanAddDiagnostic(other_diagnostic): + my_diagnostic.AddDiagnostic(other_diagnostic) + continue + self[name] = unmergeable_diagnostic_set.UnmergeableDiagnosticSet([ + my_diagnostic, other_diagnostic]) + + +MAX_DIAGNOSTIC_MAPS = 16 + + +class HistogramBin(object): + __slots__ = '_range', '_count', '_diagnostic_maps' + + def __init__(self, rang): + self._range = rang + self._count = 0 + self._diagnostic_maps = [] + + def AddSample(self, unused_x): + self._count += 1 + + @property + def count(self): + return self._count + + @property + def range(self): + return self._range + + def AddBin(self, other): + self._count += other.count + + @property + def diagnostic_maps(self): + return self._diagnostic_maps + + def AddDiagnosticMap(self, diagnostics): + UniformlySampleStream( + self._diagnostic_maps, self.count, diagnostics, MAX_DIAGNOSTIC_MAPS) + + def FromDict(self, dct): + self._count = dct[0] + if len(dct) > 1: + for diagnostic_map_dict in dct[1]: + self._diagnostic_maps.append(DiagnosticMap.FromDict( + diagnostic_map_dict)) + + def AsDict(self): + if len(self._diagnostic_maps) == 0: + return [self.count] + return [self.count, [d.AsDict() for d in self._diagnostic_maps]] + + +# TODO(#3814) Presubmit to compare with unit.html. +UNIT_NAMES = [ + 'ms', + 'msBestFitFormat', + 'tsMs', + 'n%', + 'sizeInBytes', + 'bytesPerSecond', + 'J', # Joule + 'W', # Watt + 'A', # Ampere + 'V', # Volt + 'Hz', # Hertz + 'unitless', + 'count', + 'sigma', +] + +def ExtendUnitNames(): + # Use a function in order to avoid cluttering the global namespace with a loop + # variable. + for name in list(UNIT_NAMES): + UNIT_NAMES.append(name + '_biggerIsBetter') + UNIT_NAMES.append(name + '_smallerIsBetter') + +ExtendUnitNames() + + +class Scalar(object): + __slots__ = '_unit', '_value' + + def __init__(self, unit, value): + assert unit in UNIT_NAMES, ( + 'Unrecognized unit "%r"' % unit) + self._unit = unit + self._value = value + + @property + def unit(self): + return self._unit + + @property + def value(self): + return self._value + + def AsDict(self): + return {'type': 'scalar', 'unit': self.unit, 'value': self.value} + + @staticmethod + def FromDict(dct): + return Scalar(dct['unit'], dct['value']) + + +DEFAULT_SUMMARY_OPTIONS = { + 'avg': True, + 'geometricMean': False, + 'std': True, + 'count': True, + 'sum': True, + 'min': True, + 'max': True, + 'nans': False, + # 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. +} + + +class Histogram(object): + __slots__ = ( + '_bin_boundaries_dict', + '_description', + '_name', + '_diagnostics', + '_nan_diagnostic_maps', + '_num_nans', + '_running', + '_sample_values', + '_summary_options', + '_unit', + '_bins', + '_max_num_sample_values') + + def __init__(self, name, unit, bin_boundaries=None): + assert unit in UNIT_NAMES, ( + 'Unrecognized unit "%r"' % unit) + + if bin_boundaries is None: + base_unit = unit.split('_')[0] + bin_boundaries = DEFAULT_BOUNDARIES_FOR_UNIT[base_unit] + + # Serialize bin boundaries here instead of holding a reference to it in case + # it is modified. + self._bin_boundaries_dict = bin_boundaries.AsDict() + + # HistogramBinBoundaries creates 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). + self._bins = list(bin_boundaries.bins) + self._description = '' + self._name = name + self._diagnostics = DiagnosticMap() + self._diagnostics.DisallowReservedNames() + self._nan_diagnostic_maps = [] + self._num_nans = 0 + self._running = None + self._sample_values = [] + self._summary_options = dict(DEFAULT_SUMMARY_OPTIONS) + self._summary_options['percentile'] = [] + self._unit = unit + + self._max_num_sample_values = self._GetDefaultMaxNumSampleValues() + + @property + def nan_diagnostic_maps(self): + return self._nan_diagnostic_maps + + @property + def unit(self): + return self._unit + + @property + def running(self): + return self._running + + @property + def max_num_sample_values(self): + return self._max_num_sample_values + + @max_num_sample_values.setter + def max_num_sample_values(self, n): + self._max_num_sample_values = n + UniformlySampleArray(self._sample_values, self._max_num_sample_values) + + @property + def sample_values(self): + return self._sample_values + + @property + def name(self): + return self._name + + @property + def bins(self): + return self._bins + + @property + def diagnostics(self): + return self._diagnostics + + @staticmethod + def FromDict(dct): + boundaries = HistogramBinBoundaries.FromDict(dct.get('binBoundaries')) + hist = Histogram(dct['name'], dct['unit'], boundaries) + if 'description' in dct: + hist._description = dct['description'] + if 'diagnostics' in dct: + hist._diagnostics.AddDicts(dct['diagnostics']) + if 'allBins' in dct: + if isinstance(dct['allBins'], list): + for i, bin_dct in enumerate(dct['allBins']): + # Copy HistogramBin on write, share the rest with the other + # Histograms that use the same HistogramBinBoundaries. + hist._bins[i] = HistogramBin(hist._bins[i].range) + hist._bins[i].FromDict(bin_dct) + else: + for i, bin_dct in dct['allBins'].items(): + i = int(i) + hist._bins[i] = HistogramBin(hist._bins[i].range) + hist._bins[i].FromDict(bin_dct) + if 'running' in dct: + hist._running = RunningStatistics.FromDict(dct['running']) + if 'summaryOptions' in dct: + hist.CustomizeSummaryOptions(dct['summaryOptions']) + if 'maxNumSampleValues' in dct: + hist._max_num_sample_values = dct['maxNumSampleValues'] + if 'sampleValues' in dct: + hist._sample_values = dct['sampleValues'] + if 'numNans' in dct: + hist._num_nans = dct['numNans'] + if 'nanDiagnostics' in dct: + for map_dct in dct['nanDiagnostics']: + hist._nan_diagnostic_maps.append(DiagnosticMap.FromDict(map_dct)) + return hist + + @property + def num_values(self): + if self._running is None: + return 0 + return self._running.count + + @property + def num_nans(self): + return self._num_nans + + @property + def average(self): + if self._running is None: + return None + return self._running.mean + + @property + def standard_deviation(self): + if self._running is None: + return None + return self._running.stddev + + @property + def geometric_mean(self): + if self._running is None: + return 0 + return self._running.geometric_mean + + @property + def sum(self): + if self._running is None: + return 0 + return self._running.sum + + def GetApproximatePercentile(self, percent): + if percent < 0 or percent > 1: + raise ValueError('percent must be in [0,1]') + if self.num_values == 0: + return 0 + + if len(self._bins) == 1: + sorted_sample_values = list(self._sample_values) + sorted_sample_values.sort() + return sorted_sample_values[ + int((len(sorted_sample_values) - 1) * percent)] + + values_to_skip = math.floor((self.num_values - 1) * percent) + for hbin in self._bins: + values_to_skip -= hbin.count + if values_to_skip >= 0: + continue + if hbin.range.min == -JS_MAX_VALUE: + return hbin.range.max + elif hbin.range.max == JS_MAX_VALUE: + return hbin.range.min + else: + return hbin.range.center + return self._bins[len(self._bins) - 1].range.min + + def GetBinIndexForValue(self, value): + index = FindHighIndexInSortedArray( + self._bins, lambda b: (-1 if (value < b.range.max) else 1)) + if 0 <= index < len(self._bins): + return index + return len(self._bins) - 1 + + def GetBinForValue(self, value): + return self._bins[self.GetBinIndexForValue(value)] + + def AddSample(self, value, diagnostic_map=None): + if (diagnostic_map is not None and + not isinstance(diagnostic_map, DiagnosticMap)): + diagnostic_map = DiagnosticMap(diagnostic_map) + + if not isinstance(value, numbers.Number) or math.isnan(value): + self._num_nans += 1 + if diagnostic_map: + UniformlySampleStream(self._nan_diagnostic_maps, self.num_nans, + diagnostic_map, MAX_DIAGNOSTIC_MAPS) + else: + if self._running is None: + self._running = RunningStatistics() + self._running.Add(value) + + bin_index = self.GetBinIndexForValue(value) + hbin = self._bins[bin_index] + if hbin.count == 0: + hbin = HistogramBin(hbin.range) + self._bins[bin_index] = hbin + hbin.AddSample(value) + if diagnostic_map: + hbin.AddDiagnosticMap(diagnostic_map) + + UniformlySampleStream(self._sample_values, self.num_values + self.num_nans, + value, self.max_num_sample_values) + + def CanAddHistogram(self, other): + if self.unit != other.unit: + return False + return self._bin_boundaries_dict == other._bin_boundaries_dict + + def AddHistogram(self, other): + if not self.CanAddHistogram(other): + raise ValueError('Merging incompatible Histograms') + + MergeSampledStreams( + self.sample_values, self.num_values, + other.sample_values, other.num_values, + (self.max_num_sample_values + other.max_num_sample_values) / 2) + self._num_nans += other._num_nans + + if other.running is not None: + if self.running is None: + self._running = RunningStatistics() + self._running = self._running.Merge(other.running) + + for i, hbin in enumerate(other.bins): + mybin = self._bins[i] + if mybin.count == 0: + self._bins[i] = mybin = HistogramBin(mybin.range) + mybin.AddBin(hbin) + + self.diagnostics.Merge(other.diagnostics) + + def CustomizeSummaryOptions(self, options): + for key, value in options.items(): + self._summary_options[key] = value + + def Clone(self): + return Histogram.FromDict(self.AsDict()) + + def CloneEmpty(self): + return Histogram(self.name, self.unit, HistogramBinBoundaries.FromDict( + self._bin_boundaries_dict)) + + @property + def statistics_scalars(self): + results = {} + for stat_name, option in self._summary_options.items(): + if not option: + continue + if stat_name == 'percentile': + for percent in option: + percentile = self.GetApproximatePercentile(percent) + results['pct_' + PercentToString(percent)] = Scalar( + self.unit, percentile) + elif stat_name == 'nans': + results['nans'] = Scalar('count', self.num_nans) + else: + if stat_name == 'count': + stat_unit = 'count' + else: + stat_unit = self.unit + if stat_name == 'std': + key = 'stddev' + elif stat_name == 'avg': + key = 'mean' + elif stat_name == 'geometricMean': + key = 'geometric_mean' + else: + key = stat_name + if self._running is None: + self._running = RunningStatistics() + stat_value = getattr(self._running, key) + if isinstance(stat_value, numbers.Number): + results[stat_name] = Scalar(stat_unit, stat_value) + return results + + def AsDict(self): + dct = {'name': self.name, 'unit': self.unit} + if self._bin_boundaries_dict is not None: + dct['binBoundaries'] = self._bin_boundaries_dict + if self._description: + dct['description'] = self._description + if len(self.diagnostics): + dct['diagnostics'] = self.diagnostics.AsDict() + if self.max_num_sample_values != self._GetDefaultMaxNumSampleValues(): + dct['maxNumSampleValues'] = self.max_num_sample_values + if self.num_nans: + dct['numNans'] = self.num_nans + if len(self.nan_diagnostic_maps): + dct['nanDiagnostics'] = [m.AsDict() for m in self.nan_diagnostic_maps] + if self.num_values: + dct['sampleValues'] = list(self.sample_values) + dct['running'] = self._running.AsDict() + dct['allBins'] = self._GetAllBinsAsDict() + if dct['allBins'] is None: + del dct['allBins'] + + summary_options = {} + any_overridden_summary_options = False + for name, option in self._summary_options.items(): + if name == 'percentile': + if len(option) == 0: + continue + elif option == DEFAULT_SUMMARY_OPTIONS[name]: + continue + summary_options[name] = option + any_overridden_summary_options = True + if any_overridden_summary_options: + dct['summaryOptions'] = summary_options + return dct + + def _GetAllBinsAsDict(self): + num_bins = len(self._bins) + empty_bins = 0 + for hbin in self._bins: + if hbin.count == 0: + empty_bins += 1 + if empty_bins == num_bins: + return None + + if empty_bins > (num_bins / 2): + all_bins_dict = {} + for i, hbin in enumerate(self._bins): + if hbin.count > 0: + all_bins_dict[i] = hbin.AsDict() + return all_bins_dict + + all_bins_list = [] + for hbin in self._bins: + all_bins_list.append(hbin.AsDict()) + return all_bins_list + + def _GetDefaultMaxNumSampleValues(self): + return len(self._bins) * 10 + + +class HistogramBinBoundaries(object): + __slots__ = '_builder', '_range', '_bin_ranges', '_bins' + + CACHE = {} + SLICE_TYPE_LINEAR = 0 + SLICE_TYPE_EXPONENTIAL = 1 + + def __init__(self, min_bin_boundary): + self._builder = [min_bin_boundary] + self._range = Range() + self._range.AddValue(min_bin_boundary) + self._bin_ranges = None + self._bins = None + + @property + def range(self): + return self._range + + @staticmethod + def FromDict(dct): + if dct is None: + return HistogramBinBoundaries.SINGULAR + + cache_key = json.dumps(dct) + if cache_key in HistogramBinBoundaries.CACHE: + return HistogramBinBoundaries.CACHE[cache_key] + + bin_boundaries = HistogramBinBoundaries(dct[0]) + for slic in dct[1:]: + if not isinstance(slic, list): + bin_boundaries.AddBinBoundary(slic) + continue + if slic[0] == HistogramBinBoundaries.SLICE_TYPE_LINEAR: + bin_boundaries.AddLinearBins(slic[1], slic[2]) + elif slic[0] == HistogramBinBoundaries.SLICE_TYPE_EXPONENTIAL: + bin_boundaries.AddExponentialBins(slic[1], slic[2]) + else: + raise ValueError('Unrecognized HistogramBinBoundaries slice type') + + HistogramBinBoundaries.CACHE[cache_key] = bin_boundaries + return bin_boundaries + + def AsDict(self): + if len(self._builder) == 1 and self._builder[0] == JS_MAX_VALUE: + return None + return self._builder + + @staticmethod + def CreateExponential(lower, upper, num_bins): + return HistogramBinBoundaries(lower).AddExponentialBins(upper, num_bins) + + @staticmethod + def CreateLinear(lower, upper, num_bins): + return HistogramBinBoundaries(lower).AddLinearBins(upper, num_bins) + + def _PushBuilderSlice(self, slic): + self._builder += [slic] + + def AddBinBoundary(self, next_max_bin_boundary): + if next_max_bin_boundary <= self.range.max: + raise ValueError('The added max bin boundary must be larger than ' + + 'the current max boundary') + + self._bin_ranges = None + self._bins = None + + self._PushBuilderSlice(next_max_bin_boundary) + self.range.AddValue(next_max_bin_boundary) + return self + + def AddLinearBins(self, next_max_bin_boundary, bin_count): + if bin_count <= 0: + raise ValueError('Bin count must be positive') + if next_max_bin_boundary <= self.range.max: + raise ValueError('The new max bin boundary must be greater than ' + + 'the previous max bin boundary') + + self._bin_ranges = None + self._bins = None + + self._PushBuilderSlice([ + HistogramBinBoundaries.SLICE_TYPE_LINEAR, + next_max_bin_boundary, bin_count]) + self.range.AddValue(next_max_bin_boundary) + return self + + def AddExponentialBins(self, next_max_bin_boundary, bin_count): + if bin_count <= 0: + raise ValueError('Bin count must be positive') + if self.range.max <= 0: + raise ValueError('Current max bin boundary must be positive') + if self.range.max >= next_max_bin_boundary: + raise ValueError('The last added max boundary must be greater than ' + + 'the current max boundary boundary') + + self._bin_ranges = None + self._bins = None + + self._PushBuilderSlice([ + HistogramBinBoundaries.SLICE_TYPE_EXPONENTIAL, + next_max_bin_boundary, bin_count]) + self.range.AddValue(next_max_bin_boundary) + return self + + @property + def bins(self): + if self._bins is None: + self._BuildBins() + return self._bins + + def _BuildBins(self): + self._bins = [HistogramBin(r) for r in self.bin_ranges] + + @property + def bin_ranges(self): + if self._bin_ranges is None: + self._BuildBinRanges() + return self._bin_ranges + + def _BuildBinRanges(self): + if not isinstance(self._builder[0], numbers.Number): + raise ValueError('Invalid start of builder_') + + self._bin_ranges = [] + prev_boundary = self._builder[0] + if prev_boundary > -JS_MAX_VALUE: + # underflow bin + self._bin_ranges.append(Range.FromExplicitRange( + -JS_MAX_VALUE, prev_boundary)) + + for slic in self._builder[1:]: + if not isinstance(slic, list): + self._bin_ranges.append(Range.FromExplicitRange( + prev_boundary, slic)) + prev_boundary = slic + continue + + next_max_bin_boundary = float(slic[1]) + bin_count = slic[2] + slice_min_bin_boundary = float(prev_boundary) + + if slic[0] == self.SLICE_TYPE_LINEAR: + bin_width = (next_max_bin_boundary - prev_boundary) / bin_count + for i in range(1, bin_count): + boundary = slice_min_bin_boundary + (i * bin_width) + self._bin_ranges.append(Range.FromExplicitRange( + prev_boundary, boundary)) + prev_boundary = boundary + elif slic[0] == self.SLICE_TYPE_EXPONENTIAL: + bin_exponent_width = ( + math.log(next_max_bin_boundary / prev_boundary) / bin_count) + for i in range(1, bin_count): + boundary = slice_min_bin_boundary * math.exp(i * bin_exponent_width) + self._bin_ranges.append(Range.FromExplicitRange( + prev_boundary, boundary)) + prev_boundary = boundary + else: + raise ValueError('Unrecognized HistogramBinBoundaries slice type') + + self._bin_ranges.append(Range.FromExplicitRange( + prev_boundary, next_max_bin_boundary)) + prev_boundary = next_max_bin_boundary + + if prev_boundary < JS_MAX_VALUE: + # overflow bin + self._bin_ranges.append(Range.FromExplicitRange( + prev_boundary, JS_MAX_VALUE)) + + +HistogramBinBoundaries.SINGULAR = HistogramBinBoundaries(JS_MAX_VALUE) + + +# The JS version computes these values using tr.b.convertUnit, which is +# not implemented in Python, so we write them out here. +def _CreateMsAutoFormatBins(): + bins = [ + 2000, + 5000, + 10000, + 30000, + 60000, + 120000, + 300000, + 600000, + 1800000, + 3600000, + 7200000, + 21600000, + 43200000, + 86400000, + 604800000, + 2629743840, + 31556926080 + ] + + boundaries = HistogramBinBoundaries(0).AddBinBoundary(1).AddExponentialBins( + 1e3, 3) + + for b in bins: + boundaries.AddBinBoundary(b) + + return boundaries + + +DEFAULT_BOUNDARIES_FOR_UNIT = { + 'ms': HistogramBinBoundaries.CreateExponential(1e-3, 1e6, 100), + 'tsMs': HistogramBinBoundaries.CreateLinear(0, 1e10, 1000), + 'msBestFitFormat': _CreateMsAutoFormatBins(), + 'n%': HistogramBinBoundaries.CreateLinear(0, 1.0, 20), + 'sizeInBytes': HistogramBinBoundaries.CreateExponential(1, 1e12, 100), + 'bytesPerSecond': HistogramBinBoundaries.CreateExponential(1, 1e12, 100), + 'J': HistogramBinBoundaries.CreateExponential(1e-3, 1e3, 50), + 'W': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), + 'A': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), + 'V': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), + 'Hz': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), + 'unitless': HistogramBinBoundaries.CreateExponential(1e-3, 1e3, 50), + 'count': HistogramBinBoundaries.CreateExponential(1, 1e3, 20), + 'sigma': HistogramBinBoundaries.CreateLinear(-5, 5, 50), +} diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html new file mode 100644 index 00000000000..9a61aa19510 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html @@ -0,0 +1,204 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v', function() { + /* + * HistogramGrouping objects are registered named functions that map from + * Histogram objects to strings. + * + * They are used to group Histograms by + * tr.v.HistogramSet.groupHistogramsRecursively() + * + * The tr-ui-b-grouping-table-groupby-picker module within the + * tr-v-ui-histogram-set-controls module allows users to select and reorder + * groupings. + */ + class HistogramGrouping { + /** + * @param {string} key + * @param {!function(!tr.v.Histogram):string} callback + */ + constructor(key, callback) { + this.key_ = key; + this.callback_ = callback; + + HistogramGrouping.BY_KEY.set(key, this); + } + + get key() { + return this.key_; + } + + get callback() { + return this.callback_; + } + + get label() { + return this.key; + } + + /** + * @param {!Set.<string>} tags + * @param {string} diagnosticName + * @return {!Array.<!HistogramGrouping>} + */ + static buildFromTags(tags, diagnosticName) { + const booleanTags = new Set(); + const keyValueTags = new Set(); + for (const tag of tags) { + if (tag.includes(':')) { + const key = tag.split(':')[0]; + if (booleanTags.has(key)) { + throw new Error( + `Tag "${key}" cannot be both boolean and key-value`); + } + keyValueTags.add(key); + } else { + if (keyValueTags.has(tag)) { + throw new Error( + `Tag "${tag}" cannot be both boolean and key-value`); + } + booleanTags.add(tag); + } + } + + const groupings = []; + for (const tag of booleanTags) { + groupings.push(HistogramGrouping.buildBooleanTagGrouping_( + tag, diagnosticName)); + } + for (const tag of keyValueTags) { + groupings.push(HistogramGrouping.buildKeyValueTagGrouping_( + tag, diagnosticName)); + } + return groupings; + } + + static buildBooleanTagGrouping_(tag, diagnosticName) { + return new HistogramGrouping(`${tag}Tag`, h => { + const tags = h.diagnostics.get(diagnosticName); + if (tags === undefined || !tags.has(tag)) return `~${tag}`; + return tag; + }); + } + + static buildKeyValueTagGrouping_(tag, diagnosticName) { + return new HistogramGrouping(`${tag}Tag`, h => { + const tags = h.diagnostics.get(diagnosticName); + if (tags === undefined) return `~${tag}`; + const values = new Set(); + for (const value of tags) { + const kvp = value.split(':'); + if (kvp.length < 2 || kvp[0] !== tag) continue; + values.add(kvp[1]); + } + if (values.size === 0) return `~${tag}`; + const sortedValues = Array.from(values); + sortedValues.sort(); + return sortedValues.join(','); + }, `${tag} tag`); + } + } + + HistogramGrouping.BY_KEY = new Map(); + + HistogramGrouping.HISTOGRAM_NAME = new HistogramGrouping('name', h => h.name); + + HistogramGrouping.DISPLAY_LABEL = new HistogramGrouping( + 'displayLabel', hist => { + const labels = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.LABELS); + if (labels !== undefined && labels.size > 0) { + return Array.from(labels).join(','); + } + + const benchmarks = hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.BENCHMARKS); + const start = hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.BENCHMARK_START); + if (benchmarks === undefined) { + if (start === undefined) return 'Value'; + + return start.toString(); + } + const benchmarksStr = Array.from(benchmarks).join('\n'); + + if (start === undefined) return benchmarksStr; + + return benchmarksStr + '\n' + start.toString(); + }); + + class GenericSetGrouping extends HistogramGrouping { + constructor(name) { + super(name, undefined); + this.callback_ = this.compute_.bind(this); + } + + compute_(hist) { + const diag = hist.diagnostics.get(this.key); + if (diag === undefined) return ''; + const parts = Array.from(diag); + parts.sort(); + return parts.join(','); + } + } + + GenericSetGrouping.NAMES = [ + tr.v.d.RESERVED_NAMES.ARCHITECTURES, + tr.v.d.RESERVED_NAMES.BENCHMARKS, + tr.v.d.RESERVED_NAMES.BOTS, + tr.v.d.RESERVED_NAMES.BUILDS, + tr.v.d.RESERVED_NAMES.DEVICE_IDS, + tr.v.d.RESERVED_NAMES.MASTERS, + tr.v.d.RESERVED_NAMES.MEMORY_AMOUNTS, + tr.v.d.RESERVED_NAMES.OS_NAMES, + tr.v.d.RESERVED_NAMES.OS_VERSIONS, + tr.v.d.RESERVED_NAMES.PRODUCT_VERSIONS, + tr.v.d.RESERVED_NAMES.STORIES, + tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, + tr.v.d.RESERVED_NAMES.STORY_TAGS, + tr.v.d.RESERVED_NAMES.TEST_PATH, + ]; + + for (const name of GenericSetGrouping.NAMES) { + // Instantiating a HistogramGrouping adds it to BY_KEY. + new GenericSetGrouping(name); + } + + class DateRangeGrouping extends HistogramGrouping { + constructor(name) { + super(name, undefined); + this.callback_ = this.compute_.bind(this); + } + + compute_(hist) { + const diag = hist.diagnostics.get(this.key); + if (diag === undefined) return ''; + return diag.toString(); + } + } + + DateRangeGrouping.NAMES = [ + tr.v.d.RESERVED_NAMES.BENCHMARK_START, + tr.v.d.RESERVED_NAMES.TRACE_START, + ]; + + for (const name of DateRangeGrouping.NAMES) { + new DateRangeGrouping(name); + } + + return { + HistogramGrouping, + GenericSetGrouping, + DateRangeGrouping, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py new file mode 100644 index 00000000000..49eab313d49 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py @@ -0,0 +1,161 @@ +# Copyright 2017 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. + +from tracing.value.diagnostics import reserved_infos + + +GROUPINGS_BY_KEY = {} + + +class HistogramGrouping(object): + """This class wraps a registered function that maps from a Histogram to a + string or number in order to allow grouping together Histograms that produce + the same string or number. HistogramGroupings may be looked up by key in + GROUPINGS_BY_KEY. + """ + + def __init__(self, key, callback): + self._key = key + self._callback = callback + GROUPINGS_BY_KEY[key] = self + + @property + def key(self): + return self._key + + @property + def callback(self): + return self._callback + + +def BuildFromTags(tags, diagnostic_name): + """Builds HistogramGroupings from a set of tags. + + Builds one HistogramGrouping for each tag in tags. The HistogramGroupings wrap + functions (callback) that get the named diagnostic from the given Histogram. + If the named diagnostic is found, it is assumed to be a GenericSet containing + strings. The HistogramGrouping callback returns a string indicating whether + the tag is in the GenericSet, and, if it is a key-value tag, what its values + are. + + Args: + tags: set of strings + diagnostic_name: string, e.g. reserved_infos.STORY_TAGS.name + + Returns: + list of HistogramGrouping + """ + boolean_tags = set() + key_value_tags = set() + for tag in tags: + if ':' in tag: + key = tag.split(':')[0] + assert key not in boolean_tags, key + key_value_tags.add(key) + else: + assert tag not in key_value_tags, tag + boolean_tags.add(tag) + groupings = [ + _BuildBooleanTagGrouping(tag, diagnostic_name) for tag in boolean_tags] + groupings += [ + _BuildKeyValueTagGrouping(key, diagnostic_name) for key in key_value_tags] + return groupings + + +def _BuildBooleanTagGrouping(tag, diagnostic_name): + def Closure(hist): + tags = hist.diagnostics.get(diagnostic_name) + if not tags or tag not in tags: + return '~' + tag + return tag + return HistogramGrouping(tag + 'Tag', Closure) + + +def _BuildKeyValueTagGrouping(key, diagnostic_name): + def Closure(hist): + tags = hist.diagnostics.get(diagnostic_name) + if not tags: + return '~' + key + values = set() + for tag in tags: + kvp = tag.split(':') + if len(kvp) < 2 or kvp[0] != key: + continue + values.add(kvp[1]) + if len(values) == 0: + return '~' + key + return ','.join(sorted(values)) + return HistogramGrouping(key + 'Tag', Closure) + + +HISTOGRAM_NAME = HistogramGrouping('name', lambda h: h.name) + + +def _DisplayLabel(hist): + labels = hist.diagnostics.get(reserved_infos.LABELS.name) + if labels and len(labels): + return ','.join(sorted(labels)) + + benchmarks = hist.diagnostics.get(reserved_infos.BENCHMARKS.name) + start = hist.diagnostics.get(reserved_infos.BENCHMARK_START.name) + if not benchmarks: + if not start: + return 'Value' + return str(start) + benchmarks = '\n'.join(benchmarks) + if not start: + return benchmarks + return benchmarks + '\n' + str(start) + + +DISPLAY_LABEL = HistogramGrouping('displayLabel', _DisplayLabel) + + +class GenericSetGrouping(HistogramGrouping): + """Wraps a function that looks up and formats a GenericSet by name from a + Histogram. + """ + + def __init__(self, name): + super(GenericSetGrouping, self).__init__(name, self._Compute) + + def _Compute(self, hist): + diag = hist.diagnostics.get(self.key) + if not diag: + return '' + return ','.join(str(elem) for elem in sorted(diag)) + + +GenericSetGrouping(reserved_infos.ARCHITECTURES.name) +GenericSetGrouping(reserved_infos.BENCHMARKS.name) +GenericSetGrouping(reserved_infos.BOTS.name) +GenericSetGrouping(reserved_infos.BUILDS.name) +GenericSetGrouping(reserved_infos.DEVICE_IDS.name) +GenericSetGrouping(reserved_infos.MASTERS.name) +GenericSetGrouping(reserved_infos.MEMORY_AMOUNTS.name) +GenericSetGrouping(reserved_infos.OS_NAMES.name) +GenericSetGrouping(reserved_infos.OS_VERSIONS.name) +GenericSetGrouping(reserved_infos.PRODUCT_VERSIONS.name) +GenericSetGrouping(reserved_infos.STORIES.name) +GenericSetGrouping(reserved_infos.STORYSET_REPEATS.name) +GenericSetGrouping(reserved_infos.STORY_TAGS.name) + + +class DateRangeGrouping(HistogramGrouping): + """Wraps a function that looks up and formats a DateRange by name from a + Histogram. + """ + + def __init__(self, name): + super(DateRangeGrouping, self).__init__(name, self._Compute) + + def _Compute(self, hist): + diag = hist.diagnostics.get(self.key) + if not diag: + return '' + return str(diag) + + +DateRangeGrouping(reserved_infos.BENCHMARK_START.name) +DateRangeGrouping(reserved_infos.TRACE_START.name) diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html new file mode 100644 index 00000000000..622313b54b0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html @@ -0,0 +1,163 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/histogram_grouping.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('booleanTags', function() { + const aHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet( + ['video', 'audio']), + ]]), + }); + const bHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['audio']), + ]]), + }); + const cHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['video']), + ]]), + }); + const dHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet([]), + ]]), + }); + + const groupings = tr.v.HistogramGrouping.buildFromTags( + ['video', 'audio'], tr.v.d.RESERVED_NAMES.STORY_TAGS); + assert.lengthOf(groupings, 2); + assert.strictEqual(groupings[0].key, 'videoTag'); + assert.strictEqual(groupings[1].key, 'audioTag'); + assert.strictEqual(groupings[0].callback(aHist), 'video'); + assert.strictEqual(groupings[0].callback(bHist), '~video'); + assert.strictEqual(groupings[0].callback(cHist), 'video'); + assert.strictEqual(groupings[0].callback(dHist), '~video'); + assert.strictEqual(groupings[1].callback(aHist), 'audio'); + assert.strictEqual(groupings[1].callback(bHist), 'audio'); + assert.strictEqual(groupings[1].callback(cHist), '~audio'); + assert.strictEqual(groupings[1].callback(dHist), '~audio'); + }); + + test('keyValueTags', function() { + const aHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['case:load']), + ]]), + }); + const bHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet( + ['case:browse']), + ]]), + }); + const cHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet([]), + ]]), + }); + const dHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet( + ['case:load', 'case:browse']), + ]]), + }); + + const groupings = tr.v.HistogramGrouping.buildFromTags( + ['case:load', 'case:browse'], tr.v.d.RESERVED_NAMES.STORY_TAGS); + assert.lengthOf(groupings, 1); + assert.strictEqual(groupings[0].key, 'caseTag'); + assert.strictEqual(groupings[0].callback(aHist), 'load'); + assert.strictEqual(groupings[0].callback(bHist), 'browse'); + assert.strictEqual(groupings[0].callback(cHist), '~case'); + assert.strictEqual(groupings[0].callback(dHist), 'browse,load'); + }); + + test('histogramNameGrouping', function() { + const hist = tr.v.Histogram.create('name', tr.b.Unit.byName.count, []); + assert.strictEqual(tr.v.HistogramGrouping.HISTOGRAM_NAME.callback(hist), + 'name'); + }); + + test('labelGrouping', function() { + const hist = tr.v.Histogram.create('name', tr.b.Unit.byName.count, []); + assert.strictEqual(tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist), + 'Value'); + hist.diagnostics.set(tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['H'])); + assert.strictEqual(tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist), + 'H'); + }); + + test('genericSetGrouping', function() { + const grouping = new tr.v.GenericSetGrouping('foo'); + + const empty = tr.v.Histogram.create('', tr.b.Unit.byName.count, []); + assert.strictEqual(grouping.callback(empty), ''); + + const hist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([ + ['foo', new tr.v.d.GenericSet(['baz', 'bar'])], + ]), + }); + assert.strictEqual(grouping.callback(hist), 'bar,baz'); + }); + + test('reservedGenericSetGroupings', function() { + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.ARCHITECTURES), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BENCHMARKS), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BOTS), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BUILDS), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.MASTERS), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.MEMORY_AMOUNTS), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.OS_NAMES), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.OS_VERSIONS), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.PRODUCT_VERSIONS), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORIES), tr.v.GenericSetGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.STORYSET_REPEATS), tr.v.GenericSetGrouping); + }); + + test('dateRangeGrouping', function() { + const grouping = new tr.v.DateRangeGrouping('foo'); + + const empty = tr.v.Histogram.create('', tr.b.Unit.byName.count, []); + assert.strictEqual(grouping.callback(empty), ''); + + const hist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([ + ['foo', new tr.v.d.DateRange(15e11)], + ]), + }); + assert.strictEqual(grouping.callback(hist), + tr.b.formatDate(new Date(15e11))); + }); + + test('reservedDateRangeGroupings', function() { + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BENCHMARK_START), tr.v.DateRangeGrouping); + assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.TRACE_START), tr.v.DateRangeGrouping); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py new file mode 100644 index 00000000000..3050121e970 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py @@ -0,0 +1,127 @@ +# Copyright 2017 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. + +import unittest + +from tracing.value import histogram +from tracing.value import histogram_grouping +from tracing.value.diagnostics import date_range +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import reserved_infos + +class HistogramGroupingUnittest(unittest.TestCase): + + def testBooleanTags(self): + a_hist = histogram.Histogram('', 'count') + a_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + ['video', 'audio']) + b_hist = histogram.Histogram('', 'count') + b_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + ['audio']) + c_hist = histogram.Histogram('', 'count') + c_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + ['video']) + d_hist = histogram.Histogram('', 'count') + d_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + []) + groupings = histogram_grouping.BuildFromTags( + ['audio', 'video'], reserved_infos.STORY_TAGS.name) + self.assertEqual(len(groupings), 2) + groupings.sort(key=lambda g: g.key) + self.assertEqual(groupings[0].key, 'audioTag') + self.assertEqual(groupings[1].key, 'videoTag') + self.assertEqual(groupings[0].callback(a_hist), 'audio') + self.assertEqual(groupings[0].callback(b_hist), 'audio') + self.assertEqual(groupings[0].callback(c_hist), '~audio') + self.assertEqual(groupings[0].callback(d_hist), '~audio') + self.assertEqual(groupings[1].callback(a_hist), 'video') + self.assertEqual(groupings[1].callback(b_hist), '~video') + self.assertEqual(groupings[1].callback(c_hist), 'video') + self.assertEqual(groupings[1].callback(d_hist), '~video') + + def testKeyValueTags(self): + a_hist = histogram.Histogram('', 'count') + a_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + ['case:load']) + b_hist = histogram.Histogram('', 'count') + b_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + ['case:browse']) + c_hist = histogram.Histogram('', 'count') + c_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + []) + d_hist = histogram.Histogram('', 'count') + d_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet( + ['case:load', 'case:browse']) + groupings = histogram_grouping.BuildFromTags( + ['case:load', 'case:browse'], reserved_infos.STORY_TAGS.name) + self.assertEqual(len(groupings), 1) + self.assertEqual(groupings[0].key, 'caseTag') + self.assertEqual(groupings[0].callback(a_hist), 'load') + self.assertEqual(groupings[0].callback(b_hist), 'browse') + self.assertEqual(groupings[0].callback(c_hist), '~case') + self.assertEqual(groupings[0].callback(d_hist), 'browse,load') + + def testName(self): + self.assertEqual(histogram_grouping.HISTOGRAM_NAME.callback( + histogram.Histogram('test', 'count')), 'test') + + def testDisplayLabel(self): + hist = histogram.Histogram('test', 'count') + self.assertEqual(histogram_grouping.DISPLAY_LABEL.callback(hist), 'Value') + hist.diagnostics[reserved_infos.LABELS.name] = generic_set.GenericSet(['H']) + self.assertEqual(histogram_grouping.DISPLAY_LABEL.callback(hist), 'H') + + def testGenericSet(self): + grouping = histogram_grouping.GenericSetGrouping('foo') + hist = histogram.Histogram('', 'count') + self.assertEqual(grouping.callback(hist), '') + hist.diagnostics['foo'] = generic_set.GenericSet(['baz']) + self.assertEqual(grouping.callback(hist), 'baz') + hist.diagnostics['foo'] = generic_set.GenericSet(['baz', 'bar']) + self.assertEqual(grouping.callback(hist), 'bar,baz') + + def testReservedGenericSetGroupings(self): + self.assertIsInstance( + histogram_grouping.GROUPINGS_BY_KEY[reserved_infos.ARCHITECTURES.name], + histogram_grouping.GenericSetGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.BENCHMARKS.name], histogram_grouping.GenericSetGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.BOTS.name], histogram_grouping.GenericSetGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.BUILDS.name], histogram_grouping.GenericSetGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.MASTERS.name], histogram_grouping.GenericSetGrouping) + self.assertIsInstance( + histogram_grouping.GROUPINGS_BY_KEY[reserved_infos.MEMORY_AMOUNTS.name], + histogram_grouping.GenericSetGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.OS_NAMES.name], histogram_grouping.GenericSetGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.OS_VERSIONS.name], histogram_grouping.GenericSetGrouping) + self.assertIsInstance( + histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.PRODUCT_VERSIONS.name], + histogram_grouping.GenericSetGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.STORIES.name], histogram_grouping.GenericSetGrouping) + self.assertIsInstance( + histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.STORYSET_REPEATS.name], + histogram_grouping.GenericSetGrouping) + + def testDateRange(self): + grouping = histogram_grouping.DateRangeGrouping('foo') + hist = histogram.Histogram('', 'count') + self.assertEqual(grouping.callback(hist), '') + hist.diagnostics['foo'] = date_range.DateRange(15e11) + self.assertEqual(grouping.callback(hist), str(hist.diagnostics['foo'])) + + def testReservedDateRangeGroupings(self): + self.assertIsInstance( + histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.BENCHMARK_START.name], + histogram_grouping.DateRangeGrouping) + self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[ + reserved_infos.TRACE_START.name], histogram_grouping.DateRangeGrouping) diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html new file mode 100644 index 00000000000..3a8c3356914 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/raf.html"> +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +tr.exportTo('tr.v', function() { + class HistogramImporter { + /** + * @param {!Element} loadingEl + */ + constructor(loadingEl) { + this.loadingEl_ = loadingEl; + this.histograms_ = undefined; + this.string_ = ''; + this.dataOffset_ = 0; + this.view_ = undefined; + this.fmpMark_ = tr.b.Timing.mark('HistogramImporter', 'fmp'); + + this.loadingEl_.textContent = 'Parsing HTML...'; + // The json comment appears after this script tag in results.html, so the + // browser will parse them into DOM now. + } + + /** + * @param {string} message + * @return {Promise} resolves when |message| is displayed. + */ + async update_(message) { + this.loadingEl_.textContent = message; + // Use rAF callbacks only if the document is visible. If the document is + // hidden, then the user-agent can stop triggering the rAF callbacks. So + // avoid rAF callbacks when hidden. + if (window.document.visibilityState === 'visible') { + await tr.b.animationFrame(); + } + } + + /** + * The string contains a list of histograms of the following form: + * JSON\n + * JSON\n + * ... + * The |view| should have 'display:none' so that it doesn't obnoxiously + * display "zero Histograms" while they are being imported. + * + * @param {!String} string + * @param {!Element} view A histogram-set-view. + * @return {Promise} resolves when |view| is displayed. + */ + async importHistograms(string, view) { + this.histograms_ = new tr.v.HistogramSet(); + this.string_ = string; + this.view_ = view; + tr.b.Timing.instant('HistogramImporter', 'string', this.string_.length); + + if (this.string_.length > 0) { + await this.update_('Loading Histogram 0'); + const loadMark = tr.b.Timing.mark( + 'HistogramImporter', 'loadHistograms'); + if (!this.findDataStart_()) return; + await this.loadSomeHistograms_(); + loadMark.end(); + tr.b.Timing.instant('HistogramImporter', 'nsPerJson', + parseInt(1e3 * loadMark.durationMs / this.histograms_.length)); + } + + await this.update_('Displaying Histogram table...'); + await this.displayHistograms_(); + } + + findDataStart_() { + // Find the initial data start. + this.dataOffset_ = this.string_.indexOf('\n', this.dataOffset_); + if (this.dataOffset_ < 0) return false; + // Skip over newline character. + this.dataOffset_++; + return true; + } + + async loadSomeHistograms_() { + // Don't spend so long on this chunk of Histograms that the user gets + // frustrated, but also don't call requestAnimationFrame faster than every + // 16ms, so that the browser doesn't have to wait for the next vsync. + // Powerful computers can load several hundred Histograms in 32ms. + // Also don't call performance.now() more often than necessary. + const startTime = performance.now(); + do { + for (let i = 0; i < 100; i++) { + const endIndex = this.string_.indexOf('\n', this.dataOffset_); + if (endIndex < 0) return; + const json = this.string_.substring(this.dataOffset_, endIndex); + const dict = JSON.parse(json); + this.histograms_.importDict(dict); + // Continue after last found newline character. + this.dataOffset_ = endIndex + 1; + } + } while (performance.now() - startTime < 50); + + await this.update_(`Loading Histogram ${this.histograms_.length}`); + await this.loadSomeHistograms_(); + } + + async displayHistograms_() { + this.view_.addEventListener('display-ready', async() => { + this.loadingEl_.style.display = 'none'; + this.view_.style.display = 'block'; + await tr.b.animationFrame(); + this.fmpMark_.end(); + }); + + await this.view_.build(this.histograms_, { + progress: message => this.update_(message), + helpHref: 'https://github.com/catapult-project/catapult/blob/master/docs/metrics-results-ui.md', + feedbackHref: 'https://docs.google.com/a/google.com/forms/d/e/1FAIpQLSfXvMvm_z2F9-khFaKyH_LHVZ6caPPkxI27BZqMnEt4XjyJ3g/viewform', + }); + } + } + + return { + HistogramImporter, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html new file mode 100644 index 00000000000..19213f74ba4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/timing.html"> +<link rel="import" href="/tracing/value/histogram_grouping.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +tr.exportTo('tr.v', function() { + const getDisplayLabel = + tr.v.HistogramGrouping.DISPLAY_LABEL.callback; + + const DEFAULT_POSSIBLE_GROUPS = []; + + const EXCLUDED_GROUPING_KEYS = [ + tr.v.HistogramGrouping.DISPLAY_LABEL.key, + ]; + // HISTOGRAM_NAME is overridden. + // DISPLAY_LABEL is used to define the columns, so don't allow grouping rows + // by it. + for (const group of tr.v.HistogramGrouping.BY_KEY.values()) { + if (EXCLUDED_GROUPING_KEYS.includes(group.key)) continue; + DEFAULT_POSSIBLE_GROUPS.push(group); + } + + // This Processor collects various parameters from a set of Histograms such as + // their statistics, displayLabels, and grouping keys in a single pass. + class HistogramParameterCollector { + constructor() { + this.statisticNames_ = new Set(['avg']); + + this.labelsToStartTimes_ = new Map(); + + // @typedef {!Map.<string,!tr.v.HistogramGrouping>} + this.keysToGroupings_ = new Map(DEFAULT_POSSIBLE_GROUPS.map( + g => [g.key, g])); + + // Map from HistogramGrouping keys to Sets of return values from the + // HistogramGroupings' callbacks. + this.keysToValues_ = new Map(DEFAULT_POSSIBLE_GROUPS.map( + g => [g.key, new Set()])); + + // Never remove 'name' from keysToGroupings. + this.keysToValues_.delete( + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + } + + process(histograms) { + const allStoryTags = new Set(); + let maxSampleCount = 0; + for (const hist of histograms) { + maxSampleCount = Math.max(maxSampleCount, hist.numValues); + + for (const statName of hist.statisticsNames) { + this.statisticNames_.add(statName); + } + + let startTime = hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.BENCHMARK_START); + if (startTime !== undefined) startTime = startTime.minDate.getTime(); + + const displayLabel = getDisplayLabel(hist); + + if (this.labelsToStartTimes_.has(displayLabel)) { + startTime = Math.min(startTime, + this.labelsToStartTimes_.get(displayLabel)); + } + this.labelsToStartTimes_.set(displayLabel, startTime); + + for (const [groupingKey, values] of this.keysToValues_) { + const grouping = this.keysToGroupings_.get(groupingKey); + const value = grouping.callback(hist); + if (!value) continue; + values.add(value); + if (values.size > 1) { + // This grouping will definitely stay in keysToGroupings_. We don't + // need to see any more values in the rest of histograms. Remove + // this groupingKey from this.keysToValues_ so that we don't compute + // it for any more histograms and so that we don't delete it from + // keysToGroupings_. + this.keysToValues_.delete(groupingKey); + } + } + + const storyTags = hist.diagnostics.get( + tr.v.d.RESERVED_NAMES.STORY_TAGS); + for (const tag of (storyTags || [])) { + allStoryTags.add(tag); + } + } + tr.b.Timing.instant( + 'HistogramParameterCollector', 'maxSampleCount', maxSampleCount); + + for (const tagGrouping of tr.v.HistogramGrouping.buildFromTags( + allStoryTags, tr.v.d.RESERVED_NAMES.STORY_TAGS)) { + const values = new Set(); + for (const hist of histograms) { + values.add(tagGrouping.callback(hist)); + } + if (values.size > 1) { + this.keysToGroupings_.set(tagGrouping.key, tagGrouping); + this.keysToValues_.set(tagGrouping.key, values); + } + } + + this.statisticNames_.add('pct_090'); + } + + get statisticNames() { + return Array.from(this.statisticNames_); + } + + get labels() { + const displayLabels = Array.from(this.labelsToStartTimes_.keys()); + displayLabels.sort((x, y) => + this.labelsToStartTimes_.get(x) - this.labelsToStartTimes_.get(y)); + return displayLabels; + } + + get possibleGroupings() { + for (const [key, values] of this.keysToValues_) { + if (values.size >= 2) continue; + // Remove this grouping from keysToGroupings_ if there is fewer than + // 2 possible values. + this.keysToGroupings_.delete(key); + } + + return Array.from(this.keysToGroupings_.values()); + } + } + + return { + HistogramParameterCollector, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html new file mode 100644 index 00000000000..2e508d21c92 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/histogram_parameter_collector.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + test('empty', function() { + const collector = new tr.v.HistogramParameterCollector(); + collector.process([]); + assert.lengthOf(collector.statisticNames, 2); + assert.strictEqual('avg', collector.statisticNames[0]); + assert.strictEqual('pct_090', collector.statisticNames[1]); + assert.strictEqual('name', + tr.b.getOnlyElement(collector.possibleGroupings).key); + assert.lengthOf(collector.labels, 0); + }); + + test('sortLabels', function() { + const collector = new tr.v.HistogramParameterCollector(); + collector.process([ + tr.v.Histogram.create('', tr.b.Unit.byName.count, 0, { + diagnostics: new Map([ + [ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']), + ], [ + tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1000), + ], + ]), + }), + tr.v.Histogram.create('', tr.b.Unit.byName.count, 0, { + diagnostics: new Map([ + [ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']), + ], [ + tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(3000), + ], + ]), + }), + tr.v.Histogram.create('', tr.b.Unit.byName.count, 0, { + diagnostics: new Map([ + [ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['C']), + ], [ + tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(2000), + ], + ]), + }), + ]); + const labels = collector.labels; + assert.lengthOf(labels, 3); + assert.strictEqual(labels[0], 'A'); + assert.strictEqual(labels[1], 'C'); + assert.strictEqual(labels[2], 'B'); + }); + + test('possibleGroupings', function() { + const collector = new tr.v.HistogramParameterCollector(); + collector.process([ + tr.v.Histogram.create('a', tr.b.Unit.byName.count, 0, { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['F'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1000)], + [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, new tr.v.d.GenericSet([0])], + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['R'])], + ]), + }), + tr.v.Histogram.create('b', tr.b.Unit.byName.count, 0, { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['F'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(3000)], + [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, new tr.v.d.GenericSet([1])], + [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet(['N'])], + ]), + }), + tr.v.Histogram.create('c', tr.b.Unit.byName.count, 0, { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['E', 'F'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['C'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(2000)], + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['P'])], + [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet(['M'])], + ]), + }), + ]); + + const possibleGroupingKeys = new Set( + collector.possibleGroupings.map(g => g.key)); + assert.isTrue(possibleGroupingKeys.has( + tr.v.HistogramGrouping.HISTOGRAM_NAME.key)); + assert.isTrue(possibleGroupingKeys.has( + tr.v.d.RESERVED_NAMES.BENCHMARKS)); + assert.isTrue(possibleGroupingKeys.has( + tr.v.d.RESERVED_NAMES.STORYSET_REPEATS)); + assert.isTrue(possibleGroupingKeys.has( + tr.v.d.RESERVED_NAMES.STORIES)); + assert.isFalse(possibleGroupingKeys.has( + tr.v.HistogramGrouping.DISPLAY_LABEL.key)); + assert.isFalse(possibleGroupingKeys.has( + tr.v.d.RESERVED_NAMES.TRACE_START)); + assert.isTrue(possibleGroupingKeys.has('ETag')); + assert.isFalse(possibleGroupingKeys.has('FTag')); + assert.strictEqual(possibleGroupingKeys.size, 7); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.html new file mode 100644 index 00000000000..1171233ebb1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.html @@ -0,0 +1,374 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_grouping.html"> + +<script> +'use strict'; + +tr.exportTo('tr.v', function() { + class HistogramSet { + constructor(opt_histograms) { + this.histograms_ = new Set(); + this.sharedDiagnosticsByGuid_ = new Map(); + + if (opt_histograms !== undefined) { + for (const hist of opt_histograms) { + this.addHistogram(hist); + } + } + } + + has(hist) { + return this.histograms_.has(hist); + } + + /** + * Create a Histogram, configure it, add samples to it, and add it to this + * HistogramSet. + * + * |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} + */ + createHistogram(name, unit, samples, opt_options) { + const hist = tr.v.Histogram.create(name, unit, samples, opt_options); + this.addHistogram(hist); + return hist; + } + + /** + * @param {!tr.v.Histogram} hist + * @param {(!Object|!tr.v.d.DiagnosticMap)=} opt_diagnostics + */ + addHistogram(hist, opt_diagnostics) { + if (this.has(hist)) { + throw new Error('Cannot add same Histogram twice'); + } + + if (opt_diagnostics !== undefined) { + if (!(opt_diagnostics instanceof Map)) { + opt_diagnostics = Object.entries(opt_diagnostics); + } + for (const [name, diagnostic] of opt_diagnostics) { + hist.diagnostics.set(name, diagnostic); + } + } + + this.histograms_.add(hist); + } + + /** + * Add a Diagnostic to all Histograms so that it will only be serialized + * once per HistogramSet rather than once per Histogram that contains it. + * + * @param {string} name + * @param {!tr.v.d.Diagnostic} diagnostic + */ + addSharedDiagnosticToAllHistograms(name, diagnostic) { + this.addSharedDiagnostic(diagnostic); + for (const hist of this) { + hist.diagnostics.set(name, diagnostic); + } + } + + /** + * Add a Diagnostic to this HistogramSet so that it will only be serialized + * once per HistogramSet rather than once per Histogram that contains it. + * + * @param {!tr.v.d.Diagnostic} diagnostic + */ + addSharedDiagnostic(diagnostic) { + this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic); + } + + get length() { + return this.histograms_.size; + } + + * [Symbol.iterator]() { + for (const hist of this.histograms_) { + yield hist; + } + } + + /** + * Filters Histograms by matching their name exactly. + * + * @param {string} name Histogram name. + * @return {!Array.<!tr.v.Histogram>} + */ + getHistogramsNamed(name) { + return [...this].filter(h => h.name === name); + } + + /** + * Filters to find the Histogram that matches the specified name exactly. + * If no Histogram with that name exists, undefined is returned. If multiple + * Histograms with the name exist, an error is thrown. + * + * @param {string} name Histogram name. + * @return {tr.v.Histogram} + */ + getHistogramNamed(name) { + const histograms = this.getHistogramsNamed(name); + if (histograms.length === 0) return undefined; + if (histograms.length > 1) { + throw new Error( + `Unexpectedly found multiple histograms named "${name}"`); + } + + return histograms[0]; + } + + /** + * Lookup a Diagnostic by its guid. + * + * @param {string} guid + * @return {!tr.v.d.Diagnostic|undefined} + */ + lookupDiagnostic(guid) { + return this.sharedDiagnosticsByGuid_.get(guid); + } + + /** + * Convert dicts to either Histograms or shared Diagnostics. + * + * @param {!Object} dicts + */ + importDicts(dicts) { + for (const dict of dicts) { + this.importDict(dict); + } + } + + /** + * Convert dict to either a Histogram or a shared Diagnostic. + * + * @param {!Object} dict + */ + importDict(dict) { + if (dict.type !== undefined) { + // TODO(benjhayden): Forget about TagMaps in 2019Q2. + if (dict.type === 'TagMap') return; + + if (!tr.v.d.Diagnostic.findTypeInfoWithName(dict.type)) { + throw new Error('Unrecognized shared diagnostic type ' + dict.type); + } + this.sharedDiagnosticsByGuid_.set(dict.guid, + tr.v.d.Diagnostic.fromDict(dict)); + } else { + const hist = tr.v.Histogram.fromDict(dict); + this.addHistogram(hist); + hist.diagnostics.resolveSharedDiagnostics(this, true); + } + } + + /** + * Serialize all of the Histograms and shared Diagnostics to an Array of + * dictionaries. + * + * @return {!Array.<!Object>} + */ + asDicts() { + const dicts = []; + for (const diagnostic of this.sharedDiagnosticsByGuid_.values()) { + dicts.push(diagnostic.asDict()); + } + for (const hist of this) { + dicts.push(hist.asDict()); + } + return dicts; + } + + /** + * Find the Histograms whose names are not contained in any other + * Histograms' RelatedNameMap diagnostics. + * + * @return {!Array.<!tr.v.Histogram>} + */ + get sourceHistograms() { + const diagnosticNames = new Set(); + for (const hist of this) { + for (const diagnostic of hist.diagnostics.values()) { + if (!(diagnostic instanceof tr.v.d.RelatedNameMap)) continue; + for (const name of diagnostic.values()) { + diagnosticNames.add(name); + } + } + } + + const sourceHistograms = new HistogramSet; + for (const hist of this) { + if (!diagnosticNames.has(hist.name)) { + sourceHistograms.addHistogram(hist); + } + } + return sourceHistograms; + } + + /** + * Return a nested Map, whose keys are strings and leaf values are Arrays of + * Histograms. + * See GROUPINGS for example |groupings|. + * Groupings are skipped when |opt_skipGroupingCallback| is specified and + * returns true. + * + * @typedef {!Array.<tr.v.Histogram>} HistogramArray + * @typedef {!Map.<string,!(HistogramArray|HistogramArrayMap)>} + * HistogramArrayMap + * @typedef {!Map.<string,!HistogramArray>} LeafHistogramArrayMap + * + * @param {!Array.<!tr.v.HistogramGrouping>} groupings + * @param {!function(!Grouping, !LeafHistogramArrayMap):boolean=} + * opt_skipGroupingCallback + * + * @return {!(HistogramArray|HistogramArrayMap)} + */ + groupHistogramsRecursively(groupings, opt_skipGroupingCallback) { + function recurse(histograms, level) { + if (level === groupings.length) { + return histograms; // recursion base case + } + + const grouping = groupings[level]; + const groupedHistograms = tr.b.groupIntoMap( + histograms, grouping.callback); + + if (opt_skipGroupingCallback && opt_skipGroupingCallback( + grouping, groupedHistograms)) { + return recurse(histograms, level + 1); + } + + for (const [key, group] of groupedHistograms) { + groupedHistograms.set(key, recurse(group, level + 1)); + } + + return groupedHistograms; + } + + return recurse([...this], 0); + } + + /* + * Histograms and Diagnostics are merged two at a time, without considering + * any others, so it is possible for two merged Diagnostics to be equivalent + * but not identical, which is inefficient. This method replaces equivalent + * Diagnostics with shared Diagnostics so that the HistogramSet can be + * serialized more efficiently and so that these Diagnostics can be compared + * quickly when merging relationship Diagnostics. + */ + deduplicateDiagnostics() { + const namesToCandidates = new Map(); // string: Set<Diagnostic> + const diagnosticsToHistograms = new Map(); // Diagnostic: [Histogram] + const keysToDiagnostics = new Map(); // string: Diagnostic + + for (const hist of this) { + for (const [name, candidate] of hist.diagnostics) { + // TODO(#3695): Remove this check once equality is smoke-tested. + if (candidate.equals === undefined) { + this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate); + continue; + } + + const hashKey = candidate.hashKey; + if (candidate.hashKey !== undefined) { + // TODO(857283): Fall back to slow path if same name but diff type + if (keysToDiagnostics.has(hashKey)) { + hist.diagnostics.set(name, keysToDiagnostics.get(hashKey)); + } else { + keysToDiagnostics.set(hashKey, candidate); + this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate); + } + + continue; + } + + if (diagnosticsToHistograms.get(candidate) === undefined) { + diagnosticsToHistograms.set(candidate, [hist]); + } else { + diagnosticsToHistograms.get(candidate).push(hist); + } + + if (!namesToCandidates.has(name)) { + namesToCandidates.set(name, new Set()); + } + namesToCandidates.get(name).add(candidate); + } + } + + for (const [name, candidates] of namesToCandidates) { + const deduplicatedDiagnostics = new Set(); + + for (const candidate of candidates) { + let found = false; + for (const test of deduplicatedDiagnostics) { + if (candidate.equals(test)) { + const hists = diagnosticsToHistograms.get(candidate); + for (const hist of hists) { + hist.diagnostics.set(name, test); + } + found = true; + break; + } + } + if (!found) { + deduplicatedDiagnostics.add(candidate); + } + + for (const diagnostic of deduplicatedDiagnostics) { + this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic); + } + } + } + } + + /** + * @param {!Iterable.<string>} names of GenericSet diagnostics + * @return {!Array.<!tr.v.HistogramGrouping>} + */ + buildGroupingsFromTags(names) { + const tags = new Map(); // name: Set<string> + for (const hist of this) { + for (const name of names) { + if (!hist.diagnostics.has(name)) continue; + if (!tags.has(name)) tags.set(name, new Set()); + for (const tag of hist.diagnostics.get(name)) { + tags.get(name).add(tag); + } + } + } + + const groupings = []; + for (const [name, values] of tags) { + const built = tr.v.HistogramGrouping.buildFromTags(values, name); + for (const grouping of built) { + groupings.push(grouping); + } + } + return groupings; + } + } + + return {HistogramSet}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.py new file mode 100644 index 00000000000..b9a5d795c37 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.py @@ -0,0 +1,155 @@ +# Copyright 2017 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. + +import collections + +from tracing.value import histogram as histogram_module +from tracing.value.diagnostics import all_diagnostics +from tracing.value.diagnostics import diagnostic +from tracing.value.diagnostics import diagnostic_ref +from tracing.value.diagnostics import generic_set + +class HistogramSet(object): + def __init__(self, histograms=()): + self._histograms = set() + self._shared_diagnostics_by_guid = {} + for hist in histograms: + self.AddHistogram(hist) + + @property + def shared_diagnostics(self): + return self._shared_diagnostics_by_guid.values() + + def RemoveOrphanedDiagnostics(self): + orphans = set(self._shared_diagnostics_by_guid.keys()) + for h in self._histograms: + for d in h.diagnostics.values(): + if d.guid in orphans: + orphans.remove(d.guid) + for guid in orphans: + del self._shared_diagnostics_by_guid[guid] + + def FilterHistograms(self, discard): + self._histograms = set( + hist + for hist in self._histograms + if not discard(hist)) + + def AddHistogram(self, hist, diagnostics=None): + if diagnostics: + for name, diag in diagnostics.items(): + hist.diagnostics[name] = diag + + self._histograms.add(hist) + + def AddSharedDiagnostic(self, diag): + self._shared_diagnostics_by_guid[diag.guid] = diag + + def AddSharedDiagnosticToAllHistograms(self, name, diag): + self._shared_diagnostics_by_guid[diag.guid] = diag + + for hist in self: + hist.diagnostics[name] = diag + + def GetFirstHistogram(self): + for histogram in self._histograms: + return histogram + + def GetHistogramsNamed(self, name): + return [h for h in self if h.name == name] + + def GetHistogramNamed(self, name): + hs = self.GetHistogramsNamed(name) + assert len(hs) == 1, 'Found %d Histograms names "%s"' % (len(hs), name) + return hs[0] + + def GetSharedDiagnosticsOfType(self, typ): + return [d for d in self.shared_diagnostics if isinstance(d, typ)] + + def LookupDiagnostic(self, guid): + return self._shared_diagnostics_by_guid.get(guid) + + def __len__(self): + return len(self._histograms) + + def __iter__(self): + for hist in self._histograms: + yield hist + + def ImportDicts(self, dicts): + for d in dicts: + if 'type' in d: + # TODO(benjhayden): Forget about TagMaps in 2019Q2. + if d['type'] == 'TagMap': + continue + + assert d['type'] in all_diagnostics.GetDiagnosticTypenames(), ( + 'Unrecognized shared diagnostic type ' + d['type']) + diag = diagnostic.Diagnostic.FromDict(d) + self._shared_diagnostics_by_guid[d['guid']] = diag + else: + hist = histogram_module.Histogram.FromDict(d) + hist.diagnostics.ResolveSharedDiagnostics(self) + self.AddHistogram(hist) + + def AsDicts(self): + dcts = [] + for d in self._shared_diagnostics_by_guid.values(): + dcts.append(d.AsDict()) + for h in self: + dcts.append(h.AsDict()) + return dcts + + def ReplaceSharedDiagnostic(self, old_guid, new_diagnostic): + if not isinstance(new_diagnostic, diagnostic_ref.DiagnosticRef): + self._shared_diagnostics_by_guid[new_diagnostic.guid] = new_diagnostic + + old_diagnostic = self._shared_diagnostics_by_guid.get(old_guid) + + # Fast path, if they're both generic_sets, we overwrite the contents of the + # old diagnostic. + if isinstance(new_diagnostic, generic_set.GenericSet) and ( + isinstance(old_diagnostic, generic_set.GenericSet)): + old_diagnostic.SetValues(list(new_diagnostic)) + old_diagnostic.ResetGuid(new_diagnostic.guid) + + self._shared_diagnostics_by_guid[new_diagnostic.guid] = old_diagnostic + del self._shared_diagnostics_by_guid[old_guid] + + return + + for hist in self: + for name, diag in hist.diagnostics.items(): + if diag.has_guid and diag.guid == old_guid: + hist.diagnostics[name] = new_diagnostic + + def DeduplicateDiagnostics(self): + names_to_candidates = {} + diagnostics_to_histograms = collections.defaultdict(list) + + for hist in self: + for name, candidate in hist.diagnostics.items(): + diagnostics_to_histograms[candidate].append(hist) + + if name not in names_to_candidates: + names_to_candidates[name] = set() + names_to_candidates[name].add(candidate) + + for name, candidates in names_to_candidates.items(): + deduplicated_diagnostics = set() + + for candidate in candidates: + found = False + for test in deduplicated_diagnostics: + if candidate == test: + hists = diagnostics_to_histograms.get(candidate) + for h in hists: + h.diagnostics[name] = test + found = True + break + if not found: + deduplicated_diagnostics.add(candidate) + + for diag in deduplicated_diagnostics: + self._shared_diagnostics_by_guid[diag.guid] = diag diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html new file mode 100644 index 00000000000..86a3765fbfe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html @@ -0,0 +1,157 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/histogram_set.html"> + +<script> +'use strict'; +tr.exportTo('tr.v', function() { + /* + * See also HistogramSet.groupHistogramsRecursively(). + * See also tr.v.ui.HistogramSetTableRow. + */ + class HistogramSetHierarchy { + /** + * @param {string} name + */ + constructor(name) { + this.name = name; + this.description = ''; + this.depth = 0; + this.subRows = []; + this.columns = new Map(); + } + + * walk() { + yield this; + for (const row of this.subRows) yield* row.walk(); + } + + static* walkAll(rootRows) { + for (const rootRow of rootRows) yield* rootRow.walk(); + } + + /** + * Build table rows recursively from grouped Histograms. + * + * @param {!(HistogramArray|HistogramArrayMap)} + * @returns {!Array.<!HistogramSetHierarchy>} + */ + static build(histogramArrayMap) { + const rootRows = []; + HistogramSetHierarchy.buildInternal_(histogramArrayMap, [], rootRows); + + const histograms = new tr.v.HistogramSet(); + + for (const row of HistogramSetHierarchy.walkAll(rootRows)) { + for (const hist of row.columns.values()) { + if (!(hist instanceof tr.v.Histogram)) continue; + histograms.addHistogram(hist); + } + } + + histograms.deduplicateDiagnostics(); + + for (const row of HistogramSetHierarchy.walkAll(rootRows)) { + row.maybeRebin_(); + } + + return rootRows; + } + + maybeRebin_() { + // if all of |this| row's columns are single-bin, then re-bin all of them. + const dataRange = new tr.b.math.Range(); + for (const hist of this.columns.values()) { + if (!(hist instanceof tr.v.Histogram)) continue; + if (hist.allBins.length > 1) return; // don't re-bin + if (hist.numValues === 0) continue; // ignore hist + dataRange.addValue(hist.min); + dataRange.addValue(hist.max); + } + + dataRange.addValue(tr.b.math.lesserWholeNumber(dataRange.min)); + dataRange.addValue(tr.b.math.greaterWholeNumber(dataRange.max)); + + if (dataRange.min === dataRange.max) return; // don't rebin + + const boundaries = tr.v.HistogramBinBoundaries.createLinear( + dataRange.min, dataRange.max, tr.v.DEFAULT_REBINNED_COUNT); + + for (const [name, hist] of this.columns) { + if (!(hist instanceof tr.v.Histogram)) continue; + this.columns.set(name, hist.rebin(boundaries)); + } + } + + static mergeHistogramDownHierarchy_(histogram, hierarchy, columnName) { + // Track the path down the grouping tree to each Histogram, + // but only start tracking the path at the grouping level that + // corresponds to the Histogram NAME Grouping. + for (const row of hierarchy) { + if (!row.description) { + row.description = histogram.description; + } + + const existing = row.columns.get(columnName); + + if (existing === undefined) { + row.columns.set(columnName, histogram.clone()); + continue; + } + + if (existing instanceof tr.v.HistogramSet) { + // There have already been unmergeable histograms. + existing.addHistogram(histogram); + continue; + } + + if (!existing.canAddHistogram(histogram)) { + // TODO(benjhayden) Remove? + const unmergeableHistograms = new tr.v.HistogramSet([histogram]); + row.columns.set(columnName, unmergeableHistograms); + continue; + } + + existing.addHistogram(histogram); + } + } + + static buildInternal_( + histogramArrayMap, hierarchy, rootRows) { + for (const [name, histograms] of histogramArrayMap) { + if (histograms instanceof Array) { + // This recursion base case corresponds to the recursion base case of + // groupHistogramsRecursively(). The last groupingCallback is always + // getDisplayLabel, which defines the columns of the table and is + // unskippable. + for (const histogram of histograms) { + HistogramSetHierarchy.mergeHistogramDownHierarchy_( + histogram, hierarchy, name); + } + } else if (histograms instanceof Map) { + // |histograms| is actually a nested histogramArrayMap. + const row = new HistogramSetHierarchy(name); + row.depth = hierarchy.length; + hierarchy.push(row); + HistogramSetHierarchy.buildInternal_(histograms, hierarchy, rootRows); + hierarchy.pop(); + + if (hierarchy.length === 0) { + rootRows.push(row); + } else { + const parentRow = hierarchy[hierarchy.length - 1]; + parentRow.subRows.push(row); + } + } + } + } + } + + return {HistogramSetHierarchy}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html new file mode 100644 index 00000000000..2db75bfba45 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html @@ -0,0 +1,221 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/diagnostics/generic_set.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + // TODO(#3812) Test groupHistogramsRecursively. + + test('assertType', function() { + const hs = new tr.v.HistogramSet(); + assert.throws(() => hs.importDict({type: ''}), + Error, 'Unrecognized shared diagnostic type '); + }); + + test('ignoreTagMap', function() { + const hs = new tr.v.HistogramSet(); + hs.importDict({type: 'TagMap'}); + }); + + test('importDicts', function() { + const n = new tr.v.Histogram('foo', tr.b.Unit.byName.unitlessNumber); + const histograms = new tr.v.HistogramSet([n]); + const histograms2 = new tr.v.HistogramSet(); + histograms2.importDicts(histograms.asDicts()); + assert.isDefined(histograms2.getHistogramNamed('foo')); + }); + + test('importDictsWithSampleDiagnostic', function() { + const n = new tr.v.Histogram('foo', tr.b.Unit.byName.count); + n.addSample(10, {bar: new tr.v.d.GenericSet(['baz'])}); + + const histograms = new tr.v.HistogramSet([n]); + const histograms2 = new tr.v.HistogramSet(); + histograms2.importDicts(histograms.asDicts()); + assert.isDefined(histograms2.getHistogramNamed('foo')); + const v = histograms2.getHistogramNamed('foo'); + assert.lengthOf(v.getBinForValue(10).diagnosticMaps, 1); + const dm = v.getBinForValue(10).diagnosticMaps[0]; + assert.strictEqual(dm.size, 1); + assert.instanceOf(dm.get('bar'), tr.v.d.GenericSet); + assert.strictEqual(tr.b.getOnlyElement(dm.get('bar')), 'baz'); + }); + + test('sourceHistogramsWithSampleDiagnostic', function() { + const unit = tr.b.Unit.byName.unitlessNumber; + const aHist = new tr.v.Histogram('a', unit); + aHist.addSample(1); + + const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.unitlessNumber); + const related = new tr.v.d.RelatedNameMap(); + related.set('0', aHist.name); + bHist.diagnostics.set('related', related); + bHist.addSample(1); + + const histograms = new tr.v.HistogramSet([aHist, bHist]); + assert.strictEqual(tr.b.getOnlyElement(histograms.sourceHistograms), bHist); + }); + + test('sourceHistogramsWithNameMap', function() { + const unit = tr.b.Unit.byName.unitlessNumber; + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('A', unit, []); + const bHist = histograms.createHistogram('B', unit, [], {diagnostics: { + related: tr.v.d.RelatedNameMap.fromEntries([['a', 'A']]), + }}); + assert.strictEqual(tr.b.getOnlyElement(histograms.sourceHistograms), bHist); + }); + + test('sharedDiagnostic', function() { + // Make a single Histogram, add a shared Diagnostic. + const aHist = new tr.v.Histogram('aHist', tr.b.Unit.byName.count); + const histograms = new tr.v.HistogramSet([aHist]); + const diagnostic = new tr.v.d.GenericSet(['shared']); + histograms.addSharedDiagnosticToAllHistograms('generic', diagnostic); + + // Serializing a single Histogram with a single shared diagnostic should + // produce 2 dicts. + const dicts = histograms.asDicts(); + assert.lengthOf(dicts, 2); + assert.deepEqual(diagnostic.asDict(), dicts[0]); + + // The serialized Histogram should refer to the shared diagnostic by its + // guid. + assert.strictEqual(dicts[1].diagnostics.generic, diagnostic.guid); + + // Deserialize the dicts. + const histograms2 = new tr.v.HistogramSet(); + histograms2.importDicts(dicts); + assert.lengthOf(histograms2, 1); + const aHist2 = histograms2.getHistogramNamed(aHist.name); + + assert.instanceOf(aHist2.diagnostics.get('generic'), tr.v.d.GenericSet); + assert.strictEqual(tr.b.getOnlyElement(diagnostic), + tr.b.getOnlyElement(aHist2.diagnostics.get('generic'))); + }); + + test('getHistogramNamed_noHistogramFound', function() { + const aHist = new tr.v.Histogram('aHist', tr.b.Unit.byName.count); + const histograms = new tr.v.HistogramSet([aHist]); + + assert.isUndefined(histograms.getHistogramNamed('bHist')); + }); + + test('getHistogramNamed_oneHistogramFound', function() { + const aHist = new tr.v.Histogram('aHist', tr.b.Unit.byName.count); + const histograms = new tr.v.HistogramSet([aHist]); + + assert.strictEqual(histograms.getHistogramNamed('aHist'), aHist); + }); + + test('getHistogramNamed_multipleHistogramsFound', function() { + const aHist1 = new tr.v.Histogram('aHist', tr.b.Unit.byName.count); + const aHist2 = new tr.v.Histogram('aHist', tr.b.Unit.byName.count); + const histograms = new tr.v.HistogramSet([aHist1, aHist2]); + + assert.throws(() => histograms.getHistogramNamed('aHist'), + Error, 'Unexpectedly found multiple histograms named "aHist"'); + }); + + test('deduplicateDiagnostics', function() { + const genericA = new tr.v.d.GenericSet(['A']); + const genericB = new tr.v.d.GenericSet(['B']); + const dateA = new tr.v.d.DateRange(42); + const dateB = new tr.v.d.DateRange(57); + + const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.count); + const generic0 = genericA.clone(); + generic0.addDiagnostic(genericB); + aHist.diagnostics.set('generic', generic0); + const date0 = dateA.clone(); + date0.addDiagnostic(dateB); + aHist.diagnostics.set('date', date0); + + const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.count); + const generic1 = genericA.clone(); + generic1.addDiagnostic(genericB); + bHist.diagnostics.set('generic', generic1); + const date1 = dateA.clone(); + date1.addDiagnostic(dateB); + bHist.diagnostics.set('date', date1); + + const cHist = new tr.v.Histogram('c', tr.b.Unit.byName.count); + cHist.diagnostics.set('generic', generic1); + + const histograms = new tr.v.HistogramSet([aHist, bHist, cHist]); + assert.notStrictEqual( + aHist.diagnostics.get('generic'), bHist.diagnostics.get('generic')); + assert.strictEqual( + bHist.diagnostics.get('generic'), cHist.diagnostics.get('generic')); + assert.isTrue( + aHist.diagnostics.get('generic').equals( + bHist.diagnostics.get('generic'))); + assert.notStrictEqual( + aHist.diagnostics.get('date'), bHist.diagnostics.get('date')); + assert.isTrue( + aHist.diagnostics.get('date').equals(bHist.diagnostics.get('date'))); + + histograms.deduplicateDiagnostics(); + + assert.strictEqual( + aHist.diagnostics.get('generic'), bHist.diagnostics.get('generic')); + assert.strictEqual( + bHist.diagnostics.get('generic'), cHist.diagnostics.get('generic')); + assert.strictEqual( + aHist.diagnostics.get('date'), bHist.diagnostics.get('date')); + + const histogramDicts = histograms.asDicts(); + + // All diagnostics should have been serialized as DiagnosticRefs. + for (const dict of histogramDicts) { + if (!('type' in dict)) { + for (const diagnosticDict of Object.values(dict.diagnostics)) { + assert.strictEqual(typeof(diagnosticDict), 'string'); + } + } + } + + const histograms2 = new tr.v.HistogramSet(); + histograms2.importDicts(histogramDicts); + const aHist2 = histograms2.getHistogramNamed('a'); + const bHist2 = histograms2.getHistogramNamed('b'); + + assert.strictEqual( + aHist2.diagnostics.get('generic'), bHist2.diagnostics.get('generic')); + assert.strictEqual( + aHist2.diagnostics.get('date'), bHist2.diagnostics.get('date')); + }); + + test('buildGroupingsFromTags', function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['a'])], + ]), + }); + const bHist = histograms.createHistogram('', tr.b.Unit.byName.count, [], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['b'])], + ]), + }); + const groupings = histograms.buildGroupingsFromTags([ + tr.v.d.RESERVED_NAMES.STORY_TAGS]); + assert.lengthOf(groupings, 2); + assert.strictEqual(groupings[0].callback(aHist), 'a'); + assert.strictEqual(groupings[0].callback(bHist), '~a'); + assert.strictEqual(groupings[1].callback(aHist), '~b'); + assert.strictEqual(groupings[1].callback(bHist), 'b'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py new file mode 100644 index 00000000000..39f6e45a3ec --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py @@ -0,0 +1,236 @@ +# Copyright 2017 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. + +import unittest + +from tracing.value import histogram +from tracing.value import histogram_set +from tracing.value.diagnostics import date_range +from tracing.value.diagnostics import diagnostic_ref +from tracing.value.diagnostics import generic_set + +class HistogramSetUnittest(unittest.TestCase): + + def testGetSharedDiagnosticsOfType(self): + d0 = generic_set.GenericSet(['foo']) + d1 = date_range.DateRange(0) + hs = histogram_set.HistogramSet() + hs.AddSharedDiagnosticToAllHistograms('generic', d0) + hs.AddSharedDiagnosticToAllHistograms('generic', d1) + diagnostics = hs.GetSharedDiagnosticsOfType(generic_set.GenericSet) + self.assertEqual(len(diagnostics), 1) + self.assertIsInstance(diagnostics[0], generic_set.GenericSet) + + def testImportDicts(self): + hist = histogram.Histogram('', 'unitless') + hists = histogram_set.HistogramSet([hist]) + hists2 = histogram_set.HistogramSet() + hists2.ImportDicts(hists.AsDicts()) + self.assertEqual(len(hists), len(hists2)) + + def testAssertType(self): + hs = histogram_set.HistogramSet() + with self.assertRaises(AssertionError): + hs.ImportDicts([{'type': ''}]) + + def testIgnoreTagMap(self): + histogram_set.HistogramSet().ImportDicts([{'type': 'TagMap'}]) + + def testFilterHistogram(self): + a = histogram.Histogram('a', 'unitless') + b = histogram.Histogram('b', 'unitless') + c = histogram.Histogram('c', 'unitless') + hs = histogram_set.HistogramSet([a, b, c]) + hs.FilterHistograms(lambda h: h.name == 'b') + + names = set(['a', 'c']) + for h in hs: + self.assertIn(h.name, names) + names.remove(h.name) + self.assertEqual(0, len(names)) + + def testRemoveOrphanedDiagnostics(self): + da = generic_set.GenericSet(['a']) + db = generic_set.GenericSet(['b']) + a = histogram.Histogram('a', 'unitless') + b = histogram.Histogram('b', 'unitless') + hs = histogram_set.HistogramSet([a]) + hs.AddSharedDiagnosticToAllHistograms('a', da) + hs.AddHistogram(b) + hs.AddSharedDiagnosticToAllHistograms('b', db) + hs.FilterHistograms(lambda h: h.name == 'a') + + dicts = hs.AsDicts() + self.assertEqual(3, len(dicts)) + + hs.RemoveOrphanedDiagnostics() + dicts = hs.AsDicts() + self.assertEqual(2, len(dicts)) + + def testAddSharedDiagnostic(self): + diags = {} + da = generic_set.GenericSet(['a']) + db = generic_set.GenericSet(['b']) + diags['da'] = da + diags['db'] = db + a = histogram.Histogram('a', 'unitless') + b = histogram.Histogram('b', 'unitless') + hs = histogram_set.HistogramSet() + hs.AddSharedDiagnostic(da) + hs.AddHistogram(a, {'da': da}) + hs.AddHistogram(b, {'db': db}) + + # This should produce one shared diagnostic and 2 histograms. + dicts = hs.AsDicts() + self.assertEqual(3, len(dicts)) + self.assertEqual(da.AsDict(), dicts[0]) + + + # Assert that you only see the shared diagnostic once. + seen_once = False + for idx, val in enumerate(dicts): + if idx == 0: + continue + if 'da' in val['diagnostics']: + self.assertFalse(seen_once) + self.assertEqual(val['diagnostics']['da'], da.guid) + seen_once = True + + + def testSharedDiagnostic(self): + hist = histogram.Histogram('', 'unitless') + hists = histogram_set.HistogramSet([hist]) + diag = generic_set.GenericSet(['shared']) + hists.AddSharedDiagnosticToAllHistograms('generic', diag) + + # Serializing a single Histogram with a single shared diagnostic should + # produce 2 dicts. + ds = hists.AsDicts() + self.assertEqual(len(ds), 2) + self.assertEqual(diag.AsDict(), ds[0]) + + # The serialized Histogram should refer to the shared diagnostic by its + # guid. + self.assertEqual(ds[1]['diagnostics']['generic'], diag.guid) + + # Deserialize ds. + hists2 = histogram_set.HistogramSet() + hists2.ImportDicts(ds) + self.assertEqual(len(hists2), 1) + hist2 = [h for h in hists2][0] + + self.assertIsInstance( + hist2.diagnostics.get('generic'), generic_set.GenericSet) + self.assertEqual(list(diag), list(hist2.diagnostics.get('generic'))) + + def testReplaceSharedDiagnostic(self): + hist = histogram.Histogram('', 'unitless') + hists = histogram_set.HistogramSet([hist]) + diag0 = generic_set.GenericSet(['shared0']) + diag1 = generic_set.GenericSet(['shared1']) + hists.AddSharedDiagnosticToAllHistograms('generic0', diag0) + hists.AddSharedDiagnosticToAllHistograms('generic1', diag1) + + guid0 = diag0.guid + guid1 = diag1.guid + + hists.ReplaceSharedDiagnostic( + guid0, diagnostic_ref.DiagnosticRef('fakeGuid')) + + self.assertEqual(hist.diagnostics['generic0'].guid, 'fakeGuid') + self.assertEqual(hist.diagnostics['generic1'].guid, guid1) + + def testReplaceSharedDiagnostic_NonRefAddsToMap(self): + hist = histogram.Histogram('', 'unitless') + hists = histogram_set.HistogramSet([hist]) + diag0 = generic_set.GenericSet(['shared0']) + diag1 = generic_set.GenericSet(['shared1']) + hists.AddSharedDiagnosticToAllHistograms('generic0', diag0) + + guid0 = diag0.guid + guid1 = diag1.guid + + hists.ReplaceSharedDiagnostic(guid0, diag1) + + self.assertIsNotNone(hists.LookupDiagnostic(guid1)) + + def testDeduplicateDiagnostics(self): + generic_a = generic_set.GenericSet(['A']) + generic_b = generic_set.GenericSet(['B']) + date_a = date_range.DateRange(42) + date_b = date_range.DateRange(57) + + a_hist = histogram.Histogram('a', 'unitless') + generic0 = generic_set.GenericSet.FromDict(generic_a.AsDict()) + generic0.AddDiagnostic(generic_b) + a_hist.diagnostics['generic'] = generic0 + date0 = date_range.DateRange.FromDict(date_a.AsDict()) + date0.AddDiagnostic(date_b) + a_hist.diagnostics['date'] = date0 + + b_hist = histogram.Histogram('b', 'unitless') + generic1 = generic_set.GenericSet.FromDict(generic_a.AsDict()) + generic1.AddDiagnostic(generic_b) + b_hist.diagnostics['generic'] = generic1 + date1 = date_range.DateRange.FromDict(date_a.AsDict()) + date1.AddDiagnostic(date_b) + b_hist.diagnostics['date'] = date1 + + c_hist = histogram.Histogram('c', 'unitless') + c_hist.diagnostics['generic'] = generic1 + + histograms = histogram_set.HistogramSet([a_hist, b_hist, c_hist]) + self.assertNotEqual( + a_hist.diagnostics['generic'].guid, b_hist.diagnostics['generic'].guid) + self.assertEqual( + b_hist.diagnostics['generic'].guid, c_hist.diagnostics['generic'].guid) + self.assertEqual( + a_hist.diagnostics['generic'], b_hist.diagnostics['generic']) + self.assertNotEqual( + a_hist.diagnostics['date'].guid, b_hist.diagnostics['date'].guid) + self.assertEqual( + a_hist.diagnostics['date'], b_hist.diagnostics['date']) + + histograms.DeduplicateDiagnostics() + + self.assertEqual( + a_hist.diagnostics['generic'].guid, b_hist.diagnostics['generic'].guid) + self.assertEqual( + b_hist.diagnostics['generic'].guid, c_hist.diagnostics['generic'].guid) + self.assertEqual( + a_hist.diagnostics['generic'], b_hist.diagnostics['generic']) + self.assertEqual( + a_hist.diagnostics['date'].guid, b_hist.diagnostics['date'].guid) + self.assertEqual( + a_hist.diagnostics['date'], b_hist.diagnostics['date']) + + histogram_dicts = histograms.AsDicts() + + # All diagnostics should have been serialized as DiagnosticRefs. + for d in histogram_dicts: + if 'type' not in d: + for diagnostic_dict in d['diagnostics'].values(): + self.assertIsInstance(diagnostic_dict, str) + + histograms2 = histogram_set.HistogramSet() + histograms2.ImportDicts(histograms.AsDicts()) + a_hists = histograms2.GetHistogramsNamed('a') + self.assertEqual(len(a_hists), 1) + a_hist2 = a_hists[0] + b_hists = histograms2.GetHistogramsNamed('b') + self.assertEqual(len(b_hists), 1) + b_hist2 = b_hists[0] + + self.assertEqual( + a_hist2.diagnostics['generic'].guid, + b_hist2.diagnostics['generic'].guid) + self.assertEqual( + a_hist2.diagnostics['generic'], + b_hist2.diagnostics['generic']) + self.assertEqual( + a_hist2.diagnostics['date'].guid, + b_hist2.diagnostics['date'].guid) + self.assertEqual( + a_hist2.diagnostics['date'], + b_hist2.diagnostics['date']) diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_test.html new file mode 100644 index 00000000000..16f7b948272 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_test.html @@ -0,0 +1,809 @@ +<!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/assert_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/generic_set.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const unitlessNumber = tr.b.Unit.byName.unitlessNumber; + const unitlessNumber_smallerIsBetter = + tr.b.Unit.byName.unitlessNumber_smallerIsBetter; + + const TEST_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 1000, 10); + + function checkBoundaries(boundaries, expectedMinBoundary, expectedMaxBoundary, + expectedUnit, expectedBinRanges) { + assert.strictEqual(boundaries.range.min, expectedMinBoundary); + assert.strictEqual(boundaries.range.max, expectedMaxBoundary); + + // Check that the boundaries can be used multiple times. + for (let i = 0; i < 3; i++) { + const hist = new tr.v.Histogram('', expectedUnit, boundaries); + assert.instanceOf(hist, tr.v.Histogram); + assert.strictEqual(hist.unit, expectedUnit); + assert.strictEqual(hist.numValues, 0); + + assert.lengthOf(hist.allBins, expectedBinRanges.length); + for (let j = 0; j < expectedBinRanges.length; j++) { + const bin = hist.allBins[j]; + assert.strictEqual(bin.count, 0); + assert.isTrue(bin.range.equals(expectedBinRanges[j])); + } + } + } + + test('truncateBreakdowns', function() { + const hist = tr.v.Histogram.create('a', unitlessNumber, { + value: 1, + diagnostics: {b: tr.v.d.Breakdown.fromEntries([ + ['c', 1 / 3], + ])}, + }, { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + }); + assert.strictEqual(0.3333, hist.allBins[0].diagnosticMaps[0].get( + 'b').get('c')); + }); + + test('createWithNameUnitNumber', function() { + const hist = tr.v.Histogram.create('a', unitlessNumber, 1); + assert.strictEqual(hist.name, 'a'); + assert.strictEqual(hist.unit, unitlessNumber); + assert.lengthOf(hist.sampleValues, 1); + assert.strictEqual(hist.average, 1); + }); + + test('createWithSamples', function() { + const hist = tr.v.Histogram.create('', unitlessNumber, [ + 1, + {value: 3, diagnostics: {a: new tr.v.d.GenericSet(['b'])}}, + ]); + assert.lengthOf(hist.sampleValues, 2); + assert.strictEqual(hist.average, 2); + + const bin = hist.getBinForValue(3); + assert.lengthOf(bin.diagnosticMaps, 1); + const sampleDiagnostics = tr.b.getOnlyElement(bin.diagnosticMaps); + assert.strictEqual(tr.b.getOnlyElement(sampleDiagnostics.get('a')), 'b'); + }); + + test('createWithOptions', function() { + const hist = tr.v.Histogram.create('', unitlessNumber, [], { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + description: 'foo', + diagnostics: { + generic: new tr.v.d.GenericSet(['occam']), + }, + summaryOptions: { + count: false, + percentile: [0.5], + } + }); + assert.strictEqual(hist.description, 'foo'); + assert.strictEqual(tr.b.getOnlyElement( + hist.diagnostics.get('generic')), 'occam'); + assert.isFalse(hist.summaryOptions.get('count')); + assert.strictEqual(tr.b.getOnlyElement( + hist.summaryOptions.get('percentile')), 0.5); + }); + + test('getStatisticScalar', function() { + const hist = new tr.v.Histogram('', unitlessNumber); + // getStatisticScalar should work even when the statistics are disabled. + hist.customizeSummaryOptions({ + avg: false, + count: false, + max: false, + min: false, + std: false, + sum: false, + }); + + assert.isUndefined(hist.getStatisticScalar('avg')); + assert.isUndefined(hist.getStatisticScalar('std')); + assert.strictEqual(0, hist.getStatisticScalar('geometricMean').value); + assert.strictEqual(Infinity, hist.getStatisticScalar('min').value); + assert.strictEqual(-Infinity, hist.getStatisticScalar('max').value); + assert.strictEqual(0, hist.getStatisticScalar('sum').value); + assert.strictEqual(0, hist.getStatisticScalar('nans').value); + assert.strictEqual(0, hist.getStatisticScalar('count').value); + assert.isUndefined(hist.getStatisticScalar('pct_000')); + assert.isUndefined(hist.getStatisticScalar('pct_050')); + assert.isUndefined(hist.getStatisticScalar('pct_100')); + + assert.isFalse(hist.canCompare()); + assert.throws(() => hist.getStatisticScalar(tr.v.DELTA + 'avg')); + + const ref = new tr.v.Histogram('', unitlessNumber); + for (let i = 0; i < 10; ++i) { + hist.addSample(i * 10); + ref.addSample(i); + } + + assert.strictEqual(45, hist.getStatisticScalar('avg').value); + assert.closeTo(30.277, hist.getStatisticScalar('std').value, 1e-3); + assert.closeTo(0, hist.getStatisticScalar('geometricMean').value, 1e-4); + assert.strictEqual(0, hist.getStatisticScalar('min').value); + assert.strictEqual(90, hist.getStatisticScalar('max').value); + assert.strictEqual(450, hist.getStatisticScalar('sum').value); + assert.strictEqual(0, hist.getStatisticScalar('nans').value); + assert.strictEqual(10, hist.getStatisticScalar('count').value); + assert.closeTo(18.371, hist.getStatisticScalar('pct_025').value, 1e-3); + assert.closeTo(55.48, hist.getStatisticScalar('pct_075').value, 1e-3); + assert.closeTo(37.108, hist.getStatisticScalar('ipr_025_075').value, 1e-3); + + assert.strictEqual(40.5, hist.getStatisticScalar( + tr.v.DELTA + 'avg', ref).value); + assert.closeTo(27.249, hist.getStatisticScalar( + tr.v.DELTA + 'std', ref).value, 1e-3); + assert.closeTo(0, hist.getStatisticScalar( + tr.v.DELTA + 'geometricMean', ref).value, 1e-4); + assert.strictEqual(0, hist.getStatisticScalar( + tr.v.DELTA + 'min', ref).value); + assert.strictEqual(81, hist.getStatisticScalar( + tr.v.DELTA + 'max', ref).value); + assert.strictEqual(405, hist.getStatisticScalar( + tr.v.DELTA + 'sum', ref).value); + assert.strictEqual(0, hist.getStatisticScalar( + tr.v.DELTA + 'nans', ref).value); + assert.strictEqual(0, hist.getStatisticScalar( + tr.v.DELTA + 'count', ref).value); + assert.closeTo(16.357, hist.getStatisticScalar( + tr.v.DELTA + 'pct_025', ref).value, 1e-3); + assert.closeTo(49.396, hist.getStatisticScalar( + tr.v.DELTA + 'pct_075', ref).value, 1e-3); + assert.closeTo(33.04, hist.getStatisticScalar( + tr.v.DELTA + 'ipr_025_075', ref).value, 1e-3); + + assert.strictEqual(9, hist.getStatisticScalar( + `%${tr.v.DELTA}avg`, ref).value); + assert.closeTo(9, hist.getStatisticScalar( + `%${tr.v.DELTA}std`, ref).value, 1e-3); + assert.isTrue(isNaN(hist.getStatisticScalar( + `%${tr.v.DELTA}geometricMean`, ref).value)); + assert.isTrue(isNaN(hist.getStatisticScalar( + `%${tr.v.DELTA}min`, ref).value)); + assert.strictEqual(9, hist.getStatisticScalar( + `%${tr.v.DELTA}max`, ref).value); + assert.strictEqual(9, hist.getStatisticScalar( + `%${tr.v.DELTA}sum`, ref).value); + assert.isTrue(isNaN(hist.getStatisticScalar( + `%${tr.v.DELTA}nans`, ref).value)); + assert.strictEqual(0, hist.getStatisticScalar( + `%${tr.v.DELTA}count`, ref).value); + assert.closeTo(8.12, hist.getStatisticScalar( + `%${tr.v.DELTA}pct_025`, ref).value, 1e-3); + assert.closeTo(8.12, hist.getStatisticScalar( + `%${tr.v.DELTA}pct_075`, ref).value, 1e-3); + assert.closeTo(8.12, hist.getStatisticScalar( + `%${tr.v.DELTA}ipr_025_075`, ref).value, 1e-3); + }); + + test('rebin', function() { + const hist = new tr.v.Histogram('foo', unitlessNumber_smallerIsBetter, + tr.v.HistogramBinBoundaries.SINGULAR); + assert.strictEqual(400, hist.maxNumSampleValues); + for (let i = 0; i < 100; ++i) { + hist.addSample(i); + } + + let rebinned = hist.rebin(TEST_BOUNDARIES); + assert.strictEqual(12, rebinned.allBins.length); + assert.strictEqual(100, rebinned.allBins[1].count); + assert.strictEqual(hist.numValues, rebinned.numValues); + assert.strictEqual(hist.average, rebinned.average); + assert.strictEqual(hist.standardDeviation, rebinned.standardDeviation); + assert.strictEqual(hist.geometricMean, rebinned.geometricMean); + assert.strictEqual(hist.sum, rebinned.sum); + assert.strictEqual(hist.min, rebinned.min); + assert.strictEqual(hist.max, rebinned.max); + + for (let i = 100; i < 1000; ++i) { + hist.addSample(i); + } + + rebinned = hist.rebin(TEST_BOUNDARIES); + assert.strictEqual(12, rebinned.allBins.length); + let binCountSum = 0; + for (let i = 1; i < 11; ++i) { + binCountSum += rebinned.allBins[i].count; + assert.isAbove(100, rebinned.allBins[i].count, i); + } + assert.strictEqual(400, binCountSum); + assert.strictEqual(hist.numValues, rebinned.numValues); + assert.strictEqual(hist.average, rebinned.average); + assert.strictEqual(hist.standardDeviation, rebinned.standardDeviation); + assert.strictEqual(hist.geometricMean, rebinned.geometricMean); + assert.strictEqual(hist.sum, rebinned.sum); + assert.strictEqual(hist.min, rebinned.min); + assert.strictEqual(hist.max, rebinned.max); + }); + + test('serializationSize', function() { + // Ensure that serialized Histograms don't take up too much more space than + // necessary. + const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES); + + // You can change these numbers, but when you do, please explain in your CL + // description why they changed. + let dict = hist.asDict(); + assert.strictEqual(61, JSON.stringify(dict).length); + assert.isUndefined(dict.allBins); + assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict()); + + hist.addSample(100); + dict = hist.asDict(); + assert.strictEqual(142, JSON.stringify(dict).length); + assert.isUndefined(dict.allBins.length); + assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict()); + + hist.addSample(100); + dict = hist.asDict(); + // SAMPLE_VALUES grew by "100," + assert.strictEqual(146, JSON.stringify(dict).length); + assert.isUndefined(dict.allBins.length); + assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict()); + + hist.addSample(271, {foo: new tr.v.d.GenericSet(['bar'])}); + dict = hist.asDict(); + assert.strictEqual(212, JSON.stringify(dict).length); + assert.isUndefined(dict.allBins.length); + assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict()); + + // Add samples to most bins so that allBinsArray is more efficient than + // allBinsDict. + for (let i = 10; i < 100; ++i) { + hist.addSample(10 * i); + } + dict = hist.asDict(); + assert.strictEqual(628, JSON.stringify(hist.asDict()).length); + assert.lengthOf(dict.allBins, 12); + assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict()); + + // Lowering maxNumSampleValues takes a random sub-sample of the existing + // sampleValues. We have deliberately set all samples to 3-digit numbers so + // that the serialized size is constant regardless of which samples are + // retained. + hist.maxNumSampleValues = 10; + dict = hist.asDict(); + assert.strictEqual(320, JSON.stringify(dict).length); + assert.lengthOf(dict.allBins, 12); + assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict()); + }); + + test('significance', function() { + const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 10); + const histA = new tr.v.Histogram( + '', unitlessNumber_smallerIsBetter, boundaries); + const histB = new tr.v.Histogram( + '', unitlessNumber_smallerIsBetter, boundaries); + + const dontCare = new tr.v.Histogram('', unitlessNumber, boundaries); + assert.strictEqual(dontCare.getDifferenceSignificance(dontCare), + tr.b.math.Statistics.Significance.DONT_CARE); + + for (let i = 0; i < 100; ++i) { + histA.addSample(i); + histB.addSample(i * 0.85); + } + + assert.strictEqual(histA.getDifferenceSignificance(histB), + tr.b.math.Statistics.Significance.INSIGNIFICANT); + assert.strictEqual(histB.getDifferenceSignificance(histA), + tr.b.math.Statistics.Significance.INSIGNIFICANT); + assert.strictEqual(histA.getDifferenceSignificance(histB, 0.1), + tr.b.math.Statistics.Significance.SIGNIFICANT); + assert.strictEqual(histB.getDifferenceSignificance(histA, 0.1), + tr.b.math.Statistics.Significance.SIGNIFICANT); + }); + + test('basic', function() { + const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES); + assert.strictEqual(hist.getBinForValue(250).range.min, 200); + assert.strictEqual(hist.getBinForValue(250).range.max, 300); + + hist.addSample(-1, {foo: new tr.v.d.GenericSet(['a'])}); + hist.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + hist.addSample(0, {foo: new tr.v.d.GenericSet(['c'])}); + hist.addSample(500, {foo: new tr.v.d.GenericSet(['c'])}); + hist.addSample(999, {foo: new tr.v.d.GenericSet(['d'])}); + hist.addSample(1000, {foo: new tr.v.d.GenericSet(['d'])}); + assert.strictEqual(hist.allBins[0].count, 1); + + assert.strictEqual(hist.getBinForValue(0).count, 2); + assert.deepEqual( + hist.getBinForValue(0).diagnosticMaps.map(dm => + tr.b.getOnlyElement(dm.get('foo'))), ['b', 'c']); + + assert.strictEqual(hist.getBinForValue(500).count, 1); + assert.strictEqual(hist.getBinForValue(999).count, 1); + + assert.strictEqual(hist.allBins[hist.allBins.length - 1].count, 1); + assert.strictEqual(hist.numValues, 6); + assert.closeTo(hist.average, 416.3, 0.1); + }); + + test('nans', function() { + const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES); + + hist.addSample(undefined, {foo: new tr.v.d.GenericSet(['b'])}); + hist.addSample(NaN, {'foo': new tr.v.d.GenericSet(['c'])}); + hist.addSample(undefined); + hist.addSample(NaN); + + assert.strictEqual(hist.numNans, 4); + assert.deepEqual(hist.nanDiagnosticMaps.map(dm => + tr.b.getOnlyElement(dm.get('foo'))), ['b', 'c']); + + const hist2 = tr.v.Histogram.fromDict(hist.asDict()); + assert.instanceOf(hist2.nanDiagnosticMaps[0], tr.v.d.DiagnosticMap); + assert.instanceOf(hist2.nanDiagnosticMaps[0].get('foo'), tr.v.d.GenericSet); + }); + + test('addHistogramsValid', function() { + const hist0 = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES); + const hist1 = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES); + + hist0.addSample(-1, {foo: new tr.v.d.GenericSet(['a0'])}); + hist0.addSample(0, {foo: new tr.v.d.GenericSet(['b0'])}); + hist0.addSample(0, {foo: new tr.v.d.GenericSet(['c0'])}); + hist0.addSample(500, {foo: new tr.v.d.GenericSet(['c0'])}); + hist0.addSample(1000, {foo: new tr.v.d.GenericSet(['d0'])}); + hist0.addSample(NaN, {foo: new tr.v.d.GenericSet(['e0'])}); + + hist1.addSample(-1, {foo: new tr.v.d.GenericSet(['a1'])}); + hist1.addSample(0, {foo: new tr.v.d.GenericSet(['b1'])}); + hist1.addSample(0, {foo: new tr.v.d.GenericSet(['c1'])}); + hist1.addSample(999, {foo: new tr.v.d.GenericSet(['d1'])}); + hist1.addSample(1000, {foo: new tr.v.d.GenericSet(['d1'])}); + hist1.addSample(NaN, {foo: new tr.v.d.GenericSet(['e1'])}); + + hist0.addHistogram(hist1); + + assert.strictEqual(hist0.numNans, 2); + assert.deepEqual(hist0.nanDiagnosticMaps.map(dmd => + tr.b.getOnlyElement(dmd.get('foo'))), ['e0', 'e1']); + + assert.strictEqual(hist0.allBins[0].count, 2); + assert.deepEqual( + hist0.allBins[0].diagnosticMaps.map(dmd => + tr.b.getOnlyElement(dmd.get('foo'))), ['a0', 'a1']); + + assert.strictEqual(hist0.getBinForValue(0).count, 4); + assert.deepEqual( + hist0.getBinForValue(0).diagnosticMaps.map(dmd => + tr.b.getOnlyElement(dmd.get('foo'))), ['b0', 'c0', 'b1', 'c1']); + + assert.strictEqual(hist0.getBinForValue(500).count, 1); + assert.deepEqual( + hist0.getBinForValue(500).diagnosticMaps.map(dmd => + tr.b.getOnlyElement(dmd.get('foo'))), ['c0']); + + assert.strictEqual(hist0.getBinForValue(999).count, 1); + assert.deepEqual( + hist0.getBinForValue(999).diagnosticMaps.map(dmd => + tr.b.getOnlyElement(dmd.get('foo'))), ['d1']); + + assert.strictEqual(hist0.allBins[hist0.allBins.length - 1].count, 2); + assert.deepEqual(hist0.allBins[hist0.allBins.length - 1].diagnosticMaps.map( + dmd => tr.b.getOnlyElement(dmd.get('foo'))), ['d0', 'd1']); + + assert.strictEqual(hist0.numValues, 10); + assert.closeTo(hist0.average, 349.7, 0.1); + + const hist02 = tr.v.Histogram.fromDict(hist0.asDict()); + assert.instanceOf(hist02.allBins[0].diagnosticMaps[0], + tr.v.d.DiagnosticMap); + assert.instanceOf(hist02.allBins[0].diagnosticMaps[0].get('foo'), + tr.v.d.GenericSet); + }); + + test('addHistogramsInvalid', function() { + const hist0 = new tr.v.Histogram('', tr.b.Unit.byName.timeDurationInMs, + tr.v.HistogramBinBoundaries.createLinear(0, 1000, 10)); + const hist1 = new tr.v.Histogram('', tr.b.Unit.byName.timeDurationInMs, + tr.v.HistogramBinBoundaries.createLinear(0, 1001, 10)); + const hist2 = new tr.v.Histogram('', tr.b.Unit.byName.timeDurationInMs, + tr.v.HistogramBinBoundaries.createLinear(0, 1000, 11)); + + assert.isFalse(hist0.canAddHistogram(hist1)); + assert.isFalse(hist0.canAddHistogram(hist2)); + assert.isFalse(hist1.canAddHistogram(hist0)); + assert.isFalse(hist1.canAddHistogram(hist2)); + assert.isFalse(hist2.canAddHistogram(hist0)); + assert.isFalse(hist2.canAddHistogram(hist1)); + + assert.throws(hist0.addHistogram.bind(hist0, hist1), Error); + assert.throws(hist0.addHistogram.bind(hist0, hist2), Error); + }); + + test('addHistogramWithNonDiagnosticMapThrows', function() { + const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES); + assert.throws(hist.addSample.bind(42, 'foo'), Error); + }); + + test('getApproximatePercentile', function() { + function check(array, min, max, bins, precision) { + const boundaries = tr.v.HistogramBinBoundaries.createLinear( + min, max, bins); + const hist = new tr.v.Histogram( + '', tr.b.Unit.byName.timeDurationInMs, boundaries); + array.forEach(x => hist.addSample( + x, {foo: new tr.v.d.GenericSet(['x'])})); + [0.25, 0.5, 0.75, 0.8, 0.95, 0.99].forEach(function(percent) { + const expected = tr.b.math.Statistics.percentile(array, percent); + const actual = hist.getApproximatePercentile(percent); + assert.closeTo(expected, actual, precision); + }); + } + check([1, 2, 5, 7], 0.5, 10.5, 10, 1e-3); + check([3, 3, 4, 4], 0.5, 10.5, 10, 1e-3); + check([1, 10], 0.5, 10.5, 10, 1e-3); + check([1, 2, 3, 4, 5], 0.5, 10.5, 10, 1e-3); + check([3, 3, 3, 3, 3], 0.5, 10.5, 10, 1e-3); + check([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3); + check([1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3); + check([0, 11], 0.5, 10.5, 10, 1); + check([0, 6, 11], 0.5, 10.5, 10, 1); + const array = []; + for (let i = 0; i < 1000; i++) { + array.push((i * i) % 10 + 1); + } + check(array, 0.5, 10.5, 10, 1e-3); + // If the real percentile is outside the bin range then the approximation + // error can be high. + check([-10000], 0, 10, 10, 10000); + check([10000], 0, 10, 10, 10000 - 10); + // The result is no more than the bin width away from the real percentile. + check([1, 1], 0, 10, 1, 10); + }); + + test('histogramBinBoundaries_addBinBoundary', function() { + const b = new tr.v.HistogramBinBoundaries(-100); + b.addBinBoundary(50); + + checkBoundaries(b, -100, 50, tr.b.Unit.byName.timeDurationInMs, [ + tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, -100), + tr.b.math.Range.fromExplicitRange(-100, 50), + tr.b.math.Range.fromExplicitRange(50, Number.MAX_VALUE) + ]); + + b.addBinBoundary(60); + b.addBinBoundary(75); + + checkBoundaries(b, -100, 75, tr.b.Unit.byName.timeDurationInMs, [ + tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, -100), + tr.b.math.Range.fromExplicitRange(-100, 50), + tr.b.math.Range.fromExplicitRange(50, 60), + tr.b.math.Range.fromExplicitRange(60, 75), + tr.b.math.Range.fromExplicitRange(75, Number.MAX_VALUE) + ]); + }); + + test('histogramBinBoundaries_addLinearBins', function() { + const b = new tr.v.HistogramBinBoundaries(1000); + b.addLinearBins(1200, 5); + + checkBoundaries(b, 1000, 1200, tr.b.Unit.byName.powerInWatts, [ + tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, 1000), + tr.b.math.Range.fromExplicitRange(1000, 1040), + tr.b.math.Range.fromExplicitRange(1040, 1080), + tr.b.math.Range.fromExplicitRange(1080, 1120), + tr.b.math.Range.fromExplicitRange(1120, 1160), + tr.b.math.Range.fromExplicitRange(1160, 1200), + tr.b.math.Range.fromExplicitRange(1200, Number.MAX_VALUE) + ]); + }); + + test('histogramBinBoundaries_addExponentialBins', function() { + const b = new tr.v.HistogramBinBoundaries(0.5); + b.addExponentialBins(8, 4); + + checkBoundaries(b, 0.5, 8, tr.b.Unit.byName.energyInJoules, [ + tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, 0.5), + tr.b.math.Range.fromExplicitRange(0.5, 1), + tr.b.math.Range.fromExplicitRange(1, 2), + tr.b.math.Range.fromExplicitRange(2, 4), + tr.b.math.Range.fromExplicitRange(4, 8), + tr.b.math.Range.fromExplicitRange(8, Number.MAX_VALUE) + ]); + }); + + test('histogramBinBoundaries_combined', function() { + const b = new tr.v.HistogramBinBoundaries(-273.15); + b.addBinBoundary(-50); + b.addLinearBins(4, 3); + b.addExponentialBins(16, 2); + b.addLinearBins(17, 4); + b.addBinBoundary(100); + + checkBoundaries(b, -273.15, 100, tr.b.Unit.byName.unitlessNumber, [ + tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, -273.15), + tr.b.math.Range.fromExplicitRange(-273.15, -50), + tr.b.math.Range.fromExplicitRange(-50, -32), + tr.b.math.Range.fromExplicitRange(-32, -14), + tr.b.math.Range.fromExplicitRange(-14, 4), + tr.b.math.Range.fromExplicitRange(4, 8), + tr.b.math.Range.fromExplicitRange(8, 16), + tr.b.math.Range.fromExplicitRange(16, 16.25), + tr.b.math.Range.fromExplicitRange(16.25, 16.5), + tr.b.math.Range.fromExplicitRange(16.5, 16.75), + tr.b.math.Range.fromExplicitRange(16.75, 17), + tr.b.math.Range.fromExplicitRange(17, 100), + tr.b.math.Range.fromExplicitRange(100, Number.MAX_VALUE) + ]); + }); + + test('histogramBinBoundaries_throws', function() { + const b0 = new tr.v.HistogramBinBoundaries(-7); + assert.throws(function() { b0.addBinBoundary(-10 /* must be > -7 */); }); + assert.throws(function() { b0.addBinBoundary(-7 /* must be > -7 */); }); + assert.throws(function() { b0.addLinearBins(-10 /* must be > -7 */, 10); }); + assert.throws(function() { b0.addLinearBins(-7 /* must be > -7 */, 100); }); + assert.throws(function() { b0.addLinearBins(10, 0 /* must be > 0 */); }); + assert.throws(function() { + // Current max bin boundary (-7) must be positive. + b0.addExponentialBins(16, 4); + }); + + const b1 = new tr.v.HistogramBinBoundaries(8); + assert.throws(() => b1.addExponentialBins(20, 0 /* must be > 0 */)); + assert.throws(() => b1.addExponentialBins(5 /* must be > 8 */, 3)); + assert.throws(() => b1.addExponentialBins(8 /* must be > 8 */, 3)); + }); + + test('statisticsScalars', function() { + const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 100); + let hist = new tr.v.Histogram('', unitlessNumber, boundaries); + + hist.addSample(50); + hist.addSample(60); + hist.addSample(70); + hist.addSample('i am not a number'); + + hist.customizeSummaryOptions({ + count: true, + min: true, + max: true, + sum: true, + avg: true, + std: true, + nans: true, + geometricMean: true, + percentile: [0.5, 1] + }); + + // Test round-tripping summaryOptions. + hist = tr.v.Histogram.fromDict(hist.asDict()); + + const stats = hist.statisticsScalars; + assert.strictEqual(stats.get('nans').unit, + tr.b.Unit.byName.count_smallerIsBetter); + assert.strictEqual(stats.get('nans').value, 1); + assert.strictEqual(stats.get('count').unit, + tr.b.Unit.byName.count_smallerIsBetter); + assert.strictEqual(stats.get('count').value, 3); + assert.strictEqual(stats.get('min').unit, hist.unit); + assert.strictEqual(stats.get('min').value, 50); + assert.strictEqual(stats.get('max').unit, hist.unit); + assert.strictEqual(stats.get('max').value, 70); + assert.strictEqual(stats.get('sum').unit, hist.unit); + assert.strictEqual(stats.get('sum').value, 180); + assert.strictEqual(stats.get('avg').unit, hist.unit); + assert.strictEqual(stats.get('avg').value, 60); + assert.strictEqual(stats.get('std').value, 10); + assert.strictEqual(stats.get('pct_050').unit, hist.unit); + assert.closeTo(stats.get('pct_050').value, 60, 1); + assert.strictEqual(stats.get('pct_100').unit, hist.unit); + assert.closeTo(stats.get('pct_100').value, 70, 1); + assert.strictEqual(stats.get('geometricMean').unit, hist.unit); + assert.closeTo(stats.get('geometricMean').value, 59.439, 1e-3); + }); + + test('statisticsScalarsNoSummaryOptions', function() { + const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 100); + const hist = new tr.v.Histogram('', unitlessNumber, boundaries); + + hist.addSample(50); + hist.addSample(60); + hist.addSample(70); + + hist.customizeSummaryOptions({ + count: false, + min: false, + max: false, + sum: false, + avg: false, + std: false, + percentile: [] + }); + + assert.strictEqual(hist.statisticsScalars.size, 0); + }); + + test('statisticsScalarsEmptyHistogram', function() { + const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 100); + const hist = new tr.v.Histogram('', unitlessNumber, boundaries); + hist.customizeSummaryOptions({ + count: true, + min: true, + max: true, + sum: true, + avg: true, + std: true, + percentile: [0, 0.01, 0.1, 0.5, 0.995, 1] + }); + + const stats = hist.statisticsScalars; + assert.strictEqual(stats.get('count').value, 0); + assert.strictEqual(stats.get('min').value, Infinity); + assert.strictEqual(stats.get('max').value, -Infinity); + assert.strictEqual(stats.get('sum').value, 0); + assert.strictEqual(stats.get('avg'), undefined); + assert.strictEqual(stats.get('std'), undefined); + assert.isUndefined(stats.get('pct_000')); + assert.isUndefined(stats.get('pct_001')); + assert.isUndefined(stats.get('pct_010')); + assert.isUndefined(stats.get('pct_050')); + assert.isUndefined(stats.get('pct_099_5')); + assert.isUndefined(stats.get('pct_100')); + }); + + test('sampleValues', function() { + const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 1000, 50); + const hist0 = new tr.v.Histogram('', unitlessNumber, boundaries); + const hist1 = new tr.v.Histogram('', unitlessNumber, boundaries); + // maxNumSampleValues defaults to numBins * 10, which, including the + // underflow bin and overflow bin plus this builder's 10 central bins, + // is 52 * 10. + assert.strictEqual(hist0.maxNumSampleValues, 520); + assert.strictEqual(hist1.maxNumSampleValues, 520); + const values0 = []; + const values1 = []; + for (let i = 0; i < 10; ++i) { + values0.push(i); + hist0.addSample(i); + } + for (let i = 10; i < 20; ++i) { + values1.push(i); + hist1.addSample(i); + } + assert.deepEqual(hist0.sampleValues, values0); + assert.deepEqual(hist1.sampleValues, values1); + hist0.addHistogram(hist1); + assert.deepEqual(hist0.sampleValues, values0.concat(values1)); + const hist2 = tr.v.Histogram.fromDict(hist0.asDict()); + assert.deepEqual(hist2.sampleValues, values0.concat(values1)); + + for (let i = 0; i < 500; ++i) { + hist0.addSample(i); + } + assert.strictEqual(hist0.sampleValues.length, hist0.maxNumSampleValues); + + const hist3 = new tr.v.Histogram('', unitlessNumber, boundaries); + hist3.maxNumSampleValues = 10; + for (let i = 0; i < 100; ++i) { + hist3.addSample(i); + } + assert.strictEqual(hist3.sampleValues.length, 10); + }); + + test('singularBin', function() { + const hist = new tr.v.Histogram('', unitlessNumber, + tr.v.HistogramBinBoundaries.SINGULAR); + assert.lengthOf(hist.allBins, 1); + + const dict = hist.asDict(); + assert.isUndefined(dict.binBoundaries); + const clone = tr.v.Histogram.fromDict(dict); + assert.lengthOf(clone.allBins, 1); + assert.deepEqual(dict, clone.asDict()); + + assert.isUndefined(hist.getApproximatePercentile(0)); + assert.isUndefined(hist.getApproximatePercentile(1)); + hist.addSample(0); + assert.strictEqual(0, hist.getApproximatePercentile(0)); + assert.strictEqual(0, hist.getApproximatePercentile(1)); + hist.addSample(1); + assert.strictEqual(0, hist.getApproximatePercentile(0)); + assert.strictEqual(1, hist.getApproximatePercentile(1)); + hist.addSample(2); + assert.strictEqual(0, hist.getApproximatePercentile(0)); + assert.strictEqual(1, hist.getApproximatePercentile(0.5)); + assert.strictEqual(2, hist.getApproximatePercentile(1)); + hist.addSample(3); + assert.strictEqual(0, hist.getApproximatePercentile(0)); + assert.strictEqual(1, hist.getApproximatePercentile(0.5)); + assert.strictEqual(2, hist.getApproximatePercentile(0.9)); + assert.strictEqual(3, hist.getApproximatePercentile(1)); + hist.addSample(4); + assert.strictEqual(0, hist.getApproximatePercentile(0)); + assert.strictEqual(1, hist.getApproximatePercentile(0.4)); + assert.strictEqual(2, hist.getApproximatePercentile(0.7)); + assert.strictEqual(3, hist.getApproximatePercentile(0.9)); + assert.strictEqual(4, hist.getApproximatePercentile(1)); + }); + + test('singularBin_with_multiBin', function() { + const multiBin = new tr.v.Histogram('', unitlessNumber); + const singleBin = new tr.v.Histogram('', unitlessNumber, + tr.v.HistogramBinBoundaries.SINGULAR); + multiBin.addSample(1); + singleBin.addSample(3); + assert.strictEqual(1, multiBin.average); + assert.strictEqual(3, singleBin.average); + multiBin.addHistogram(singleBin); + assert.strictEqual(2, multiBin.average); + multiBin.addSample(1); + singleBin.addHistogram(multiBin); + assert.strictEqual(2, singleBin.average); + }); + + test('mergeSummaryOptions', function() { + const hist0 = new tr.v.Histogram('', unitlessNumber); + const hist1 = new tr.v.Histogram('', unitlessNumber); + + hist0.customizeSummaryOptions({ + sum: false, + percentile: [0.1, 0.9], + iprs: [ + tr.b.math.Range.fromExplicitRange(0.1, 0.9), + tr.b.math.Range.fromExplicitRange(0.25, 0.75), + ], + }); + hist1.customizeSummaryOptions({ + min: false, + percentile: [0.1, 0.95], + iprs: [ + tr.b.math.Range.fromExplicitRange(0.1, 0.9), + tr.b.math.Range.fromExplicitRange(0.2, 0.8), + ], + }); + + let merged = tr.v.Histogram.fromDict(hist0.asDict()); + let mergedIprs = merged.summaryOptions.get('iprs'); + assert.isTrue(merged.summaryOptions.get('min')); + assert.isFalse(merged.summaryOptions.get('sum')); + assert.deepEqual(merged.summaryOptions.get('percentile'), [0.1, 0.9]); + assert.lengthOf(merged.summaryOptions.get('iprs'), 2); + tr.b.assertRangeEquals( + mergedIprs[0], tr.b.math.Range.fromExplicitRange(0.1, 0.9)); + tr.b.assertRangeEquals( + mergedIprs[1], tr.b.math.Range.fromExplicitRange(0.25, 0.75)); + + merged = tr.v.Histogram.fromDict(hist1.asDict()); + mergedIprs = merged.summaryOptions.get('iprs'); + assert.isFalse(merged.summaryOptions.get('min')); + assert.isTrue(merged.summaryOptions.get('sum')); + assert.deepEqual(merged.summaryOptions.get('percentile'), [0.1, 0.95]); + assert.lengthOf(merged.summaryOptions.get('iprs'), 2); + tr.b.assertRangeEquals( + mergedIprs[0], tr.b.math.Range.fromExplicitRange(0.1, 0.9)); + tr.b.assertRangeEquals( + mergedIprs[1], tr.b.math.Range.fromExplicitRange(0.2, 0.8)); + + merged = hist0.clone(); + merged.addHistogram(hist1); + + assert.isTrue(merged.summaryOptions.get('min')); + assert.isTrue(merged.summaryOptions.get('sum')); + assert.deepEqual(merged.summaryOptions.get('percentile'), [0.1, 0.9, 0.95]); + mergedIprs = merged.summaryOptions.get('iprs'); + assert.lengthOf(mergedIprs, 3); + tr.b.assertRangeEquals( + mergedIprs[0], tr.b.math.Range.fromExplicitRange(0.1, 0.9)); + tr.b.assertRangeEquals( + mergedIprs[1], tr.b.math.Range.fromExplicitRange(0.25, 0.75)); + tr.b.assertRangeEquals( + mergedIprs[2], tr.b.math.Range.fromExplicitRange(0.2, 0.8)); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py new file mode 100644 index 00000000000..ab9b473e21c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py @@ -0,0 +1,674 @@ +# Copyright 2017 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. + +import json +import math +import unittest + +from tracing.value import histogram +from tracing.value.diagnostics import date_range +from tracing.value.diagnostics import diagnostic +from tracing.value.diagnostics import diagnostic_ref +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import related_event_set +from tracing.value.diagnostics import related_name_map +from tracing.value.diagnostics import reserved_infos +from tracing.value.diagnostics import unmergeable_diagnostic_set as ums + +# pylint: disable=too-many-lines + +class PercentToStringUnittest(unittest.TestCase): + def testPercentToString(self): + with self.assertRaises(Exception) as ex: + histogram.PercentToString(-1) + self.assertEqual(str(ex.exception), 'percent must be in [0,1]') + + with self.assertRaises(Exception) as ex: + histogram.PercentToString(2) + self.assertEqual(str(ex.exception), 'percent must be in [0,1]') + + self.assertEqual(histogram.PercentToString(0), '000') + self.assertEqual(histogram.PercentToString(1), '100') + + with self.assertRaises(Exception) as ex: + histogram.PercentToString(float('nan')) + self.assertEqual(str(ex.exception), 'Unexpected percent') + + self.assertEqual(histogram.PercentToString(0.50), '050') + self.assertEqual(histogram.PercentToString(0.95), '095') + + +class StatisticsUnittest(unittest.TestCase): + def testFindHighIndexInSortedArray(self): + self.assertEqual(histogram.FindHighIndexInSortedArray( + list(range(0, -10, -1)), lambda x: x + 5), 6) + + def testUniformlySampleArray(self): + self.assertEqual(len(histogram.UniformlySampleArray( + list(range(10)), 5)), 5) + + def testUniformlySampleStream(self): + samples = [] + histogram.UniformlySampleStream(samples, 1, 'A', 5) + self.assertEqual(samples, ['A']) + histogram.UniformlySampleStream(samples, 2, 'B', 5) + histogram.UniformlySampleStream(samples, 3, 'C', 5) + histogram.UniformlySampleStream(samples, 4, 'D', 5) + histogram.UniformlySampleStream(samples, 5, 'E', 5) + self.assertEqual(samples, ['A', 'B', 'C', 'D', 'E']) + histogram.UniformlySampleStream(samples, 6, 'F', 5) + self.assertEqual(len(samples), 5) + + samples = [0, 0, 0] + histogram.UniformlySampleStream(samples, 1, 'G', 5) + self.assertEqual(samples, ['G', 0, 0]) + + def testMergeSampledStreams(self): + samples = [] + histogram.MergeSampledStreams(samples, 0, ['A'], 1, 5) + self.assertEqual(samples, ['A']) + histogram.MergeSampledStreams(samples, 1, ['B', 'C', 'D', 'E'], 4, 5) + self.assertEqual(samples, ['A', 'B', 'C', 'D', 'E']) + histogram.MergeSampledStreams(samples, 9, ['F', 'G', 'H', 'I', 'J'], 7, 5) + self.assertEqual(len(samples), 5) + + +class RangeUnittest(unittest.TestCase): + def testAddValue(self): + r = histogram.Range() + self.assertEqual(r.empty, True) + r.AddValue(1) + self.assertEqual(r.empty, False) + self.assertEqual(r.min, 1) + self.assertEqual(r.max, 1) + self.assertEqual(r.center, 1) + r.AddValue(2) + self.assertEqual(r.empty, False) + self.assertEqual(r.min, 1) + self.assertEqual(r.max, 2) + self.assertEqual(r.center, 1.5) + + +class RunningStatisticsUnittest(unittest.TestCase): + def _Run(self, data): + running = histogram.RunningStatistics() + for datum in data: + running.Add(datum) + return running + + def testStatistics(self): + running = self._Run([1, 2, 3]) + self.assertEqual(running.sum, 6) + self.assertEqual(running.mean, 2) + self.assertEqual(running.min, 1) + self.assertEqual(running.max, 3) + self.assertEqual(running.variance, 1) + self.assertEqual(running.stddev, 1) + self.assertEqual(running.geometric_mean, math.pow(6, 1./3)) + self.assertEqual(running.count, 3) + + running = self._Run([2, 4, 4, 2]) + self.assertEqual(running.sum, 12) + self.assertEqual(running.mean, 3) + self.assertEqual(running.min, 2) + self.assertEqual(running.max, 4) + self.assertEqual(running.variance, 4./3) + self.assertEqual(running.stddev, math.sqrt(4./3)) + self.assertAlmostEqual(running.geometric_mean, math.pow(64, 1./4)) + self.assertEqual(running.count, 4) + + def testMerge(self): + def Compare(data1, data2): + a_running = self._Run(data1 + data2) + b_running = self._Run(data1).Merge(self._Run(data2)) + CompareRunningStatistics(a_running, b_running) + a_running = histogram.RunningStatistics.FromDict(a_running.AsDict()) + CompareRunningStatistics(a_running, b_running) + b_running = histogram.RunningStatistics.FromDict(b_running.AsDict()) + CompareRunningStatistics(a_running, b_running) + + def CompareRunningStatistics(a_running, b_running): + self.assertEqual(a_running.sum, b_running.sum) + self.assertEqual(a_running.mean, b_running.mean) + self.assertEqual(a_running.min, b_running.min) + self.assertEqual(a_running.max, b_running.max) + self.assertAlmostEqual(a_running.variance, b_running.variance) + self.assertAlmostEqual(a_running.stddev, b_running.stddev) + self.assertAlmostEqual(a_running.geometric_mean, b_running.geometric_mean) + self.assertEqual(a_running.count, b_running.count) + + Compare([], []) + Compare([], [1, 2, 3]) + Compare([1, 2, 3], []) + Compare([1, 2, 3], [10, 20, 100]) + Compare([1, 1, 1, 1, 1], [10, 20, 10, 40]) + + +def ToJSON(x): + return json.dumps(x, separators=(',', ':'), sort_keys=True) + + +class HistogramUnittest(unittest.TestCase): + TEST_BOUNDARIES = histogram.HistogramBinBoundaries.CreateLinear(0, 1000, 10) + + def assertDeepEqual(self, a, b): + self.assertEqual(ToJSON(a), ToJSON(b)) + + def testDefaultBoundaries(self): + hist = histogram.Histogram('', 'ms') + self.assertEqual(len(hist.bins), 102) + + hist = histogram.Histogram('', 'tsMs') + self.assertEqual(len(hist.bins), 1002) + + hist = histogram.Histogram('', 'n%') + self.assertEqual(len(hist.bins), 22) + + hist = histogram.Histogram('', 'sizeInBytes') + self.assertEqual(len(hist.bins), 102) + + hist = histogram.Histogram('', 'J') + self.assertEqual(len(hist.bins), 52) + + hist = histogram.Histogram('', 'W') + self.assertEqual(len(hist.bins), 52) + + hist = histogram.Histogram('', 'unitless') + self.assertEqual(len(hist.bins), 52) + + hist = histogram.Histogram('', 'count') + self.assertEqual(len(hist.bins), 22) + + hist = histogram.Histogram('', 'sigma') + self.assertEqual(len(hist.bins), 52) + + hist = histogram.Histogram('', 'sigma_smallerIsBetter') + self.assertEqual(len(hist.bins), 52) + + hist = histogram.Histogram('', 'sigma_biggerIsBetter') + self.assertEqual(len(hist.bins), 52) + + def testSerializationSize(self): + hist = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + d = hist.AsDict() + self.assertEqual(61, len(ToJSON(d))) + self.assertIsNone(d.get('allBins')) + self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict()) + + hist.AddSample(100) + d = hist.AsDict() + self.assertEqual(152, len(ToJSON(d))) + self.assertIsInstance(d['allBins'], dict) + self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict()) + + hist.AddSample(100) + d = hist.AsDict() + # SAMPLE_VALUES grew by "100," + self.assertEqual(156, len(ToJSON(d))) + self.assertIsInstance(d['allBins'], dict) + self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict()) + + hist.AddSample(271, {'foo': generic_set.GenericSet(['bar'])}) + d = hist.AsDict() + self.assertEqual(222, len(ToJSON(d))) + self.assertIsInstance(d['allBins'], dict) + self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict()) + + # Add samples to most bins so that allBinsArray is more efficient than + # allBinsDict. + for i in range(10, 100): + hist.AddSample(10 * i) + d = hist.AsDict() + self.assertEqual(651, len(ToJSON(d))) + self.assertIsInstance(d['allBins'], list) + self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict()) + + # Lowering maxNumSampleValues takes a random sub-sample of the existing + # sampleValues. We have deliberately set all samples to 3-digit numbers so + # that the serialized size is constant regardless of which samples are + # retained. + hist.max_num_sample_values = 10 + d = hist.AsDict() + self.assertEqual(343, len(ToJSON(d))) + self.assertIsInstance(d['allBins'], list) + self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict()) + + def testBasic(self): + hist = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + self.assertEqual(hist.GetBinForValue(250).range.min, 200) + self.assertEqual(hist.GetBinForValue(250).range.max, 300) + + hist.AddSample(-1) + hist.AddSample(0) + hist.AddSample(0) + hist.AddSample(500) + hist.AddSample(999) + hist.AddSample(1000) + self.assertEqual(hist.bins[0].count, 1) + + self.assertEqual(hist.GetBinForValue(0).count, 2) + self.assertEqual(hist.GetBinForValue(500).count, 1) + self.assertEqual(hist.GetBinForValue(999).count, 1) + self.assertEqual(hist.bins[-1].count, 1) + self.assertEqual(hist.num_values, 6) + self.assertAlmostEqual(hist.average, 416.3333333) + + def testNans(self): + hist = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + hist.AddSample(None) + hist.AddSample(float('nan')) + self.assertEqual(hist.num_nans, 2) + + def testAddHistogramValid(self): + hist0 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + hist1 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + hist0.AddSample(0) + hist0.AddSample(None) + hist1.AddSample(1) + hist1.AddSample(float('nan')) + hist0.AddHistogram(hist1) + self.assertEqual(hist0.num_nans, 2) + self.assertEqual(hist0.GetBinForValue(0).count, 2) + + def testAddHistogramInvalid(self): + hist0 = histogram.Histogram( + '', 'ms', histogram.HistogramBinBoundaries.CreateLinear(0, 1000, 10)) + hist1 = histogram.Histogram( + '', 'unitless', histogram.HistogramBinBoundaries.CreateLinear( + 0, 1000, 10)) + hist2 = histogram.Histogram( + '', 'ms', histogram.HistogramBinBoundaries.CreateLinear(0, 1001, 10)) + hist3 = histogram.Histogram( + '', 'ms', histogram.HistogramBinBoundaries.CreateLinear(0, 1000, 11)) + hists = [hist0, hist1, hist2, hist3] + for hista in hists: + for histb in hists: + if hista is histb: + continue + self.assertFalse(hista.CanAddHistogram(histb)) + with self.assertRaises(Exception): + hista.AddHistogram(histb) + + def testPercentile(self): + def Check(ary, mn, mx, bins, precision): + boundaries = histogram.HistogramBinBoundaries.CreateLinear(mn, mx, bins) + hist = histogram.Histogram('', 'ms', boundaries) + for x in ary: + hist.AddSample(x) + for percent in [0.25, 0.5, 0.75, 0.8, 0.95, 0.99]: + self.assertLessEqual( + abs(histogram.Percentile(ary, percent) - + hist.GetApproximatePercentile(percent)), precision) + Check([1, 2, 5, 7], 0.5, 10.5, 10, 1e-3) + Check([3, 3, 4, 4], 0.5, 10.5, 10, 1e-3) + Check([1, 10], 0.5, 10.5, 10, 1e-3) + Check([1, 2, 3, 4, 5], 0.5, 10.5, 10, 1e-3) + Check([3, 3, 3, 3, 3], 0.5, 10.5, 10, 1e-3) + Check([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3) + Check([1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3) + Check([0, 11], 0.5, 10.5, 10, 1) + Check([0, 6, 11], 0.5, 10.5, 10, 1) + array = [] + for i in range(1000): + array.append((i * i) % 10 + 1) + Check(array, 0.5, 10.5, 10, 1e-3) + # If the real percentile is outside the bin range then the approximation + # error can be high. + Check([-10000], 0, 10, 10, 10000) + Check([10000], 0, 10, 10, 10000 - 10) + # The result is no more than the bin width away from the real percentile. + Check([1, 1], 0, 10, 1, 10) + + def _CheckBoundaries(self, boundaries, expected_min_boundary, + expected_max_boundary, expected_bin_ranges): + self.assertEqual(boundaries.range.min, expected_min_boundary) + self.assertEqual(boundaries.range.max, expected_max_boundary) + + # Check that the boundaries can be used multiple times. + for _ in range(3): + hist = histogram.Histogram('', 'unitless', boundaries) + self.assertEqual(len(expected_bin_ranges), len(hist.bins)) + for j, hbin in enumerate(hist.bins): + self.assertAlmostEqual(hbin.range.min, expected_bin_ranges[j].min) + self.assertAlmostEqual(hbin.range.max, expected_bin_ranges[j].max) + + def testAddBinBoundary(self): + b = histogram.HistogramBinBoundaries(-100) + b.AddBinBoundary(50) + self._CheckBoundaries(b, -100, 50, [ + histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, -100), + histogram.Range.FromExplicitRange(-100, 50), + histogram.Range.FromExplicitRange(50, histogram.JS_MAX_VALUE), + ]) + + b.AddBinBoundary(60) + b.AddBinBoundary(75) + self._CheckBoundaries(b, -100, 75, [ + histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, -100), + histogram.Range.FromExplicitRange(-100, 50), + histogram.Range.FromExplicitRange(50, 60), + histogram.Range.FromExplicitRange(60, 75), + histogram.Range.FromExplicitRange(75, histogram.JS_MAX_VALUE), + ]) + + def testAddLinearBins(self): + b = histogram.HistogramBinBoundaries(1000) + b.AddLinearBins(1200, 5) + self._CheckBoundaries(b, 1000, 1200, [ + histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, 1000), + histogram.Range.FromExplicitRange(1000, 1040), + histogram.Range.FromExplicitRange(1040, 1080), + histogram.Range.FromExplicitRange(1080, 1120), + histogram.Range.FromExplicitRange(1120, 1160), + histogram.Range.FromExplicitRange(1160, 1200), + histogram.Range.FromExplicitRange(1200, histogram.JS_MAX_VALUE), + ]) + + def testAddExponentialBins(self): + b = histogram.HistogramBinBoundaries(0.5) + b.AddExponentialBins(8, 4) + self._CheckBoundaries(b, 0.5, 8, [ + histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, 0.5), + histogram.Range.FromExplicitRange(0.5, 1), + histogram.Range.FromExplicitRange(1, 2), + histogram.Range.FromExplicitRange(2, 4), + histogram.Range.FromExplicitRange(4, 8), + histogram.Range.FromExplicitRange(8, histogram.JS_MAX_VALUE), + ]) + + def testBinBoundariesCombined(self): + b = histogram.HistogramBinBoundaries(-273.15) + b.AddBinBoundary(-50) + b.AddLinearBins(4, 3) + b.AddExponentialBins(16, 2) + b.AddLinearBins(17, 4) + b.AddBinBoundary(100) + + self._CheckBoundaries(b, -273.15, 100, [ + histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, -273.15), + histogram.Range.FromExplicitRange(-273.15, -50), + histogram.Range.FromExplicitRange(-50, -32), + histogram.Range.FromExplicitRange(-32, -14), + histogram.Range.FromExplicitRange(-14, 4), + histogram.Range.FromExplicitRange(4, 8), + histogram.Range.FromExplicitRange(8, 16), + histogram.Range.FromExplicitRange(16, 16.25), + histogram.Range.FromExplicitRange(16.25, 16.5), + histogram.Range.FromExplicitRange(16.5, 16.75), + histogram.Range.FromExplicitRange(16.75, 17), + histogram.Range.FromExplicitRange(17, 100), + histogram.Range.FromExplicitRange(100, histogram.JS_MAX_VALUE) + ]) + + def testBinBoundariesRaises(self): + b = histogram.HistogramBinBoundaries(-7) + with self.assertRaises(Exception): + b.AddBinBoundary(-10) + with self.assertRaises(Exception): + b.AddBinBoundary(-7) + with self.assertRaises(Exception): + b.AddLinearBins(-10, 10) + with self.assertRaises(Exception): + b.AddLinearBins(-7, 10) + with self.assertRaises(Exception): + b.AddLinearBins(10, 0) + with self.assertRaises(Exception): + b.AddExponentialBins(16, 4) + b = histogram.HistogramBinBoundaries(8) + with self.assertRaises(Exception): + b.AddExponentialBins(20, 0) + with self.assertRaises(Exception): + b.AddExponentialBins(5, 3) + with self.assertRaises(Exception): + b.AddExponentialBins(8, 3) + + def testStatisticsScalars(self): + b = histogram.HistogramBinBoundaries.CreateLinear(0, 100, 100) + hist = histogram.Histogram('', 'unitless', b) + hist.AddSample(50) + hist.AddSample(60) + hist.AddSample(70) + hist.AddSample('i am not a number') + hist.CustomizeSummaryOptions({ + 'count': True, + 'min': True, + 'max': True, + 'sum': True, + 'avg': True, + 'std': True, + 'nans': True, + 'geometricMean': True, + 'percentile': [0.5, 1], + }) + + # Test round-tripping summaryOptions + hist = hist.Clone() + stats = hist.statistics_scalars + self.assertEqual(stats['nans'].unit, 'count') + self.assertEqual(stats['nans'].value, 1) + self.assertEqual(stats['count'].unit, 'count') + self.assertEqual(stats['count'].value, 3) + self.assertEqual(stats['min'].unit, hist.unit) + self.assertEqual(stats['min'].value, 50) + self.assertEqual(stats['max'].unit, hist.unit) + self.assertEqual(stats['max'].value, 70) + self.assertEqual(stats['sum'].unit, hist.unit) + self.assertEqual(stats['sum'].value, 180) + self.assertEqual(stats['avg'].unit, hist.unit) + self.assertEqual(stats['avg'].value, 60) + self.assertEqual(stats['std'].unit, hist.unit) + self.assertEqual(stats['std'].value, 10) + self.assertEqual(stats['pct_050'].unit, hist.unit) + self.assertEqual(stats['pct_050'].value, 60.5) + self.assertEqual(stats['pct_100'].unit, hist.unit) + self.assertEqual(stats['pct_100'].value, 70.5) + self.assertEqual(stats['geometricMean'].unit, hist.unit) + self.assertLess(abs(stats['geometricMean'].value - 59.439), 1e-3) + + hist.CustomizeSummaryOptions({ + 'count': False, + 'min': False, + 'max': False, + 'sum': False, + 'avg': False, + 'std': False, + 'nans': False, + 'geometricMean': False, + 'percentile': [], + }) + self.assertEqual(0, len(hist.statistics_scalars)) + + def testStatisticsScalarsEmpty(self): + b = histogram.HistogramBinBoundaries.CreateLinear(0, 100, 100) + hist = histogram.Histogram('', 'unitless', b) + hist.CustomizeSummaryOptions({ + 'count': True, + 'min': True, + 'max': True, + 'sum': True, + 'avg': True, + 'std': True, + 'nans': True, + 'geometricMean': True, + 'percentile': [0, 0.01, 0.1, 0.5, 0.995, 1], + }) + stats = hist.statistics_scalars + self.assertEqual(stats['nans'].value, 0) + self.assertEqual(stats['count'].value, 0) + self.assertEqual(stats['min'].value, histogram.JS_MAX_VALUE) + self.assertEqual(stats['max'].value, -histogram.JS_MAX_VALUE) + self.assertEqual(stats['sum'].value, 0) + self.assertNotIn('avg', stats) + self.assertNotIn('stddev', stats) + self.assertEqual(stats['pct_000'].value, 0) + self.assertEqual(stats['pct_001'].value, 0) + self.assertEqual(stats['pct_010'].value, 0) + self.assertEqual(stats['pct_050'].value, 0) + self.assertEqual(stats['pct_099_5'].value, 0) + self.assertEqual(stats['pct_100'].value, 0) + + def testSampleValues(self): + hist0 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + hist1 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + self.assertEqual(hist0.max_num_sample_values, 120) + self.assertEqual(hist1.max_num_sample_values, 120) + values0 = [] + values1 = [] + for i in range(10): + values0.append(i) + hist0.AddSample(i) + values1.append(10 + i) + hist1.AddSample(10 + i) + self.assertDeepEqual(hist0.sample_values, values0) + self.assertDeepEqual(hist1.sample_values, values1) + hist0.AddHistogram(hist1) + self.assertDeepEqual(hist0.sample_values, values0 + values1) + hist2 = hist0.Clone() + self.assertDeepEqual(hist2.sample_values, values0 + values1) + + for i in range(200): + hist0.AddSample(i) + self.assertEqual(len(hist0.sample_values), hist0.max_num_sample_values) + + hist3 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES) + hist3.max_num_sample_values = 10 + for i in range(100): + hist3.AddSample(i) + self.assertEqual(len(hist3.sample_values), 10) + + def testSingularBin(self): + hist = histogram.Histogram( + '', 'unitless', histogram.HistogramBinBoundaries.SINGULAR) + self.assertEqual(1, len(hist.bins)) + d = hist.AsDict() + self.assertNotIn('binBoundaries', d) + clone = histogram.Histogram.FromDict(d) + self.assertEqual(1, len(clone.bins)) + self.assertDeepEqual(d, clone.AsDict()) + + self.assertEqual(0, hist.GetApproximatePercentile(0)) + self.assertEqual(0, hist.GetApproximatePercentile(1)) + hist.AddSample(0) + self.assertEqual(0, hist.GetApproximatePercentile(0)) + self.assertEqual(0, hist.GetApproximatePercentile(1)) + hist.AddSample(1) + self.assertEqual(0, hist.GetApproximatePercentile(0)) + self.assertEqual(1, hist.GetApproximatePercentile(1)) + hist.AddSample(2) + self.assertEqual(0, hist.GetApproximatePercentile(0)) + self.assertEqual(1, hist.GetApproximatePercentile(0.5)) + self.assertEqual(2, hist.GetApproximatePercentile(1)) + hist.AddSample(3) + self.assertEqual(0, hist.GetApproximatePercentile(0)) + self.assertEqual(1, hist.GetApproximatePercentile(0.5)) + self.assertEqual(2, hist.GetApproximatePercentile(0.9)) + self.assertEqual(3, hist.GetApproximatePercentile(1)) + hist.AddSample(4) + self.assertEqual(0, hist.GetApproximatePercentile(0)) + self.assertEqual(1, hist.GetApproximatePercentile(0.4)) + self.assertEqual(2, hist.GetApproximatePercentile(0.7)) + self.assertEqual(3, hist.GetApproximatePercentile(0.9)) + self.assertEqual(4, hist.GetApproximatePercentile(1)) + + +class DiagnosticMapUnittest(unittest.TestCase): + def testDisallowReservedNames(self): + diagnostics = histogram.DiagnosticMap() + with self.assertRaises(TypeError): + diagnostics[None] = generic_set.GenericSet(()) + with self.assertRaises(TypeError): + diagnostics['generic'] = None + diagnostics[reserved_infos.TRACE_URLS.name] = date_range.DateRange(0) + diagnostics.DisallowReservedNames() + diagnostics[reserved_infos.TRACE_URLS.name] = generic_set.GenericSet(()) + with self.assertRaises(TypeError): + diagnostics[reserved_infos.TRACE_URLS.name] = date_range.DateRange(0) + + def testResetGuid(self): + generic = generic_set.GenericSet(['generic diagnostic']) + guid1 = generic.guid + generic.ResetGuid() + guid2 = generic.guid + self.assertNotEqual(guid1, guid2) + + # TODO(eakuefner): Find a better place for these non-map tests once we + # break up the Python implementation more. + def testInlineSharedDiagnostic(self): + generic = generic_set.GenericSet(['generic diagnostic']) + hist = histogram.Histogram('', 'count') + _ = generic.guid # First access sets guid + hist.diagnostics['foo'] = generic + generic.Inline() + self.assertFalse(generic.has_guid) + hist_dict = hist.AsDict() + diag_dict = hist_dict['diagnostics']['foo'] + self.assertIsInstance(diag_dict, dict) + self.assertEqual(diag_dict['type'], 'GenericSet') + + def testCloneWithRef(self): + diagnostics = histogram.DiagnosticMap() + diagnostics['ref'] = diagnostic_ref.DiagnosticRef('abc') + + clone = histogram.DiagnosticMap.FromDict(diagnostics.AsDict()) + self.assertIsInstance(clone.get('ref'), diagnostic_ref.DiagnosticRef) + self.assertEqual(clone.get('ref').guid, 'abc') + + def testDiagnosticGuidDeserialized(self): + d = { + 'type': 'GenericSet', + 'values': [], + 'guid': 'bar' + } + g = diagnostic.Diagnostic.FromDict(d) + self.assertEqual('bar', g.guid) + + def testMerge(self): + events = related_event_set.RelatedEventSet() + events.Add({ + 'stableId': '0.0', + 'title': 'foo', + 'start': 0, + 'duration': 1, + }) + generic = generic_set.GenericSet(['generic diagnostic']) + generic2 = generic_set.GenericSet(['generic diagnostic 2']) + related_map = related_name_map.RelatedNameMap() + related_map.Set('a', 'histogram') + + hist = histogram.Histogram('', 'count') + + # When Histograms are merged, first an empty clone is created with an empty + # DiagnosticMap. + hist2 = histogram.Histogram('', 'count') + hist2.diagnostics['a'] = generic + hist.diagnostics.Merge(hist2.diagnostics) + self.assertIs(generic, hist.diagnostics['a']) + + # Separate keys are not merged. + hist3 = histogram.Histogram('', 'count') + hist3.diagnostics['b'] = generic2 + hist.diagnostics.Merge(hist3.diagnostics) + self.assertIs(generic, hist.diagnostics['a']) + self.assertIs(generic2, hist.diagnostics['b']) + + # Merging unmergeable diagnostics should produce an + # UnmergeableDiagnosticSet. + hist4 = histogram.Histogram('', 'count') + hist4.diagnostics['a'] = related_map + hist.diagnostics.Merge(hist4.diagnostics) + self.assertIsInstance(hist.diagnostics['a'], ums.UnmergeableDiagnosticSet) + diagnostics = list(hist.diagnostics['a']) + self.assertIs(generic, diagnostics[0]) + self.assertIs(related_map, diagnostics[1]) + + # UnmergeableDiagnosticSets are mergeable. + hist5 = histogram.Histogram('', 'count') + hist5.diagnostics['a'] = ums.UnmergeableDiagnosticSet([events, generic2]) + hist.diagnostics.Merge(hist5.diagnostics) + self.assertIsInstance(hist.diagnostics['a'], ums.UnmergeableDiagnosticSet) + diagnostics = list(hist.diagnostics['a']) + self.assertIs(generic, diagnostics[0]) + self.assertIs(related_map, diagnostics[1]) + self.assertIs(events, diagnostics[2]) + self.assertIs(generic2, diagnostics[3]) diff --git a/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py new file mode 100644 index 00000000000..ce1b90f55b1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py @@ -0,0 +1,22 @@ +# Copyright 2017 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. + +import os +import vinn +import tracing_project + +def HistogramsToCsv(json_path): + """Convert HistogramSet JSON to CSV. + + Args: + json_path: path to a file containing HistogramSet JSON + + Returns: + a Vinn result object whose 'returncode' indicates whether there was an + exception, and whose 'stdout' contains CSV. + """ + return vinn.RunFile( + os.path.join(os.path.dirname(__file__), 'histograms_to_csv_cmdline.html'), + source_paths=list(tracing_project.TracingProject().source_paths), + js_args=[os.path.abspath(json_path)]) diff --git a/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html new file mode 100644 index 00000000000..c3b5163544e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html @@ -0,0 +1,23 @@ +<!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/xhr.html"> +<link rel="import" href="/tracing/value/csv_builder.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +/* eslint-disable no-console */ + +if (tr.isHeadless) { + const histograms = new tr.v.HistogramSet(); + histograms.importDicts(JSON.parse(tr.b.getSync('file://' + sys.argv[1]))); + const csv = new tr.v.CSVBuilder(histograms); + csv.build(); + console.log(csv.toString()); +} +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py new file mode 100755 index 00000000000..a940f3dde5c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py @@ -0,0 +1,68 @@ +# Copyright 2018 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. + +from tracing.value import histogram +from tracing.value import histogram_set +from tracing.value.diagnostics import generic_set +from tracing.value.diagnostics import reserved_infos + + +def ConvertLegacyDicts(dicts): + """Convert legacy JSON dicts to Histograms. + + Args: + dicts: A list of v0 JSON dicts + + Returns: + A HistogramSet containing equivalent histograms and diagnostics + """ + if len(dicts) < 1: + return histogram_set.HistogramSet() + + first_dict = dicts[0] + master = first_dict['master'] + bot = first_dict['bot'] + suite = first_dict['test'].split('/')[0] + + hs = histogram_set.HistogramSet() + + for d in dicts: + assert d['master'] == master + assert d['bot'] == bot + + test_parts = d['test'].split('/') + assert test_parts[0] == suite + name = test_parts[1] + + # TODO(843643): Generalize this + assert 'units' not in d + # TODO(861822): Port this to CreateHistogram + h = histogram.Histogram(name, 'unitless') + h.AddSample(d['value']) + # TODO(876379): Support more than three components + if len(test_parts) == 3: + h.diagnostics[reserved_infos.STORIES.name] = generic_set.GenericSet( + [test_parts[2]]) + + hs.AddHistogram(h) + + hs.AddSharedDiagnosticToAllHistograms( + reserved_infos.MASTERS.name, generic_set.GenericSet([master])) + hs.AddSharedDiagnosticToAllHistograms( + reserved_infos.BOTS.name, generic_set.GenericSet([bot])) + hs.AddSharedDiagnosticToAllHistograms( + reserved_infos.BENCHMARKS.name, generic_set.GenericSet([suite])) + _AddRevision(first_dict, hs) + + return hs + + +def _AddRevision(d, hs): + r_commit_pos = d.get('supplemental_columns', {}).get('r_commit_pos') + rev = d['revision'] + if r_commit_pos == rev: + name = reserved_infos.CHROMIUM_COMMIT_POSITIONS.name + else: + name = reserved_infos.POINT_ID.name + hs.AddSharedDiagnosticToAllHistograms(name, generic_set.GenericSet([rev])) diff --git a/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py new file mode 100644 index 00000000000..84b173fd04b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py @@ -0,0 +1,116 @@ +# Copyright 2018 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. + +import unittest + +from tracing.value import legacy_json_converter +from tracing.value.diagnostics import reserved_infos + + +class LegacyJsonConverterUnittest(unittest.TestCase): + + def testConvertBasic(self): + dicts = [{ + 'master': 'Master', + 'bot': 'Bot', + 'test': 'Suite/Metric', + 'revision': 1234, + 'value': 42.0 + }] + + histograms = legacy_json_converter.ConvertLegacyDicts(dicts) + + self.assertEqual(len(histograms), 1) + + h = histograms.GetFirstHistogram() + + self.assertEqual(h.name, 'Metric') + self.assertEqual(len(h.diagnostics), 4) + + masters = h.diagnostics[reserved_infos.MASTERS.name] + self.assertEqual(masters.GetOnlyElement(), 'Master') + + bots = h.diagnostics[reserved_infos.BOTS.name] + self.assertEqual(bots.GetOnlyElement(), 'Bot') + + benchmarks = h.diagnostics[reserved_infos.BENCHMARKS.name] + self.assertEqual(benchmarks.GetOnlyElement(), 'Suite') + + point_id = h.diagnostics[reserved_infos.POINT_ID.name].GetOnlyElement() + self.assertEqual(point_id, 1234) + + self.assertEqual(h.num_values, 1) + self.assertEqual(h.average, 42.0) + + def testConvertWithStory(self): + dicts = [{ + 'master': 'Master', + 'bot': 'Bot', + 'test': 'Suite/Metric/Case', + 'revision': 1234, + 'value': 42.0 + }] + + histograms = legacy_json_converter.ConvertLegacyDicts(dicts) + h = histograms.GetFirstHistogram() + + stories = h.diagnostics[reserved_infos.STORIES.name] + self.assertEqual(stories.GetOnlyElement(), 'Case') + + def testConvertWithRCommitPos(self): + dicts = [{ + 'master': 'Master', + 'bot': 'Bot', + 'test': 'Suite/Metric/Case', + 'revision': 1234, + 'value': 42.0, + 'supplemental_columns': { + 'r_commit_pos': 1234 + } + }] + + histograms = legacy_json_converter.ConvertLegacyDicts(dicts) + h = histograms.GetFirstHistogram() + + commit_pos = h.diagnostics[reserved_infos.CHROMIUM_COMMIT_POSITIONS.name] + self.assertEqual(commit_pos.GetOnlyElement(), 1234) + self.assertNotIn(reserved_infos.POINT_ID.name, h.diagnostics) + + def testConvertWithDifferentRCommitPos(self): + dicts = [{ + 'master': 'Master', + 'bot': 'Bot', + 'test': 'Suite/Metric/Case', + 'revision': 1234, + 'value': 42.0, + 'supplemental_columns': { + 'r_commit_pos': 2435 + } + }] + + histograms = legacy_json_converter.ConvertLegacyDicts(dicts) + h = histograms.GetFirstHistogram() + + point_id = h.diagnostics[reserved_infos.POINT_ID.name].GetOnlyElement() + self.assertEqual(point_id, 1234) + self.assertNotIn(reserved_infos.CHROMIUM_COMMIT_POSITIONS.name, + h.diagnostics) + + def testConvertUsesPointIdIfSupplementalColumnsButNoRCommitPos(self): + dicts = [{ + 'master': 'Master', + 'bot': 'Bot', + 'test': 'Suite/Metric/Case', + 'revision': 1234, + 'value': 42.0, + 'supplemental_columns': { + 'r_v8_rev': 1234 + } + }] + + histograms = legacy_json_converter.ConvertLegacyDicts(dicts) + h = histograms.GetFirstHistogram() + + point_id = h.diagnostics[reserved_infos.POINT_ID.name].GetOnlyElement() + self.assertEqual(point_id, 1234) diff --git a/chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html b/chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html new file mode 100644 index 00000000000..801ae4d6533 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html @@ -0,0 +1,271 @@ +<!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/unit.html"> + +<script> +'use strict'; +tr.exportTo('tr.v', function() { + // This information is used to convert results from chart-json format to + // Histograms. + // Improvement directions are copied from + // telemetry/telemetry/value/unit-info.json + // but can be overridden by 'improvement_direction' in chart-json. + const LEGACY_UNIT_INFO = new Map(); + LEGACY_UNIT_INFO.set('%', { + name: 'normalizedPercentage', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.DONT_CARE, + }); + LEGACY_UNIT_INFO.set('Celsius', { + name: 'unitlessNumber', + // Colder machines are faster. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('Hz', { + name: 'unitlessNumber', + // Higher frequencies are faster. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('KB', { + name: 'sizeInBytes', + conversionFactor: 1024, + // Less memory usage is better. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('MB', { + name: 'sizeInBytes', + conversionFactor: 1024 * 1024, + // Less memory usage is better. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('ObjectsAt30FPS', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('available_kB', { + name: 'sizeInBytes', + conversionFactor: 1024, + // More memory available is better. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('bit/s', { + name: 'unitlessNumber', + // TODO(#3815) Reconcile with char/s. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('bytes', { + name: 'sizeInBytes', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('chars/s', { + name: 'unitlessNumber', + // TODO(#3815) Reconcile with bit/s. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('commit_count', { + name: 'count', + // layer_tree_host_perftest + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('count', { + name: 'count', + // Processes + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('coverage%', { + name: 'normalizedPercentage', + // Used in alloy-perf-test/cts%/passed. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('dB', { + name: 'unitlessNumber', + // Decibels peak signal-to-noise ratio. Used by WebRTC quality tests. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('files', { + name: 'count', + // Static initializers + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('fps', { + name: 'unitlessNumber', + // Used by scirra benchmark. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('frame_count', { + name: 'count', + // layer_tree_host_perftest + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('frame_time', { + name: 'timeDurationInMs', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('frames', { + name: 'count', + // Dropped frames. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('frames-per-second', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('garbage_collections', { + name: 'count', + // Number of GCs needed to collect an object. Less is better. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('idle%', { + name: 'normalizedPercentage', + // Percentage of work done in idle time. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('janks', { + name: 'count', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('kb', { + name: 'sizeInBytes', + conversionFactor: 1024, + // Less memory usage is better. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('lines', { + name: 'count', + // More test coverage is better. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('mWh', { + name: 'energyInJoules', + conversionFactor: 3.6, + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('milliseconds', { + name: 'timeDurationInMs', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('milliseconds-per-frame', { + name: 'timeDurationInMs', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('minutes', { + name: 'timeInMsAutoFormat', + conversionFactor: 60e3, + // Used for NaCl build time. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('mips', { + name: 'unitlessNumber', + // More instructions processed per time unit. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('mpixels_sec', { + name: 'unitlessNumber', + // More pixels processed per time unit. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('ms', { + name: 'timeDurationInMs', + // Used in many Telemetry measurements. Fewer ms of time means faster. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('mtri_sec', { + name: 'unitlessNumber', + // More triangles processed per time unit. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('mvtx_sec', { + name: 'unitlessNumber', + // More vertices processed per time unit. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('objects (bigger is better)', { + name: 'count', + // Used in spaceport benchmark. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('packets', { + name: 'count', + // Monitors how many packets we use to accomplish something. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('percent', { + name: 'normalizedPercentage', + // Synonym for %, used in memory metric for percent fragmentation. + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('points', { + name: 'unitlessNumber', + // Synonym for score, used in ChromeOS touchpad tests. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('ports', { + name: 'count', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('reduction%', { + name: 'normalizedPercentage', + // Used in draw_property measurement to indicate relative improvement. + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('relocs', { + name: 'count', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('runs/s', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('runs_per_s', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('runs_per_second', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('score (bigger is better)', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('score', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('score_(bigger_is_better)', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('seconds', { + name: 'timeDurationInMs', + conversionFactor: 1e3, + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('tokens/s', { + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('tasks', { + // Used by thread_times but actually indicates tasks/s, so not count. + name: 'unitlessNumber', + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + LEGACY_UNIT_INFO.set('us', { + name: 'timeDurationInMs', + conversionFactor: 1e-3, + defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER, + }); + + return { + LEGACY_UNIT_INFO, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py new file mode 100644 index 00000000000..c7e4c9a95b1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py @@ -0,0 +1,34 @@ +# 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. + +import json +import os +import sys + +import tracing_project +import vinn + +_MERGE_HISTOGRAMS_CMD_LINE = os.path.join( + os.path.dirname(__file__), 'merge_histograms_cmdline.html') + + +def MergeHistograms(json_path, groupby=()): + """Merge Histograms. + + Args: + json_path: Path to a HistogramSet JSON file. + groupby: Array of grouping keys (name, benchmark, time, storyset_repeat, + story_repeat, story, tir, label) + Returns: + HistogramSet dicts of the merged Histograms. + """ + result = vinn.RunFile( + _MERGE_HISTOGRAMS_CMD_LINE, + source_paths=list(tracing_project.TracingProject().source_paths), + js_args=[os.path.abspath(json_path)] + list(groupby)) + if result.returncode != 0: + sys.stderr.write(result.stdout) + raise Exception('vinn merge_histograms_cmdline.html returned ' + + str(result.returncode)) + return json.loads(result.stdout) diff --git a/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html new file mode 100644 index 00000000000..090390e81e8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html @@ -0,0 +1,84 @@ +<!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/xhr.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +/* eslint-disable no-console */ + +function findGrouping(key) { + const grouping = tr.v.HistogramGrouping.BY_KEY.get(key); + if (grouping === undefined) { + throw new Error(`Could not find grouping "${key}"`); + } + return grouping; +} + +function findMergeable(hist, candidates) { + for (const candidate of candidates) { + if (candidate.canAddHistogram(hist)) return candidate; + } + return undefined; +} + +function mergeLeafHistograms(groupedHistograms, mergedHistograms) { + for (const [name, histograms] of groupedHistograms) { + if (histograms instanceof Map) { + mergeLeafHistograms(histograms, mergedHistograms); + continue; + } + + if (histograms.length === 1) { + mergedHistograms.addHistogram(histograms[0].clone()); + continue; + } + + // Merge Histograms in this leaf array and return the merged Histograms to + // mergedHistograms. + // If it isn't possible to merge all Histograms in |histograms| together, + // then merge them into as few merged Histograms as possible. + const merged = [histograms.shift().clone()]; + for (const hist of histograms) { + const candidate = findMergeable(hist, merged); + if (candidate !== undefined) { + candidate.addHistogram(hist); + continue; + } + merged.push(hist.clone()); + } + for (const hist of merged) { + mergedHistograms.addHistogram(hist); + } + } +} + +function stripInternalDiagnostics(mergedHistograms) { + for (const hist of mergedHistograms) { + hist.diagnostics.delete(tr.v.d.RESERVED_NAMES.TEST_PATH); + } +} + +function mergeHistograms(histogramsPath, groupingKeys) { + const histograms = new tr.v.HistogramSet(); + histograms.importDicts(JSON.parse(tr.b.getSync('file://' + histogramsPath))); + histograms.buildGroupingsFromTags([tr.v.d.RESERVED_NAMES.STORY_TAGS]); + const groupings = groupingKeys.map(findGrouping); + const groupedHistograms = histograms.groupHistogramsRecursively(groupings); + const mergedHistograms = new tr.v.HistogramSet(); + mergeLeafHistograms(groupedHistograms, mergedHistograms); + mergedHistograms.deduplicateDiagnostics(); + stripInternalDiagnostics(mergedHistograms); + return mergedHistograms; +} + +if (tr.isHeadless) { + const mergedHistograms = mergeHistograms(sys.argv[1], sys.argv.slice(2)); + console.log(JSON.stringify(mergedHistograms.asDicts())); +} +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py new file mode 100644 index 00000000000..376f3ac59d8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py @@ -0,0 +1,31 @@ +# Copyright 2017 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. + +import json +import os +import tempfile +import unittest + +from tracing.value import histogram +from tracing.value import histogram_set +from tracing.value import merge_histograms + + +class MergeHistogramsUnittest(unittest.TestCase): + + def testSingularHistogramsGetMergedFrom(self): + hist0 = histogram.Histogram('foo', 'count') + hist1 = histogram.Histogram('bar', 'count') + histograms = histogram_set.HistogramSet([hist0, hist1]) + histograms_file = tempfile.NamedTemporaryFile(delete=False) + histograms_file.write(json.dumps(histograms.AsDicts()).encode('utf-8')) + histograms_file.close() + + merged_dicts = merge_histograms.MergeHistograms(histograms_file.name, + ('name',)) + merged_histograms = histogram_set.HistogramSet() + merged_histograms.ImportDicts(merged_dicts) + self.assertEqual(len(list(merged_histograms.shared_diagnostics)), 0) + self.assertEqual(len(merged_histograms), 2) + os.remove(histograms_file.name) diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html new file mode 100644 index 00000000000..228afbd2891 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html @@ -0,0 +1,350 @@ +<!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/fixed_color_scheme.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/metrics/all_fixed_color_schemes.html"> +<link rel="import" href="/tracing/ui/base/column_chart.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-breakdown-span"> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + #table_container { + display: flex; + flex: 0 0 auto; + } + #table { + max-height: 150px; + overflow-y: auto; + } + </style> + + <div id="empty">(empty)</div> + <div id="table_container"> + <div id="container"></div> + <span> + <tr-ui-b-table id="table"></tr-ui-b-table> + </span> + </div> + </template> +</dom-module> + +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + const DEFAULT_COLOR_SCHEME = new tr.b.SinebowColorGenerator(); + + function getHistogramName(histogram, diagnosticName, key) { + if (histogram === undefined) return undefined; + const nameMap = histogram.diagnostics.get(diagnosticName); + if (nameMap === undefined) return undefined; + return nameMap.get(key); + } + + class BreakdownTableSummaryRow { + constructor(displayElement, histogramNames) { + this.displayElement_ = displayElement; + this.histogramNames_ = histogramNames; + this.keySpan_ = undefined; + } + + get numberValue() { + // Prevent this row from appearing in the ColumnChart. + return undefined; + } + + get keySpan() { + if (this.keySpan_ === undefined) { + if (this.histogramNames_.length) { + this.keySpan_ = document.createElement('tr-ui-a-analysis-link'); + this.keySpan_.setSelectionAndContent( + this.histogramNames_, 'Select All'); + } else { + this.keySpan_ = 'Sum'; + } + } + return this.keySpan_; + } + + get name() { + return 'Sum'; + } + + get displayElement() { + return this.displayElement_; + } + + get stringPercent() { + return '100%'; + } + } + + class BreakdownTableRow { + constructor(name, value, histogramName, unit, color) { + this.name_ = name; + this.value_ = value; + this.histogramName_ = histogramName; + this.unit_ = unit; + + if (typeof value !== 'number') { + throw new Error('unsupported value ' + value); + } + + this.tableSum_ = undefined; + this.keySpan_ = undefined; + + this.color_ = color; + const hsl = this.color.toHSL(); + hsl.l *= 0.85; + this.highlightedColor_ = tr.b.Color.fromHSL(hsl); + + if (this.unit_) { + this.displayElement_ = tr.v.ui.createScalarSpan(this.numberValue, { + unit: this.unit_, + }); + } else { + this.displayElement_ = tr.ui.b.createSpan({ + textContent: this.stringValue, + }); + } + } + + get name() { + return this.name_; + } + + get color() { + return this.color_; + } + + get highlightedColor() { + return this.highlightedColor_; + } + + get keySpan() { + if (this.keySpan_ === undefined) { + if (this.histogramName_) { + this.keySpan_ = document.createElement('tr-ui-a-analysis-link'); + this.keySpan_.setSelectionAndContent( + [this.histogramName_], this.name); + this.keySpan_.color = this.color; + this.keySpan_.title = this.histogramName_; + } else { + this.keySpan_ = document.createElement('span'); + this.keySpan_.innerText = this.name; + this.keySpan_.style.color = this.color; + } + } + return this.keySpan_; + } + + /** + * @return {number|undefined} + */ + get numberValue() { + if (!isNaN(this.value_) && + (this.value_ !== Infinity) && + (this.value_ !== -Infinity) && + (this.value_ > 0)) return this.value_; + // Prevent this row from appearing in the ColumnChart. + return undefined; + } + + get stringValue() { + if ((this.unit_ !== undefined) && + !isNaN(this.value_) && + (this.value_ !== Infinity) && + (this.value_ !== -Infinity)) { + return this.unit_.format(this.value_); + } + return this.value_.toString(); + } + + set tableSum(s) { + this.tableSum_ = s; + } + + get stringPercent() { + if (this.tableSum_ === undefined) return ''; + const num = this.numberValue; + if (num === undefined) return ''; + return Math.floor(num * 100.0 / this.tableSum_) + '%'; + } + + get displayElement() { + return this.displayElement_; + } + + compare(other) { + if (this.numberValue === undefined) { + if (other.numberValue === undefined) { + return this.name.localeCompare(other.name); + } + return 1; + } + if (other.numberValue === undefined) { + return -1; + } + if (this.numberValue === other.numberValue) { + return this.name.localeCompare(other.name); + } + return other.numberValue - this.numberValue; + } + } + + Polymer({ + is: 'tr-v-ui-breakdown-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + created() { + this.chart_ = new tr.ui.b.ColumnChart(); + this.chart_.graphHeight = 130; + this.chart_.isStacked = true; + this.chart_.hideXAxis = true; + this.chart_.hideLegend = true; + this.chart_.enableHoverBox = false; + this.chart_.addEventListener('rect-mouseenter', + event => this.onRectMouseEnter_(event)); + this.chart_.addEventListener('rect-mouseleave', + event => this.onRectMouseLeave_(event)); + }, + + onRectMouseEnter_(event) { + for (const row of this.$.table.tableRows) { + if (row.name === event.rect.key) { + row.displayElement.style.background = event.rect.color; + row.keySpan.scrollIntoViewIfNeeded(); + } else { + row.displayElement.style.background = ''; + } + } + }, + + onRectMouseLeave_(event) { + for (const row of this.$.table.tableRows) { + row.displayElement.style.background = ''; + } + }, + + ready() { + Polymer.dom(this.$.container).appendChild(this.chart_); + + this.$.table.zebra = true; + this.$.table.showHeader = false; + this.$.table.tableColumns = [ + { + value: row => row.keySpan, + }, + { + value: row => row.displayElement, + align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT, + }, + { + value: row => row.stringPercent, + align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT, + }, + ]; + }, + + updateContents_() { + this.$.container.style.display = 'none'; + this.$.table.style.display = 'none'; + this.$.empty.style.display = 'block'; + + if (!this.diagnostic_) { + this.chart_.data = []; + return; + } + + if (this.histogram_) this.chart_.unit = this.histogram_.unit; + + let colorScheme = undefined; + // https://github.com/catapult-project/catapult/issues/2970 + if (this.diagnostic.colorScheme === + tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER) { + colorScheme = (name) => { + let cat = name.split(' '); + cat = cat[cat.length - 1]; + return tr.e.chrome.ChromeUserFriendlyCategoryDriver.getColor(cat); + }; + } else if (this.diagnostic.colorScheme !== undefined) { + colorScheme = (name) => tr.b.FixedColorSchemeRegistry.lookUp( + this.diagnostic.colorScheme).getColor(name); + } else { + colorScheme = (name) => DEFAULT_COLOR_SCHEME.colorForKey(name); + } + + const tableRows = []; + let tableSum = 0; + const histogramNames = []; + for (const [key, value] of this.diagnostic) { + const histogramName = getHistogramName( + this.histogram_, this.name_, key); + const row = new BreakdownTableRow( + key, value, histogramName, this.chart_.unit, colorScheme(key)); + tableRows.push(row); + if (row.numberValue !== undefined) tableSum += row.numberValue; + if (histogramName) { + histogramNames.push(histogramName); + } + } + tableRows.sort((x, y) => x.compare(y)); + + if (tableSum > 0) { + let summaryDisplayElement = tableSum; + if (this.chart_.unit !== undefined) { + summaryDisplayElement = this.chart_.unit.format(tableSum); + } + summaryDisplayElement = tr.ui.b.createSpan({ + textContent: summaryDisplayElement, + }); + tableRows.unshift(new BreakdownTableSummaryRow( + summaryDisplayElement, histogramNames)); + } + + const chartData = {x: 0}; + for (const row of tableRows) { + if (row.numberValue === undefined) continue; + + // Let the row compute its percentage. + row.tableSum = tableSum; + + // Add it to the chart. + chartData[row.name] = row.numberValue; + + // Configure the colors. + const dataSeries = this.chart_.getDataSeries(row.name); + dataSeries.color = row.color; + dataSeries.highlightedColor = row.highlightedColor; + } + + if (tableRows.length > 0) { + this.$.table.style.display = 'block'; + this.$.empty.style.display = 'none'; + this.$.table.tableRows = tableRows; + this.$.table.rebuild(); + } + + if (Object.keys(chartData).length > 1) { + this.$.container.style.display = 'block'; + this.$.empty.style.display = 'none'; + this.chart_.data = [chartData]; + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html new file mode 100644 index 00000000000..71079cdc856 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html @@ -0,0 +1,149 @@ +<!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/fixed_color_scheme.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/breakdown.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/breakdown_span.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_Breakdown', function() { + let breakdown = new tr.v.d.Breakdown(); + breakdown.colorScheme = + tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER; + breakdown.set('script', 42); + breakdown.set('style', 57); + + // Test weird numbers. + breakdown.set('ba---a', NaN); + breakdown.set('inf', Infinity); + breakdown.set('-inf', -Infinity); + breakdown.set('goose egg', 0); + breakdown.set('<0', -1); + + // Test lots of categories + for (let i = 0; i < 10; ++i) { + breakdown.set('cat ' + i, i); + } + + // Test round-tripping. + breakdown = tr.v.d.Diagnostic.fromDict(breakdown.asDict()); + + const span = tr.v.ui.createDiagnosticSpan(breakdown); + assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('instantiate_BreakdownWithFixedColorScheme', function() { + const colorScheme = tr.b.FixedColorScheme.fromNames([ + 'foo', + 'bar', + ]); + tr.b.FixedColorSchemeRegistry.register(() => colorScheme, { + 'name': 'myColorScheme', + }); + + let breakdown = new tr.v.d.Breakdown(); + breakdown.colorScheme = 'myColorScheme'; + breakdown.set('foo', 42); + breakdown.set('bar', 57); + + // Test round-tripping. + breakdown = tr.v.d.Diagnostic.fromDict(breakdown.asDict()); + + const span = tr.v.ui.createDiagnosticSpan(breakdown); + span.updateContents_(); + assert.strictEqual( + span.chart_.getDataSeries('foo').color, colorScheme.getColor('foo')); + this.addHTMLOutput(span); + }); + + test('empty', function() { + const breakdown = new tr.v.d.Breakdown(); + const span = tr.v.ui.createDiagnosticSpan(breakdown); + assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('emptyExceptForWeirdNumbers', function() { + const breakdown = new tr.v.d.Breakdown(); + breakdown.set('ba---a', NaN); + breakdown.set('inf', Infinity); + breakdown.set('-inf', -Infinity); + breakdown.set('goose egg', 0); + breakdown.set('<0', -1); + + const span = tr.v.ui.createDiagnosticSpan(breakdown); + assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('correlate', function() { + const histograms = new tr.v.HistogramSet(); + const sample0Breakdown = new tr.v.d.Breakdown(); + sample0Breakdown.set('a', 5); + sample0Breakdown.set('b', 3); + sample0Breakdown.set('c', 2); + const sample1Breakdown = new tr.v.d.Breakdown(); + sample1Breakdown.set('a', 50); + sample1Breakdown.set('b', 30); + sample1Breakdown.set('c', 20); + const related = new tr.v.d.RelatedNameMap(); + related.set('a', histograms.createHistogram('root:a', + tr.b.Unit.byName.timeDurationInMs, [5, 50]).name); + related.set('b', tr.v.Histogram.create('root:b', + tr.b.Unit.byName.timeDurationInMs, [3, 30]).name); + related.set('c', tr.v.Histogram.create('root:c', + tr.b.Unit.byName.timeDurationInMs, [2, 20]).name); + const hist = histograms.createHistogram('root', + tr.b.Unit.byName.timeDurationInMs, [ + { + value: 10, + diagnostics: new Map([['breakdown', sample0Breakdown]]), + }, + { + value: 100, + diagnostics: new Map([['breakdown', sample1Breakdown]]), + }, + ], { + diagnostics: new Map([ + ['breakdown', related], + ]), + }); + const span = tr.v.ui.createDiagnosticSpan(sample0Breakdown, 'breakdown', + hist); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, + 'tr-ui-a-analysis-link'); + assert.lengthOf(links, 4); + assert.strictEqual(links[0].title, ''); + assert.strictEqual(links[1].title, 'root:a'); + assert.strictEqual(links[2].title, 'root:b'); + assert.strictEqual(links[3].title, 'root:c'); + assert.strictEqual(links[0].textContent, 'Select All'); + assert.strictEqual(links[1].textContent, 'a'); + assert.strictEqual(links[2].textContent, 'b'); + assert.strictEqual(links[3].textContent, 'c'); + assert.lengthOf(links[0].selection, 3); + assert.strictEqual(links[0].selection[0], 'root:a'); + assert.strictEqual(links[0].selection[1], 'root:b'); + assert.strictEqual(links[0].selection[2], 'root:c'); + assert.lengthOf(links[1].selection, 1); + assert.strictEqual(links[1].selection[0], 'root:a'); + assert.lengthOf(links[2].selection, 1); + assert.strictEqual(links[2].selection[0], 'root:b'); + assert.lengthOf(links[3].selection, 1); + assert.strictEqual(links[3].selection[0], 'root:c'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html new file mode 100644 index 00000000000..08e0cc91dca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-collected-related-event-set-span"> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-collected-related-event-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + Polymer.dom(this).textContent = ''; + for (const [canonicalUrl, events] of this.diagnostic) { + const link = document.createElement('a'); + if (events.length === 1) { + const event = tr.b.getOnlyElement(events); + link.textContent = event.title + ' ' + + tr.b.Unit.byName.timeDurationInMs.format(event.duration); + } else { + link.textContent = events.length + ' events'; + } + link.href = canonicalUrl; + Polymer.dom(this).appendChild(link); + Polymer.dom(this).appendChild(document.createElement('br')); + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html new file mode 100644 index 00000000000..c14f75a9826 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('merge', function() { + let aSlice; + let bSlice; + const model = tr.c.TestUtils.newModel(function(model) { + aSlice = tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + title: 'a', + start: 0, + duration: 10 + }); + bSlice = tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + title: 'b', + start: 1, + duration: 10 + }); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + thread.sliceGroup.pushSlice(aSlice); + thread.sliceGroup.pushSlice(bSlice); + }); + assert.notEqual(aSlice.stableId, bSlice.stableId); + + const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.count); + const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.count); + + aHist.diagnostics.set('events', new tr.v.d.RelatedEventSet(aSlice)); + bHist.diagnostics.set('events', new tr.v.d.RelatedEventSet(bSlice)); + + let mergedHist = aHist.clone(); + mergedHist.addHistogram(bHist); + mergedHist = tr.v.Histogram.fromDict(mergedHist.asDict()); + + const mergedEvents = mergedHist.diagnostics.get('events'); + const span = tr.v.ui.createDiagnosticSpan(mergedEvents); + assert.strictEqual( + 'TR-V-UI-COLLECTED-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html new file mode 100644 index 00000000000..29773057810 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-date-range-span"> + <template> + <content></content> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-date-range-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + if (this.diagnostic === undefined) { + Polymer.dom(this).textContent = ''; + return; + } + + Polymer.dom(this).textContent = this.diagnostic.toString(); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html new file mode 100644 index 00000000000..3e7f02f0727 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/diagnostics/date_range.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_one', function() { + const diagnostic = new tr.v.d.DateRange(1496693745398); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-DATE-RANGE-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('instantiate_merged', function() { + const diagnostic = new tr.v.d.DateRange(1496693745398); + diagnostic.addDiagnostic(new tr.v.d.DateRange(1496693745399)); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-DATE-RANGE-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html new file mode 100644 index 00000000000..3a600c00925 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html @@ -0,0 +1,125 @@ +<!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/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<dom-module id="tr-v-ui-diagnostic-map-table"> + <template> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + function makeColumn(title, histogram) { + return { + title, + value(map) { + const diagnostic = map.get(title); + if (!diagnostic) return ''; + return tr.v.ui.createDiagnosticSpan(diagnostic, title, histogram); + } + }; + } + + Polymer({ + is: 'tr-v-ui-diagnostic-map-table', + + created() { + this.diagnosticMaps_ = undefined; + this.histogram_ = undefined; + this.isMetadata_ = false; + }, + + set histogram(h) { + this.histogram_ = h; + }, + + set isMetadata(m) { + this.isMetadata_ = m; + this.$.table.showHeader = !this.isMetadata_; + }, + + /** + * The |title| will be used as the heading for the column containing + * diagnostic-spans for |diagnosticMap|'s Diagnostics. + * + * @param {!Array.<!Object>} maps + * @param {!string} maps[].title + * @param {!tr.v.d.DiagnosticMap} maps[].diagnosticMap + */ + set diagnosticMaps(maps) { + this.diagnosticMaps_ = maps; + this.updateContents_(); + }, + + get diagnosticMaps() { + return this.diagnosticMaps_; + }, + + updateContents_() { + if (this.isMetadata_ && this.diagnosticMaps_.length !== 1) { + throw new Error( + 'Metadata diagnostic-map-tables require exactly 1 DiagnosticMap'); + } + if (this.diagnosticMaps_ === undefined || + this.diagnosticMaps_.length === 0) { + this.$.table.tableRows = []; + this.$.table.tableColumns = []; + return; + } + + let names = new Set(); + for (const map of this.diagnosticMaps_) { + for (const [name, diagnostic] of map) { + // https://github.com/catapult-project/catapult/issues/2842 + if (diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet) continue; + if (diagnostic instanceof tr.v.d.CollectedRelatedEventSet) continue; + + names.add(name); + } + } + names = Array.from(names).sort(); + + const histogram = this.histogram_; + if (this.isMetadata_) { + const diagnosticMap = this.diagnosticMaps_[0]; + this.$.table.tableColumns = [ + { + value(name) { + return name.name; + } + }, + { + value(name) { + const diagnostic = diagnosticMap.get(name.name); + if (!diagnostic) return ''; + return tr.v.ui.createDiagnosticSpan( + diagnostic, name.name, histogram); + } + }, + ]; + this.$.table.tableRows = names.map(name => { + // tr-ui-b-table requires rows to be objects. + return {name}; + }); + } else { + this.$.table.tableColumns = names.map( + name => makeColumn(name, histogram)); + this.$.table.tableRows = this.diagnosticMaps_; + } + + this.$.table.rebuild(); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html new file mode 100644 index 00000000000..d5d4ac02761 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html @@ -0,0 +1,27 @@ +<!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/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_map_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const map0 = new tr.v.d.DiagnosticMap(); + map0.set('genericA', new tr.v.d.GenericSet([{a: 0}])); + map0.set('genericB', new tr.v.d.GenericSet([{b: 0}])); + const map1 = new tr.v.d.DiagnosticMap(); + map1.set('genericA', new tr.v.d.GenericSet([{a: 1}])); + map1.set('genericB', new tr.v.d.GenericSet([{b: 1}])); + const table = document.createElement('tr-v-ui-diagnostic-map-table'); + table.diagnosticMaps = [map0, map1]; + this.addHTMLOutput(table); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html new file mode 100644 index 00000000000..741fc07f58e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html @@ -0,0 +1,73 @@ +<!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/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic.html"> +<link rel="import" href="/tracing/value/ui/breakdown_span.html"> +<link rel="import" href="/tracing/value/ui/collected_related_event_set_span.html"> +<link rel="import" href="/tracing/value/ui/date_range_span.html"> +<link rel="import" href="/tracing/value/ui/generic_set_span.html"> +<link rel="import" href="/tracing/value/ui/related_event_set_span.html"> +<link rel="import" href="/tracing/value/ui/scalar_diagnostic_span.html"> +<link rel="import" href="/tracing/value/ui/unmergeable_diagnostic_set_span.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + /** + * Find the name of a polymer element registered to display |diagnostic| + * or one of its base classes. + * + * @param {!tr.v.d.Diagnostic} diagnostic + * @return {string} + */ + function findElementNameForDiagnostic(diagnostic) { + let typeInfo = undefined; + let curProto = diagnostic.constructor.prototype; + while (curProto) { + typeInfo = tr.v.d.Diagnostic.findTypeInfo(curProto.constructor); + if (typeInfo && typeInfo.metadata.elementName) break; + typeInfo = undefined; + curProto = curProto.__proto__; + } + + if (typeInfo === undefined) { + throw new Error( + diagnostic.constructor.name + + ' or a base class must have a registered elementName'); + } + + const tagName = typeInfo.metadata.elementName; + + if (tr.ui.b.isUnknownElementName(tagName)) { + throw new Error('Element not registered: ' + tagName); + } + + return tagName; + } + + /** + * Create a visualization for |diagnostic|. + * + * @param {!tr.v.d.Diagnostic} diagnostic + * @param {string} name + * @param {!tr.v.Histogram} histogram + * @return {Element} + */ + function createDiagnosticSpan(diagnostic, name, histogram) { + const tagName = findElementNameForDiagnostic(diagnostic); + const span = document.createElement(tagName); + if (span.build === undefined) throw new Error(tagName); + span.build(diagnostic, name, histogram); + return span; + } + + return { + createDiagnosticSpan, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html new file mode 100644 index 00000000000..a40c15cb1c8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html @@ -0,0 +1,44 @@ +<!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/base.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const DIAGNOSTIC_SPAN_BEHAVIOR = { + created() { + this.diagnostic_ = undefined; + this.name_ = undefined; + this.histogram_ = undefined; + }, + + attached() { + if (this.diagnostic_) this.updateContents_(); + }, + + get diagnostic() { + return this.diagnostic_; + }, + + build(diagnostic, name, histogram) { + this.diagnostic_ = diagnostic; + this.name_ = name; + this.histogram_ = histogram; + if (this.isAttached) this.updateContents_(); + }, + + updateContents_() { + throw new Error('dom-modules must override updateContents_()'); + } + }; + + return { + DIAGNOSTIC_SPAN_BEHAVIOR, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html new file mode 100644 index 00000000000..6f355e44478 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html @@ -0,0 +1,97 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-generic-set-span"> + <template> + <style> + a { + display: block; + } + </style> + + <tr-ui-a-generic-object-view id="generic"></tr-ui-a-generic-object-view> + + <div id="links"></div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + function isLinkTuple(value) { + return ((value instanceof Array) && + (value.length === 2) && + (typeof value[0] === 'string') && + tr.b.isUrl(value[1])); + } + + Polymer({ + is: 'tr-v-ui-generic-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + this.$.generic.style.display = 'none'; + this.$.links.textContent = ''; + if (this.diagnostic === undefined) return; + const values = Array.from(this.diagnostic); + + let areAllStrings = true; + let areAllNumbers = true; + for (const value of values) { + if (typeof value !== 'number') { + areAllNumbers = false; + if (typeof value !== 'string' && !isLinkTuple(value)) { + areAllStrings = false; + break; + } + } + } + + if (!areAllStrings) { + this.$.generic.style.display = ''; + this.$.generic.object = values; + return; + } + + if (areAllNumbers) { + values.sort((x, y) => x - y); + } else { + values.sort(); + } + + for (const value of values) { + const link = {textContent: '' + value}; + if (isLinkTuple(value)) { + link.textContent = value[0]; + link.href = value[1]; + } else if (tr.b.isUrl(value)) { + link.href = value; + } + if (this.name_ === tr.v.d.RESERVED_NAMES.TRACE_URLS) { + link.textContent = value.substr(1 + value.lastIndexOf('/')); + } + const linkEl = tr.ui.b.createLink(link); + if (link.href) { + linkEl.target = '_blank'; + // In case there's a listener in the hierarchy that calls + // preventDefault(), stop the event from propagating to it so that + // clicking the link always opens it in a new tab. + linkEl.addEventListener('click', e => e.stopPropagation()); + } + this.$.links.appendChild(linkEl); + } + } + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html new file mode 100644 index 00000000000..6bddec81aa6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html @@ -0,0 +1,112 @@ +<!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/raf.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/generic_set.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('link_tuple', function() { + const diagnostic = new tr.v.d.GenericSet([ + ['label', 'http://example.com/'], + ]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, diagnostic.size); + assert.strictEqual('label', links[0].textContent); + assert.strictEqual('http://example.com/', links[0].href); + }); + + test('instantiate', function() { + const diagnostic = new tr.v.d.GenericSet([{foo: 'bar', baz: [42]}]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('strings', function() { + const diagnostic = new tr.v.d.GenericSet([ + 'foo', 'bar', 1, 0, Infinity, NaN, + ]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, diagnostic.size); + assert.strictEqual(links[0].textContent, '0'); + assert.strictEqual(links[0].href, ''); + assert.strictEqual(links[1].textContent, '1'); + assert.strictEqual(links[1].href, ''); + assert.strictEqual(links[2].textContent, 'Infinity'); + assert.strictEqual(links[2].href, ''); + assert.strictEqual(links[3].textContent, 'NaN'); + assert.strictEqual(links[3].href, ''); + assert.strictEqual(links[4].textContent, 'bar'); + assert.strictEqual(links[4].href, ''); + assert.strictEqual(links[5].textContent, 'foo'); + assert.strictEqual(links[5].href, ''); + }); + + test('numbers', function() { + const diagnostic = new tr.v.d.GenericSet([10, 1, 0, 2, 11]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, diagnostic.size); + assert.strictEqual(links[0].textContent, '0'); + assert.strictEqual(links[0].href, ''); + assert.strictEqual(links[1].textContent, '1'); + assert.strictEqual(links[1].href, ''); + assert.strictEqual(links[2].textContent, '2'); + assert.strictEqual(links[2].href, ''); + assert.strictEqual(links[3].textContent, '10'); + assert.strictEqual(links[3].href, ''); + assert.strictEqual(links[4].textContent, '11'); + assert.strictEqual(links[4].href, ''); + }); + + test('urls', function() { + const urls = [ + 'http://google.com/', + 'http://cnn.com/', + ]; + const span = tr.v.ui.createDiagnosticSpan(new tr.v.d.GenericSet(urls)); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, urls.length); + assert.strictEqual(links[0].textContent, urls[1]); + assert.strictEqual(links[0].href, urls[1]); + assert.strictEqual(links[1].textContent, urls[0]); + assert.strictEqual(links[1].href, urls[0]); + }); + + test('traceUrls', function() { + const urls = [ + 'https://console.developers.google.com/m/cloudstorage/b/chromium-telemetry/o/c.html', + 'file://d/e/f.html', + ]; + const span = tr.v.ui.createDiagnosticSpan( + new tr.v.d.GenericSet(urls), tr.v.d.RESERVED_NAMES.TRACE_URLS); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, urls.length); + assert.strictEqual(links[0].textContent, 'f.html'); + assert.strictEqual(links[0].href, urls[1]); + assert.strictEqual(links[1].textContent, 'c.html'); + assert.strictEqual(links[1].href, urls[0]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md new file mode 100644 index 00000000000..951e4a9918c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md @@ -0,0 +1,71 @@ +<!-- Copyright 2017 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. +--> + +# HistogramSet UI Architecture + +Documentation for users of this UI is in [metrics-results-ui](/docs/metrics-results-ui.md). + +This document outlines the MVC architecture of the implementation of the UI. + * Model: [HistogramSetViewState](/tracing/tracing/value/ui/histogram_set_view_state.html) + * searchQuery: regex filters Histogram names + * referenceDisplayLabel selects the reference column in the table + * showAll: when false, only sourceHistograms are shown in the table + * groupings: array of HistogramGroupings configures how the hierarchy is constructed + * sortColumnIndex + * sortDescending + * constrainNameColumn: whether the Name column in the table is constrained to 300px + * tableRowStates: Map from row name to HistogramSetTableRowState + * subRows: Map from row name to HistogramSetTableRowState + * isExpanded: whether the row is expanded to show its subRows + * isOverviewed: whether the overview charts are displayed + * cells: map from column names to HistogramSetTableCellState: + * isOpen: whether the cell's histogram-span is open and displaying the BarChart and Diagnostics + * brushedBinRange: which bins are brushed in the BarChart + * mergeSampleDiagnostics: whether sample diagnostics are merged + * Setters delegate to the main entry point, update(delta), which dispatches an update event to listeners + * View-Controllers: + * [histogram-set-view](/tracing/tracing/value/ui/histogram_set_view.html): + * Main entry point: build(HistogramSet, progressIndicator):Promise + * Displays "zero Histograms" + * Listens for download-csv event from [histogram-set-controls](/tracing/tracing/value/ui/histogram_set_controls.html) + * gets leafHistograms from the [histogram-set-table](/tracing/tracing/value/ui/histogram_set_table.html) + * builds a CSV using [CSVBuilder](/tracing/tracing/value/csv_builder.html) + * Collects possible configurations of the HistogramSet and passes them to the child elements directly (not through the HistogramSetViewState!): + * Possible groupings + * displayLabels + * baseStatisticNames + * Contains child elements: + * [histogram-set-controls](/tracing/tracing/value/ui/histogram_set_controls.html) + * visualizes and controls the top half of HistogramSetViewState: + * searchQuery + * toggle display of all isOvervieweds + * referenceDisplayLabel + * showAll + * groupings + * Displays a button to download a CSV of the leafHistograms + * Displays a "Help" link to [metrics-results-ui](/docs/metrics-results-ui.md) + * [histogram-set-table](/tracing/tracing/value/ui/histogram_set_table.html) + * Visualizes and controls the bottom half of HistogramSetViewState: + * sortColumnIndex + * sortDescending + * constrainNameColumn + * HistogramSetTableRowStates + * Builds [HistogramSetTableRow](/tracing/tracing/value/ui/histogram_set_table_row.html)s containing + * [histogram-set-table-name-cell](/tracing/tracing/value/ui/histogram_set_table_name_cell.html) + * Toggles HistogramSetTableRowState.isOverviewed + * Overview [NameLineChart](/tracing/tracing/ui/base/name_line_chart.html) + * [histogram-set-table-cell](/tracing/tracing/value/ui/histogram_set_table_cell.html) + * (missing) / (empty) / (unmergeable) + * when closed, [scalar-span](/tracing/tracing/value/ui/scalar_span.html) displays a single summary statistic + * when open, [histogram-span](/tracing/tracing/value/ui/histogram_span.html) contains: + * [NameBarChart](/tracing/tracing/ui/base/name_bar_chart.html) visualizes and controls HistogramSetTableCellState.brushedBinRange + * [scalar-map-table](/tracing/tracing/value/ui/scalar_map_table.html) of statistics + * Two [diagnostic-map-tables](/tracing/tracing/value/ui/diagnostic_map_table.html): one for Histogram.diagnostics and another for the sample diagnostics + * A checkbox to visualize and control HistogramSetTableCellState.mergeSampleDiagnostics + * Overview [NameLineChart](/tracing/tracing/ui/base/name_line_chart.html) + * Main entry points: + * build(allHistograms, sourceHistograms, displayLabels, progressIndicator):Promise + * onViewStateUpdate_(delta) + * The [HistogramSetLocation](/tracing/tracing/value/ui/histogram_set_location.html) synchronizes the HistogramSetViewState with the URL using the HTML5 history API. diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html new file mode 100644 index 00000000000..bb4e1e11ed0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/histogram_importer.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(() => { + const kHtmlString = '<script>throw new Error("oops");<' + '/script>'; + + function createHistogram(id) { + const histogram = + new tr.v.Histogram('name<' + id + '>', tr.b.Unit.byName.count); + histogram.addSample(id); + histogram.customizeSummaryOptions({ + count: false, + max: false, + min: false, + std: false, + sum: false, + }); + histogram.diagnostics.set('html', new tr.v.d.GenericSet([kHtmlString])); + return histogram; + } + + test('importZeroHistograms', async function() { + const loadingEl = document.createElement('div'); + this.addHTMLOutput(loadingEl); + const importer = new tr.v.HistogramImporter(loadingEl); + const histogramData = '\n'; + + const view = document.createElement('tr-v-ui-histogram-set-view'); + view.style.display = 'none'; + this.addHTMLOutput(view); + + await importer.importHistograms(histogramData, view); + + assert.strictEqual('block', view.style.display); + assert.strictEqual(undefined, view.histograms); + }); + + test('importOneHistogram', async function() { + const loadingEl = document.createElement('div'); + this.addHTMLOutput(loadingEl); + const importer = new tr.v.HistogramImporter(loadingEl); + + const hist = createHistogram(42); + const histogramData = '\n' + JSON.stringify(hist.asDict()) + '\n'; + + const view = document.createElement('tr-v-ui-histogram-set-view'); + view.style.display = 'none'; + this.addHTMLOutput(view); + + await importer.importHistograms(histogramData, view); + + assert.strictEqual('none', loadingEl.style.display); + assert.strictEqual('block', view.style.display); + assert.lengthOf(view.histograms, 1); + const histogram = view.histograms.getHistogramNamed('name<42>'); + assert.strictEqual(kHtmlString, tr.b.getOnlyElement( + histogram.diagnostics.get('html'))); + assert.deepEqual([42], histogram.sampleValues); + }); + + test('importNHistogram', async function() { + const loadingEl = document.createElement('div'); + this.addHTMLOutput(loadingEl); + const importer = new tr.v.HistogramImporter(loadingEl); + + const kNofHistograms = 1000; + let histogramData = '\n'; + for (let i = 0; i < kNofHistograms; i++) { + const id = kNofHistograms * 100 + i; + const histogram = createHistogram(id); + histogramData += JSON.stringify(histogram.asDict()) + '\n'; + } + + const view = document.createElement('tr-v-ui-histogram-set-view'); + view.style.display = 'none'; + this.addHTMLOutput(view); + + await importer.importHistograms(histogramData, view); + + assert.strictEqual('none', loadingEl.style.display); + assert.strictEqual('block', view.style.display); + assert.lengthOf(view.histograms, kNofHistograms); + for (let i = 0; i < kNofHistograms; i++) { + const id = kNofHistograms * 100 + i; + const histogram = view.histograms.getHistogramNamed('name<' + id + '>'); + assert.strictEqual(kHtmlString, tr.b.getOnlyElement( + histogram.diagnostics.get('html'))); + assert.deepEqual([id], histogram.sampleValues); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html new file mode 100644 index 00000000000..c55093a90ec --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html @@ -0,0 +1,557 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/timing.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/dropdown.html"> +<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_controls_export.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> + +<dom-module id="tr-v-ui-histogram-set-controls"> + <template> + <style> + :host { + display: block; + } + + #help, #feedback { + display: none; + margin-left: 20px; + } + + #search_container { + display: inline-flex; + margin-right: 20px; + padding-bottom: 1px; + border-bottom: 1px solid darkgrey; + } + + #search { + border: 0; + max-width: 20em; + outline: none; + } + + #clear_search { + visibility: hidden; + height: 1em; + stroke: black; + stroke-width: 16; + } + + #controls { + white-space: nowrap; + } + + #show_overview, #hide_overview { + height: 1em; + margin-right: 20px; + } + + #show_overview { + stroke: blue; + stroke-width: 16; + } + + #show_overview:hover { + background: blue; + stroke: white; + } + + #hide_overview { + display: none; + stroke-width: 18; + stroke: black; + } + + #hide_overview:hover { + background: black; + stroke: white; + } + + #reference_display_label { + display: none; + margin-right: 20px; + } + + #alpha, #alpha_slider_container { + display: none; + } + + #alpha { + margin-right: 20px; + } + + #alpha_slider_container { + background: white; + border: 1px solid black; + flex-direction: column; + padding: 0.5em; + position: absolute; + z-index: 10; /* scalar-span uses z-index :-( */ + } + + #alpha_slider { + -webkit-appearance: slider-vertical; + align-self: center; + height: 200px; + width: 30px; + } + + #statistic { + display: none; + margin-right: 20px; + } + + #show_visualization { + margin-right: 20px; + } + + #export { + margin-right: 20px; + } + </style> + + <div id="controls"> + <span id="search_container"> + <input id="search" value="{{searchQuery::keyup}}" placeholder="Find Histogram name"> + <svg viewbox="0 0 128 128" id="clear_search" on-tap="clearSearch_"> + <g> + <title>Clear search</title> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </g> + </svg> + </span> + + <svg viewbox="0 0 128 128" id="show_overview" + on-tap="toggleOverviewLineCharts_"> + <g> + <title>Show overview charts</title> + <line x1="19" y1="109" x2="49" y2="49"/> + <line x1="49" y1="49" x2="79" y2="79"/> + <line x1="79" y1="79" x2="109" y2="19"/> + </g> + </svg> + <svg viewbox="0 0 128 128" id="hide_overview" + on-tap="toggleOverviewLineCharts_"> + <g> + <title>Hide overview charts</title> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </g> + </svg> + + <select id="reference_display_label" value="{{referenceDisplayLabel::change}}"> + <option value="">Select a reference column</option> + </select> + + <button id="alpha" on-tap="openAlphaSlider_">α=[[alphaString]]</button> + <div id="alpha_slider_container"> + <input type="range" id="alpha_slider" value="{{alphaIndex::change}}" min="0" max="18" on-blur="closeAlphaSlider_" on-input="updateAlpha_"> + </div> + + <select id="statistic" value="{{displayStatisticName::change}}"> + </select> + + <button id="show_visualization" on-tap="loadVisualization_">Visualize</button> + + <tr-ui-b-dropdown label="Export"> + <tr-v-ui-histogram-set-controls-export> + </tr-v-ui-histogram-set-controls-export> + </tr-ui-b-dropdown> + + <input type="checkbox" id="show_all" checked="{{showAll::change}}" title="When unchecked, less important histograms are hidden."> + <label for="show_all" title="When unchecked, less important histograms are hidden.">Show all</label> + + <a id="help">Help</a> + <a id="feedback">Feedback</a> + </div> + + <tr-ui-b-grouping-table-groupby-picker id="picker"> + </tr-ui-b-grouping-table-groupby-picker> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const ALPHA_OPTIONS = []; + for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-3); + for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-2); + ALPHA_OPTIONS.push(0.1); + + Polymer({ + is: 'tr-v-ui-histogram-set-controls', + + properties: { + searchQuery: { + type: String, + value: '', + observer: 'onSearchQueryChange_', + }, + showAll: { + type: Boolean, + value: true, + observer: 'onUserChange_', + }, + referenceDisplayLabel: { + type: String, + value: '', + observer: 'onUserChange_', + }, + displayStatisticName: { + type: String, + value: '', + observer: 'onUserChange_', + }, + alphaString: { + type: String, + computed: 'getAlphaString_(alphaIndex)', + }, + alphaIndex: { + type: Number, + value: 9, + observer: 'onUserChange_', + }, + }, + + created() { + this.viewState_ = undefined; + this.rowListener_ = this.onRowViewStateUpdate_.bind(this); + this.baseStatisticNames_ = []; + + // When onViewStateUpdate_() copies multiple properties from the viewState + // to polymer properties, disable onUserChange_ until all properties are + // copied in order to prevent nested mutations to the ViewState. + this.isInOnViewStateUpdate_ = false; + this.searchQueryDebounceMs = 200; + }, + + ready() { + this.$.picker.addEventListener('current-groups-changed', + this.onGroupsChanged_.bind(this)); + }, + + get viewState() { + return this.viewState_; + }, + + set viewState(vs) { + if (this.viewState_) { + throw new Error('viewState must be set exactly once.'); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + // It would be arduous to construct a delta and call viewStateListener_ + // here in case vs contains non-default values, so callers must set + // viewState first and then update it. + }, + + async onSearchQueryChange_() { + // Bypass debouncing for testing purpose: + if (this.searchQueryDebounceMs === 0) return this.onUserChange_(); + // Limit the update rate for instance caused by typing in a search. + this.debounce('onSearchQueryDebounce', this.onUserChange_, + this.searchQueryDebounceMs); + }, + + async onUserChange_() { + if (!this.viewState) return; + if (this.isInOnViewStateUpdate_) return; + + const marks = []; + if (this.searchQuery !== this.viewState.searchQuery) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'search')); + } + if (this.showAll !== this.viewState.showAll) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'showAll')); + } + if (this.referenceDisplayLabel !== this.viewState.referenceDisplayLabel) { + marks.push(tr.b.Timing.mark( + 'histogram-set-controls', 'referenceColumn')); + } + if (this.displayStatisticName !== this.viewState.displayStatisticName) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'statistic')); + } + if (parseInt(this.alphaIndex) !== this.getAlphaIndexFromViewState_()) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'alpha')); + } + + this.$.clear_search.style.visibility = + this.searchQuery ? 'visible' : 'hidden'; + + let displayStatisticName = this.displayStatisticName; + if (this.viewState.referenceDisplayLabel === '' && + this.referenceDisplayLabel !== '' && + this.baseStatisticNames.length) { + // The user selected a reference display label. + displayStatisticName = `%${tr.v.DELTA}${this.displayStatisticName}`; + // Can't set this.displayStatisticName before updating viewState -- that + // would cause an infinite loop of onUserChange_(). + } + if (this.referenceDisplayLabel === '' && + this.viewState.referenceDisplayLabel !== '' && + this.baseStatisticNames.length) { + // The user unset the reference display label. + // Ensure that displayStatisticName is not a delta statistic. + const deltaIndex = displayStatisticName.indexOf(tr.v.DELTA); + if (deltaIndex >= 0) { + displayStatisticName = displayStatisticName.slice(deltaIndex + 1); + } else if (!this.baseStatisticNames.includes(displayStatisticName)) { + displayStatisticName = 'avg'; + } + } + + // Propagate updates from the user to the view state. + await this.viewState.update({ + searchQuery: this.searchQuery, + showAll: this.showAll, + referenceDisplayLabel: this.referenceDisplayLabel, + displayStatisticName, + alpha: ALPHA_OPTIONS[this.alphaIndex], + }); + + if (this.referenceDisplayLabel && + this.statisticNames.length === this.baseStatisticNames.length) { + // When a reference column is selected, delta statistics should be + // available. + this.statisticNames = this.baseStatisticNames.concat( + tr.v.Histogram.getDeltaStatisticsNames(this.baseStatisticNames)); + } else if (!this.referenceDisplayLabel && + this.statisticNames.length > this.baseStatisticNames.length) { + // When a reference column is not selected, delta statistics should not + // be available. + this.statisticNames = this.baseStatisticNames; + } + + for (const mark of marks) mark.end(); + }, + + onViewStateUpdate_(event) { + this.isInOnViewStateUpdate_ = true; + + if (event.delta.searchQuery) { + this.searchQuery = this.viewState.searchQuery; + } + + if (event.delta.showAll) this.showAll = this.viewState.showAll; + + if (event.delta.displayStatisticName) { + this.displayStatisticName = this.viewState.displayStatisticName; + } + + if (event.delta.referenceDisplayLabel) { + this.referenceDisplayLabel = this.viewState.referenceDisplayLabel; + this.$.alpha.style.display = this.referenceDisplayLabel ? 'inline' : ''; + } + + if (event.delta.groupings) { + this.$.picker.currentGroupKeys = this.viewState.groupings.map( + g => g.key); + } + + if (event.delta.tableRowStates) { + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + this.viewState.tableRowStates.values())) { + row.addUpdateListener(this.rowListener_); + } + + const anyShowing = this.anyOverviewCharts_; + this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none'; + this.$.show_overview.style.display = anyShowing ? 'none' : 'inline'; + } + + if (event.delta.alpha) { + this.alphaIndex = this.getAlphaIndexFromViewState_(); + } + + this.isInOnViewStateUpdate_ = false; + this.onUserChange_(); + }, + + onRowViewStateUpdate_(event) { + if (event.delta.isOverviewed) { + const anyShowing = event.delta.isOverviewed.current || + this.anyOverviewCharts_; + this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none'; + this.$.show_overview.style.display = anyShowing ? 'none' : 'inline'; + } + + if (event.delta.subRows) { + for (const subRow of event.delta.subRows.previous) { + subRow.removeUpdateListener(this.rowListener_); + } + for (const subRow of event.delta.subRows.current) { + subRow.addUpdateListener(this.rowListener_); + } + } + }, + + onGroupsChanged_() { + if (this.$.picker.currentGroups.length === 0 && + this.$.picker.possibleGroups.length > 0) { + // If the current groupings are now empty but there are possible + // groupings, then force there to be at least one grouping. + // The histogram-set-table requires there to be at least one grouping. + this.$.picker.currentGroupKeys = [this.$.picker.possibleGroups[0].key]; + } + this.viewState.groupings = this.$.picker.currentGroups; + }, + + set showAllEnabled(enable) { + if (!enable) this.$.show_all.checked = true; + this.$.show_all.disabled = !enable; + }, + + set possibleGroupings(groupings) { + this.$.picker.possibleGroups = groupings; + this.$.picker.style.display = (groupings.length < 2) ? 'none' : 'block'; + this.onGroupsChanged_(); + }, + + set displayLabels(labels) { + this.$.reference_display_label.style.display = + (labels.length < 2) ? 'none' : 'inline'; + + while (this.$.reference_display_label.children.length > 1) { + this.$.reference_display_label.removeChild( + this.$.reference_display_label.lastChild); + } + + for (const displayLabel of labels) { + const option = document.createElement('option'); + option.textContent = displayLabel; + option.value = displayLabel; + this.$.reference_display_label.appendChild(option); + } + + if (labels.includes(this.viewState.referenceDisplayLabel)) { + this.referenceDisplayLabel = this.viewState.referenceDisplayLabel; + } else { + this.viewState.referenceDisplayLabel = ''; + } + }, + + get baseStatisticNames() { + return this.baseStatisticNames_; + }, + + set baseStatisticNames(names) { + this.baseStatisticNames_ = names; + this.statisticNames = names; + }, + + get statisticNames() { + return Array.from(this.$.statistic.options).map(o => o.value); + }, + + set statisticNames(names) { + this.$.statistic.style.display = (names.length < 2) ? 'none' : 'inline'; + + while (this.$.statistic.children.length) { + this.$.statistic.removeChild(this.$.statistic.lastChild); + } + + for (const name of names) { + const option = document.createElement('option'); + option.textContent = name; + this.$.statistic.appendChild(option); + } + + if (names.includes(this.viewState.displayStatisticName)) { + this.displayStatisticName = this.viewState.displayStatisticName; + // Polymer doesn't reset the value when the options change, so do that + // manually. + this.$.statistic.value = this.displayStatisticName; + } else { + this.viewState.displayStatisticName = names[0] || ''; + } + }, + + get anyOverviewCharts_() { + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + this.viewState.tableRowStates.values())) { + if (row.isOverviewed) return true; + } + return false; + }, + + async toggleOverviewLineCharts_() { + const showOverviews = !this.anyOverviewCharts_; + const mark = tr.b.Timing.mark('histogram-set-controls', + (showOverviews ? 'show' : 'hide') + 'OverviewCharts'); + + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + this.viewState.tableRowStates.values())) { + await row.update({isOverviewed: showOverviews}); + } + + this.$.hide_overview.style.display = showOverviews ? 'inline' : 'none'; + this.$.show_overview.style.display = showOverviews ? 'none' : 'inline'; + + await tr.b.animationFrame(); + mark.end(); + }, + + set helpHref(href) { + this.$.help.href = href; + this.$.help.style.display = 'inline'; + }, + + set feedbackHref(href) { + this.$.feedback.href = href; + this.$.feedback.style.display = 'inline'; + }, + + clearSearch_() { + this.set('searchQuery', ''); + this.$.search.focus(); + }, + + getAlphaString_(alphaIndex) { + // (9 * 1e-3).toString() is "0.009000000000000001", so truncate. + return ('' + ALPHA_OPTIONS[alphaIndex]).substr(0, 5); + }, + + openAlphaSlider_() { + const alphaButtonRect = this.$.alpha.getBoundingClientRect(); + this.$.alpha_slider_container.style.display = 'flex'; + this.$.alpha_slider_container.style.top = alphaButtonRect.bottom + 'px'; + this.$.alpha_slider_container.style.left = alphaButtonRect.left + 'px'; + this.$.alpha_slider.focus(); + }, + + closeAlphaSlider_() { + this.$.alpha_slider_container.style.display = ''; + }, + + updateAlpha_() { + this.alphaIndex = this.$.alpha_slider.value; + }, + + getAlphaIndexFromViewState_() { + for (let i = 0; i < ALPHA_OPTIONS.length; ++i) { + if (ALPHA_OPTIONS[i] >= this.viewState.alpha) return i; + } + return ALPHA_OPTIONS.length - 1; + }, + + set enableVisualization(enable) { + this.$.show_visualization.style.display = enable ? 'inline' : 'none'; + }, + + loadVisualization_() { + tr.b.dispatchSimpleEvent(this, 'loadVisualization', true, true, {}); + }, + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html new file mode 100644 index 00000000000..98936f24bba --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/timing.html"> + +<dom-module id="tr-v-ui-histogram-set-controls-export"> + <template> + <style> + :host { + display: grid; + grid-gap: 1em; + grid-template-rows: auto auto; + grid-template-columns: auto auto; + } + button { + -webkit-appearance: none; + border: 0; + font-size: initial; + padding: 5px; + } + </style> + + <button on-tap="exportRawCsv_">raw CSV</button> + <button on-tap="exportRawJson_">raw JSON</button> + <button on-tap="exportMergedCsv_">merged CSV</button> + <button on-tap="exportMergedJson_">merged JSON</button> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-histogram-set-controls-export', + + exportRawCsv_() { + this.export_(false, 'csv'); + }, + + exportRawJson_() { + this.export_(false, 'json'); + }, + + exportMergedCsv_() { + this.export_(true, 'csv'); + }, + + exportMergedJson_() { + this.export_(true, 'json'); + }, + + export_(merged, format) { + tr.b.dispatchSimpleEvent(this, 'export', true, true, {merged, format}); + }, + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html new file mode 100644 index 00000000000..9783059ccbe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html @@ -0,0 +1,300 @@ +<!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/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/histogram_grouping.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_controls.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + function buildControls(test) { + const controls = document.createElement('tr-v-ui-histogram-set-controls'); + controls.viewState = new tr.v.ui.HistogramSetViewState(); + test.addHTMLOutput(controls); + return controls; + } + + test('helpHref', function() { + const controls = buildControls(this); + controls.helpHref = 'data:text/html,hello'; + const help = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'help'); + assert.strictEqual(help.style.display, 'inline'); + assert.strictEqual(help.href, 'data:text/html,hello'); + }); + + test('feedbackHref', function() { + const controls = buildControls(this); + controls.feedbackHref = 'data:text/html,hello'; + const feedback = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'feedback'); + assert.strictEqual(feedback.style.display, 'inline'); + assert.strictEqual(feedback.href, 'data:text/html,hello'); + }); + + test('displayLabels', function() { + const controls = buildControls(this); + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'reference_display_label'); + assert.strictEqual('none', getComputedStyle(selector).display); + + controls.displayLabels = []; + assert.strictEqual('none', getComputedStyle(selector).display); + + controls.displayLabels = ['Value']; + assert.strictEqual('none', getComputedStyle(selector).display); + + controls.displayLabels = ['a', 'b\nc']; + assert.strictEqual('inline-block', getComputedStyle(selector).display); + assert.strictEqual('', selector.children[0].value); + assert.strictEqual('a', selector.children[1].value); + assert.strictEqual('a', selector.children[1].textContent); + + // displayLabels can contain newlines, which <option> replace with spaces. + // histogram-set-controls must set option.value in order for selector.value + // to contain the newlines. + assert.strictEqual('b\nc', selector.children[2].value); + assert.strictEqual('b\nc', selector.children[2].textContent); + selector.selectedIndex = 2; + assert.strictEqual('b\nc', selector.value); + + controls.displayLabels = ['Value']; + assert.strictEqual('none', getComputedStyle(selector).display); + }); + + test('baseStatisticNames', function() { + const controls = buildControls(this); + controls.baseStatisticNames = ['avg', 'std']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'statistic'); + assert.strictEqual('inline-block', getComputedStyle(selector).display); + assert.lengthOf(selector.children, 2); + assert.strictEqual('avg', selector.children[0].value); + assert.strictEqual('avg', selector.children[0].textContent); + assert.strictEqual('std', selector.children[1].value); + assert.strictEqual('std', selector.children[1].textContent); + assert.strictEqual('avg', selector.value); + }); + + test('viewDisplayStatisticName', function() { + const controls = buildControls(this); + controls.baseStatisticNames = ['avg', 'std']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'statistic'); + controls.viewState.displayStatisticName = 'std'; + assert.strictEqual('std', selector.value); + }); + + test('controlDisplayStatisticName', function() { + const controls = buildControls(this); + controls.baseStatisticNames = ['avg', 'std']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'statistic'); + selector.value = 'std'; + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', false, true); + selector.dispatchEvent(changeEvent); + assert.strictEqual('std', controls.viewState.displayStatisticName); + }); + + test('viewSearchQuery', function() { + const controls = buildControls(this); + controls.viewState.searchQuery = 'foo'; + const search = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'search'); + assert.strictEqual(search.value, 'foo'); + }); + + test('controlSearchQuery', function() { + const controls = buildControls(this); + controls.searchQueryDebounceMs = 0; + const search = tr.ui.b.findDeepElementMatching(controls, '#search'); + search.value = 'x'; + const keyupEvent = document.createEvent('KeyboardEvent'); + keyupEvent.initEvent('keyup'); + search.dispatchEvent(keyupEvent); + assert.strictEqual(controls.viewState.searchQuery, 'x'); + controls.clearSearch_(); + assert.strictEqual(controls.viewState.searchQuery, ''); + }); + + test('viewShowAll', function() { + const controls = buildControls(this); + const showAll = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'show_all'); + assert.strictEqual(controls.viewState.showAll, true); + assert.strictEqual(showAll.checked, true); + controls.viewState.showAll = false; + assert.strictEqual(showAll.checked, false); + }); + + test('controlShowAll', function() { + const controls = buildControls(this); + const showAll = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'show_all'); + assert.strictEqual(controls.viewState.showAll, true); + assert.strictEqual(showAll.checked, true); + showAll.click(); + assert.strictEqual(showAll.checked, false); + assert.strictEqual(controls.viewState.showAll, false); + const showAllLabel = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.tagName === 'LABEL' && e.htmlFor === 'show_all'); + showAllLabel.click(); + assert.strictEqual(showAll.checked, true); + assert.strictEqual(controls.viewState.showAll, true); + }); + + test('viewReferenceDisplayLabel', function() { + const controls = buildControls(this); + controls.displayLabels = ['a', 'b']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'reference_display_label'); + + assert.strictEqual('', selector.value); + assert.strictEqual('', controls.viewState.referenceDisplayLabel); + + controls.viewState.referenceDisplayLabel = 'a'; + assert.strictEqual('a', selector.value); + + controls.viewState.referenceDisplayLabel = 'b'; + assert.strictEqual('b', selector.value); + + controls.viewState.referenceDisplayLabel = ''; + assert.strictEqual('', selector.value); + }); + + test('controlReferenceDisplayLabel', function() { + const controls = buildControls(this); + controls.displayLabels = ['a', 'b']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'reference_display_label'); + assert.strictEqual('', selector.value); + assert.strictEqual('', controls.viewState.referenceDisplayLabel); + + selector.value = 'a'; + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', false, true); + selector.dispatchEvent(changeEvent); + assert.strictEqual('a', controls.viewState.referenceDisplayLabel); + + selector.value = 'b'; + selector.dispatchEvent(changeEvent); + assert.strictEqual('b', controls.viewState.referenceDisplayLabel); + + selector.value = ''; + selector.dispatchEvent(changeEvent); + assert.strictEqual('', controls.viewState.referenceDisplayLabel); + }); + + test('viewGroupings', function() { + const controls = buildControls(this); + const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo'); + const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values()); + groupings.push(fooGrouping); + controls.possibleGroupings = groupings; + const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER'); + assert.lengthOf(picker.currentGroupKeys, 1); + assert.strictEqual(picker.currentGroupKeys[0], + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + + controls.viewState.groupings = [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + ]; + assert.lengthOf(picker.currentGroupKeys, 1); + assert.strictEqual(picker.currentGroupKeys[0], + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + assert.strictEqual('block', picker.style.display); + + controls.viewState.groupings = [ + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + fooGrouping, + ]; + assert.lengthOf(picker.currentGroupKeys, 2); + assert.strictEqual(picker.currentGroupKeys[0], + tr.v.d.RESERVED_NAMES.STORIES); + assert.strictEqual(picker.currentGroupKeys[1], 'foo'); + }); + + test('controlGroupings', function() { + const controls = buildControls(this); + const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo'); + const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values()); + groupings.push(fooGrouping); + controls.possibleGroupings = groupings; + const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER'); + assert.lengthOf(picker.currentGroupKeys, 1); + assert.strictEqual(controls.viewState.groupings[0].key, + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + + picker.currentGroupKeys = ['name']; + assert.lengthOf(controls.viewState.groupings, 1); + assert.strictEqual(controls.viewState.groupings[0].key, + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + + picker.currentGroupKeys = [tr.v.d.RESERVED_NAMES.STORIES, 'foo']; + assert.lengthOf(controls.viewState.groupings, 2); + assert.strictEqual(controls.viewState.groupings[0], + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES)); + assert.strictEqual(controls.viewState.groupings[1], + fooGrouping); + }); + + test('viewIsOverviewed', function() { + const controls = buildControls(this); + const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'show_overview'); + const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'hide_overview'); + controls.viewState.tableRowStates = new Map([ + ['a', new tr.v.ui.HistogramSetTableRowState()], + ['b', new tr.v.ui.HistogramSetTableRowState()], + ]); + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + + controls.viewState.tableRowStates.get('a').isOverviewed = true; + assert.strictEqual('none', showOverview.style.display); + assert.strictEqual('inline', hideOverview.style.display); + + controls.viewState.tableRowStates.get('a').isOverviewed = false; + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + }); + + test('controlIsOverviewed', async function() { + const controls = buildControls(this); + const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'show_overview'); + const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'hide_overview'); + controls.viewState.tableRowStates = new Map([ + ['a', new tr.v.ui.HistogramSetTableRowState()], + ['b', new tr.v.ui.HistogramSetTableRowState()], + ]); + assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed); + assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed); + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + + await controls.toggleOverviewLineCharts_(); + assert.strictEqual('none', showOverview.style.display); + assert.strictEqual('inline', hideOverview.style.display); + assert.isTrue(controls.viewState.tableRowStates.get('a').isOverviewed); + assert.isTrue(controls.viewState.tableRowStates.get('b').isOverviewed); + + await controls.toggleOverviewLineCharts_(); + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed); + assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html new file mode 100644 index 00000000000..882806ba32b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html @@ -0,0 +1,251 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/math.html"> +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/base/url_json.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + // This number is used to decide whether the tableStates can fit in the URL, + // and omit them if not. + // There is no specification for maximum URL length, so it is typically + // limited by hosts, not browsers. + // TODO(#3816) Tune this number. + const MAX_URL_LENGTH = 2048; + + // This class wraps |window.location| and |window.history| to allow tests to + // mock it. + class Locus { + get origin() { + return window.location.origin; + } + + get pathname() { + return window.location.pathname; + } + + get search() { + return window.location.search; + } + + get hash() { + return window.location.hash; + } + + get state() { + if (this.stateMode === '#') return this.hash.substr(1); + return this.search.substr(1); + } + + get stateMode() { + if (this.hash) return '#'; + return '?'; + } + + buildUrlFromState(state) { + let url = this.origin + this.pathname; + if (this.stateMode === '#') url += this.search; + url += this.stateMode + state; + return url; + } + + pushState(state) { + if (state === this.state) return; + + // TODO(#3837) When should this actually call pushState()? + window.history.replaceState(null, null, this.buildUrlFromState(state)); + } + + addPopStateListener(listener) { + window.addEventListener('popstate', listener); + } + } + + class HistogramSetLocation { + constructor(opt_location) { + // Optional dependency injection for testing. + this.location_ = opt_location || new Locus(); + this.location_.addPopStateListener(this.onPopState_.bind(this)); + + this.viewState_ = undefined; + this.rowListener_ = this.onRowStateUpdate_.bind(this); + this.cellListener_ = this.onCellStateUpdate_.bind(this); + + // pushState_ is disabled while handling onPopState_. + this.poppingState_ = false; + } + + /** + * @return {!tr.v.ui.HistogramSetViewState} + */ + get viewState() { + return this.viewState_; + } + + /** + * @param {!tr.v.ui.HistogramSetViewState} vs + */ + async build(vs) { + if (this.viewState !== undefined) { + throw new Error('viewState must be set exactly once.'); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + + await this.onPopState_(); + } + + onViewStateUpdate_(event) { + if (event.delta.tableRowStates) { + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + event.delta.tableRowStates.previous.values())) { + row.removeUpdateListener(this.rowListener_); + for (const cell of row.cells.values()) { + cell.removeUpdateListener(this.cellListener_); + } + } + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + event.delta.tableRowStates.current.values())) { + row.addUpdateListener(this.rowListener_); + for (const cell of row.cells.values()) { + cell.addUpdateListener(this.cellListener_); + } + } + } + + this.pushState_(); + } + + onRowStateUpdate_(event) { + // This assumes that subRows and cells are not updated. + this.pushState_(); + } + + onCellStateUpdate_(event) { + this.pushState_(); + } + + pushState_() { + if (this.poppingState_) return; + const mark = tr.b.Timing.mark('HistogramSetLocation', 'pushState'); + + const params = new Map(); + if (this.viewState.searchQuery) { + params.set('q', this.viewState.searchQuery); + } + if (this.viewState.referenceDisplayLabel) { + params.set('r', this.viewState.referenceDisplayLabel); + } + params.set('s', this.viewState.displayStatisticName); + if (!this.viewState.showAll) params.set('m', ''); + params.set('g', this.viewState.groupings.map(g => g.key).join('.')); + if (this.viewState.sortColumnIndex !== undefined) { + params.set('c', '' + this.viewState.sortColumnIndex); + } + if (this.viewState.sortDescending) params.set('d', ''); + if (!this.viewState.constrainNameColumn) params.set('n', '0'); + if (!tr.b.math.approximately(this.viewState.alpha, 0.01)) { + params.set('p', ('' + this.viewState.alpha).substr(0, 5)); + } + + let urlState = ''; + for (const [key, value] of params) { + if (urlState) urlState += '&'; + urlState += key + '=' + window.encodeURIComponent(value); + } + + const rowDicts = {}; + for (const [name, rowState] of this.viewState.tableRowStates) { + const dict = rowState.asCompactDict(); + if (dict === undefined) continue; + rowDicts[name] = dict; + } + + if (Object.keys(rowDicts).length > 0) { + const rowsParam = '&t=' + tr.b.UrlJson.stringify(rowDicts); + + if (this.location_.buildUrlFromState(urlState + rowsParam).length < + MAX_URL_LENGTH) { + urlState += rowsParam; + } + } + + this.location_.pushState(urlState); + mark.end(); + } + + async onPopState_() { + const mark = tr.b.Timing.mark('HistogramSetLocation', 'onPopState'); + this.poppingState_ = true; + + const params = new Map(); + for (const kvp of this.location_.state.split('&')) { + const [key, value] = kvp.split('='); + try { + params.set(key, window.decodeURIComponent(value)); + } catch (e) { + // If the user tampers with the params so that a value cannot be + // decoded, ignore it. + } + } + + const delta = new Map(); + if (params.has('q')) delta.set('searchQuery', params.get('q')); + if (params.has('r')) delta.set('referenceDisplayLabel', params.get('r')); + if (params.has('s')) delta.set('displayStatisticName', params.get('s')); + delta.set('showAll', !params.has('m')); + if (params.has('g')) { + delta.set('groupings', params.get('g').split('.').map( + k => tr.v.HistogramGrouping.BY_KEY.get(k))); + } + if (params.has('c')) { + delta.set('sortColumnIndex', parseInt(params.get('c'))); + } else { + delta.set('sortColumnIndex', 0); + } + delta.set('sortDescending', params.has('d')); + delta.set('constrainNameColumn', params.get('n') !== '0'); + if (params.has('p')) { + delta.set('alpha', parseFloat(params.get('p'))); + } + + await this.viewState.update(delta); + + if (params.has('t')) { + let rowDicts; + try { + rowDicts = tr.b.UrlJson.parse(params.get('t')); + } catch (e) { + // If the user tampers with the params so that rowDicts cannot be + // parsed, ignore it. + } + + if (rowDicts) { + for (const [name, rowDict] of Object.entries(rowDicts)) { + const rowState = this.viewState.tableRowStates.get(name); + if (rowState === undefined) continue; + await rowState.updateFromCompactDict(rowDict); + } + } + } + + this.poppingState_ = false; + mark.end(); + } + } + + HistogramSetLocation.Locus = Locus; + + return { + HistogramSetLocation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html new file mode 100644 index 00000000000..d9987e7b097 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html @@ -0,0 +1,290 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/value/ui/histogram_set_location.html"> + +<script> +'use strict'; +/* eslint-disable max-len */ +tr.b.unittest.testSuite(function() { + class TestLocus extends tr.v.ui.HistogramSetLocation.Locus { + constructor() { + super(); + this.search_ = ''; + this.hash_ = ''; + this.listener_ = undefined; + } + + get origin() { + return 'http://example.com'; + } + + get pathname() { + return '/pathname'; + } + + get search() { + return this.search_; + } + + set search(s) { + this.search_ = s; + } + + get hash() { + return this.hash_; + } + + set hash(h) { + this.hash_ = h; + } + + pushState(state) { + if (this.hash) { + this.hash = '#' + state; + } else { + this.search = '?' + state; + } + } + + get stateMode() { + if (this.hash) return '#'; + return '?'; + } + + addPopStateListener(listener) { + this.listener_ = listener; + } + + async popState(state) { + if (state[0] === '?') { + this.search = state; + } else if (state[0] === '#') { + this.hash = state; + } + await this.listener_(); + } + } + + test('viewStateUpdateHashAndSearch', async function() { + const locus = new TestLocus(); + locus.search = '?'; + locus.hash = '#'; + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await hsl.viewState.update({ + displayStatisticName: 'avg', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: undefined, + }); + assert.strictEqual(locus.hash, '#s=avg&g=name.stories'); + }); + + test('viewStateUpdateHash', async function() { + const locus = new TestLocus(); + locus.hash = '#'; + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await hsl.viewState.update({ + displayStatisticName: 'avg', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: undefined, + }); + assert.strictEqual(locus.hash, '#s=avg&g=name.stories'); + + await hsl.viewState.update({searchQuery: 'foo'}); + assert.strictEqual(locus.hash, '#q=foo&s=avg&g=name.stories'); + + await hsl.viewState.update({referenceDisplayLabel: 'bar'}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&g=name.stories'); + + await hsl.viewState.update({showAll: false}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories'); + + await hsl.viewState.update({sortColumnIndex: 2}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2'); + + await hsl.viewState.update({sortDescending: true}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d='); + + await hsl.viewState.update({constrainNameColumn: false}); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + const rowState = new tr.v.ui.HistogramSetTableRowState(); + rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState()); + await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])}); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true}); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)'); + + await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({ + isOpen: true, + }); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))'); + }); + + test('viewStateUpdateSearch', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await hsl.viewState.update({ + displayStatisticName: 'avg', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: undefined, + }); + assert.strictEqual(locus.search, '?s=avg&g=name.stories'); + + await hsl.viewState.update({searchQuery: 'foo'}); + assert.strictEqual(locus.search, '?q=foo&s=avg&g=name.stories'); + + await hsl.viewState.update({referenceDisplayLabel: 'bar'}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&g=name.stories'); + + await hsl.viewState.update({showAll: false}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories'); + + await hsl.viewState.update({sortColumnIndex: 2}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2'); + + await hsl.viewState.update({sortDescending: true}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d='); + + await hsl.viewState.update({constrainNameColumn: false}); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + const rowState = new tr.v.ui.HistogramSetTableRowState(); + rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState()); + await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])}); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true}); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)'); + + await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({ + isOpen: true, + }); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))'); + }); + + test('popStateSearch', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await locus.popState('?q=foo&r=bar&s=qux&m=&c=2&d=&g=name.stories'); + assert.strictEqual('foo', hsl.viewState.searchQuery); + assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel); + assert.strictEqual('qux', hsl.viewState.displayStatisticName); + assert.isFalse(hsl.viewState.showAll); + assert.lengthOf(hsl.viewState.groupings, 2); + assert.strictEqual('name', hsl.viewState.groupings[0].key); + assert.strictEqual('stories', hsl.viewState.groupings[1].key); + assert.strictEqual(2, hsl.viewState.sortColumnIndex); + assert.isTrue(hsl.viewState.sortDescending); + + // onPopState_ should ignore missing rows and cells + await locus.popState('?t=f%3Am_p-(o-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.size); + + await hsl.viewState.update({tableRowStates: new Map([ + ['f:m_p', new tr.v.ui.HistogramSetTableRowState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await locus.popState('?t=f%3Am_p-(e-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([ + ['Value', new tr.v.ui.HistogramSetTableCellState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + + await locus.popState('?t=f%3Am_p-(c-(Value-(o-1)))'); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + }); + + test('popStateHashAndSearch', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await locus.popState('?q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories'); + assert.strictEqual('foo', hsl.viewState.searchQuery); + assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel); + assert.strictEqual('qux', hsl.viewState.displayStatisticName); + assert.isTrue(hsl.viewState.showAll); + assert.lengthOf(hsl.viewState.groupings, 2); + assert.strictEqual('name', hsl.viewState.groupings[0].key); + assert.strictEqual('stories', hsl.viewState.groupings[1].key); + assert.strictEqual(2, hsl.viewState.sortColumnIndex); + assert.isTrue(hsl.viewState.sortDescending); + + await locus.popState('#q=q'); + assert.strictEqual('q', hsl.viewState.searchQuery); + }); + + test('popStateHash', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await locus.popState('#q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories'); + assert.strictEqual('foo', hsl.viewState.searchQuery); + assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel); + assert.strictEqual('qux', hsl.viewState.displayStatisticName); + assert.isTrue(hsl.viewState.showAll); + assert.lengthOf(hsl.viewState.groupings, 2); + assert.strictEqual('name', hsl.viewState.groupings[0].key); + assert.strictEqual('stories', hsl.viewState.groupings[1].key); + assert.strictEqual(2, hsl.viewState.sortColumnIndex); + assert.isTrue(hsl.viewState.sortDescending); + + // onPopState_ should ignore missing rows and cells + await locus.popState('#t=f%3Am_p-(o-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.size); + + await hsl.viewState.update({tableRowStates: new Map([ + ['f:m_p', new tr.v.ui.HistogramSetTableRowState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await locus.popState('#t=f%3Am_p-(e-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([ + ['Value', new tr.v.ui.HistogramSetTableCellState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + + await locus.popState('#t=f%3Am_p-(c-(Value-(o-1)))'); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html new file mode 100644 index 00000000000..9ac3046c5d3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html @@ -0,0 +1,459 @@ +<!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/timing.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/histogram_set_hierarchy.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_table_row.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> + +<dom-module id="tr-v-ui-histogram-set-table"> + <template> + <style> + :host { + min-height: 0px; + overflow: auto; + } + #table { + margin-top: 5px; + } + </style> + + <tr-ui-b-table id="table"/> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const MIDLINE_HORIZONTAL_ELLIPSIS = String.fromCharCode(0x22ef); + + // http://stackoverflow.com/questions/3446170 + function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + } + + Polymer({ + is: 'tr-v-ui-histogram-set-table', + + created() { + this.viewState_ = undefined; + this.progress_ = () => Promise.resolve(); + this.nameColumnTitle_ = undefined; + this.displayLabels_ = []; + this.histograms_ = undefined; + this.sourceHistograms_ = undefined; + this.filteredHistograms_ = undefined; + this.groupedHistograms_ = undefined; + this.hierarchies_ = undefined; + this.tableRows_ = undefined; + + // Store this listener so it can be removed while updateContents_ modifies + // sortColumnIndex and sortDescending, then re-added. + this.sortColumnChangedListener_ = e => this.onSortColumnChanged_(e); + }, + + ready() { + this.$.table.zebra = true; + this.addEventListener('sort-column-changed', + this.sortColumnChangedListener_); + this.addEventListener('requestSelectionChange', + this.onRequestSelectionChange_.bind(this)); + this.addEventListener('row-expanded-changed', + this.onRowExpandedChanged_.bind(this)); + }, + + get viewState() { + return this.viewState_; + }, + + set viewState(vs) { + if (this.viewState_) { + throw new Error('viewState must be set exactly once.'); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + // It would be arduous to construct a delta and call onViewStateUpdate_ + // here in case vs contains non-default values, so callers must set + // viewState first and then update it. + }, + + get histograms() { + return this.histograms_; + }, + + /** + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.v.HistogramSet} sourceHistograms + * @param {!Array.<string>} displayLabels + * @param {function(string, function())=} opt_progress + */ + async build(histograms, sourceHistograms, displayLabels, opt_progress) { + this.histograms_ = histograms; + this.sourceHistograms_ = sourceHistograms; + this.filteredHistograms_ = undefined; + this.groupedHistograms_ = undefined; + this.displayLabels_ = displayLabels; + + if (opt_progress !== undefined) this.progress_ = opt_progress; + + if (histograms.length === 0) { + throw new Error('histogram-set-table requires non-empty HistogramSet.'); + } + + await this.progress_('Building columns...'); + this.$.table.tableColumns = [ + { + title: this.buildNameColumnTitle_(), + value: row => row.nameCell, + cmp: (a, b) => a.compareNames(b), + } + ].concat(displayLabels.map(l => this.buildColumn_(l))); + + tr.b.Timing.instant('histogram-set-table', 'columnCount', + this.$.table.tableColumns.length); + + // updateContents_() displays its own progress. + await this.updateContents_(); + + // Building some elements requires being able to measure them, which is + // impossible until they are displayed. If clients hide this table while + // it is being built, then they must display it when this event fires. + this.fire('display-ready'); + + this.progress_ = () => Promise.resolve(); + + this.checkNameColumnOverflow_( + tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows)); + }, + + buildNameColumnTitle_() { + this.nameColumnTitle_ = document.createElement('span'); + this.nameColumnTitle_.style.display = 'inline-flex'; + + // Wrap the string in a span instead of using createTextNode() so that the + // span can be styled later. + const nameEl = document.createElement('span'); + nameEl.textContent = 'Name'; + this.nameColumnTitle_.appendChild(nameEl); + + const toggleWidthEl = document.createElement('span'); + toggleWidthEl.style.fontWeight = 'bold'; + toggleWidthEl.style.background = '#bbb'; + toggleWidthEl.style.color = '#333'; + toggleWidthEl.style.padding = '0px 3px'; + toggleWidthEl.style.marginRight = '8px'; + toggleWidthEl.style.display = 'none'; + toggleWidthEl.textContent = MIDLINE_HORIZONTAL_ELLIPSIS; + toggleWidthEl.addEventListener('click', + this.toggleNameColumnWidth_.bind(this)); + this.nameColumnTitle_.appendChild(toggleWidthEl); + return this.nameColumnTitle_; + }, + + toggleNameColumnWidth_(opt_event) { + this.viewState.update({ + constrainNameColumn: !this.viewState.constrainNameColumn, + }); + + if (opt_event !== undefined) { + opt_event.stopPropagation(); + opt_event.preventDefault(); + tr.b.Timing.instant('histogram-set-table', 'nameColumn' + + (this.viewState.constrainNameColumn ? 'Constrained' : + 'Unconstrained')); + } + }, + + buildColumn_(displayLabel) { + const title = document.createElement('span'); + title.textContent = displayLabel; + title.style.whiteSpace = 'pre'; + + return { + displayLabel, + title, + value: row => row.getCell(displayLabel), + cmp: (rowA, rowB) => rowA.compareCells(rowB, displayLabel), + }; + }, + + async updateContents_() { + const previousRowStates = this.viewState.tableRowStates; + + if (!this.filteredHistograms_) { + await this.progress_('Filtering rows...'); + this.filteredHistograms_ = this.viewState.showAll ? + this.histograms : this.sourceHistograms_; + + if (this.viewState.searchQuery) { + let query; + try { + query = new RegExp(this.viewState.searchQuery); + } catch (e) { + } + if (query !== undefined) { + this.filteredHistograms_ = new tr.v.HistogramSet( + [...this.filteredHistograms_].filter( + hist => hist.name.match(query))); + if (this.filteredHistograms_.length === 0 && + !this.viewState.showAll) { + await this.viewState.update({showAll: true}); + return; + } + } + } + this.groupedHistograms_ = undefined; + } + + if (!this.groupedHistograms_) { + await this.progress_('Grouping Histograms...'); + this.groupHistograms_(); + } + + if (!this.hierarchies_) { + await this.progress_('Merging Histograms...'); + this.hierarchies_ = tr.v.HistogramSetHierarchy.build( + this.groupedHistograms_); + this.tableRows_ = undefined; + } + + const tableRowsDirty = this.tableRows_ === undefined; + if (tableRowsDirty) { + // Wait to set this.$.table.tableRows until we're ready for it to build + // DOM. When tableRows are set on it, tr-ui-b-table calls + // setTimeout(..., 0) to schedule rebuild for the next interpreter tick, + // but that can happen in between the next await, which is too early. + this.tableRows_ = this.hierarchies_.map(hierarchy => + new tr.v.ui.HistogramSetTableRow( + hierarchy, this.$.table, this.viewState)); + + tr.b.Timing.instant('histogram-set-table', 'rootRowCount', + this.tableRows_.length); + + const namesToRowStates = new Map(); + for (const row of this.tableRows_) { + namesToRowStates.set(row.name, row.viewState); + } + await this.viewState.update({tableRowStates: namesToRowStates}); + } + + await this.progress_('Configuring table...'); + this.nameColumnTitle_.children[1].style.filter = + this.viewState.constrainNameColumn ? 'invert(100%)' : ''; + + const referenceDisplayLabelIndex = this.displayLabels_.indexOf( + this.viewState.referenceDisplayLabel); + this.$.table.selectedTableColumnIndex = (referenceDisplayLabelIndex < 0) ? + undefined : (1 + referenceDisplayLabelIndex); + + // Temporarily stop listening for this event in order to prevent the + // listener from updating viewState unnecessarily. + this.removeEventListener('sort-column-changed', + this.sortColumnChangedListener_); + this.$.table.sortColumnIndex = this.viewState.sortColumnIndex; + this.$.table.sortDescending = this.viewState.sortDescending; + this.addEventListener('sort-column-changed', + this.sortColumnChangedListener_); + + // Each name-cell listens to this.viewState for updates to + // constrainNameColumn. + // Each table-cell listens to this.viewState for updates to + // displayStatisticName and referenceDisplayLabel. + + if (tableRowsDirty) { + await this.progress_('Building DOM...'); + this.$.table.tableRows = this.tableRows_; + + // Try to restore previous row state. + // Wait to do this until after the base table has the new rows so that + // setExpandedForTableRow doesn't get confused. + for (const row of this.tableRows_) { + const previousState = previousRowStates.get(row.name); + if (!previousState) continue; + await row.restoreState(previousState); + } + } + + // It's always safe to call this, it will only recompute what is dirty. + // We want to make sure that the table is up to date when this async + // function resolves. + this.$.table.rebuild(); + }, + + async onRowExpandedChanged_(event) { + event.row.viewState.isExpanded = + this.$.table.getExpandedForTableRow(event.row); + tr.b.Timing.instant('histogram-set-table', + 'row' + (event.row.viewState.isExpanded ? 'Expanded' : 'Collapsed')); + + // When the user expands a row, the table builds subRows' name-cells. + // If a subRow's name isOverflowing even though none of the top-level rows + // are constrained, show the dots to allow the user to unconstrain the + // name column. + // Each name-cell.isOverflowing would force layout if we don't await + // animationFrame here, which would be inefficient. + if (this.nameColumnTitle_.children[1].style.display === 'block') return; + await tr.b.animationFrame(); + this.checkNameColumnOverflow_(event.row.subRows); + }, + + checkNameColumnOverflow_(rows) { + for (const row of rows) { + if (!row.nameCell.isOverflowing) continue; + + const [nameSpan, dots] = this.nameColumnTitle_.children; + dots.style.display = 'block'; + + // Size the span containing 'Name' so that the dots align with the + // ellipses in the name-cells. + const labelWidthPx = tr.v.ui.NAME_COLUMN_WIDTH_PX - + dots.getBoundingClientRect().width; + nameSpan.style.width = labelWidthPx + 'px'; + + return; + } + }, + + groupHistograms_() { + const groupings = this.viewState.groupings.slice(); + groupings.push(tr.v.HistogramGrouping.DISPLAY_LABEL); + + function canSkipGrouping(grouping, groupedHistograms) { + // Never skip meaningful groupings. + if (groupedHistograms.size > 1) return false; + + // Never skip the zero-th grouping. + if (grouping.key === groupings[0].key) return false; + + // Never skip the grouping that defines the table columns. + if (grouping.key === tr.v.HistogramGrouping.DISPLAY_LABEL.key) { + return false; + } + + // Skip meaningless groupings. + return true; + } + + this.groupedHistograms_ = + this.filteredHistograms_.groupHistogramsRecursively( + groupings, canSkipGrouping); + + this.hierarchies_ = undefined; + }, + + /** + * @param {!tr.b.Event} event + * @param {!Object} event.delta + * @param {!Object} event.delta.searchQuery + * @param {!Object} event.delta.referenceDisplayLabel + * @param {!Object} event.delta.displayStatisticName + * @param {!Object} event.delta.showAll + * @param {!Object} event.delta.groupings + * @param {!Object} event.delta.sortColumnIndex + * @param {!Object} event.delta.sortDescending + * @param {!Object} event.delta.constrainNameColumn + * @param {!Object} event.delta.tableRowStates + */ + async onViewStateUpdate_(event) { + if (this.histograms_ === undefined) return; + + if (event.delta.searchQuery !== undefined || + event.delta.showAll !== undefined) { + this.filteredHistograms_ = undefined; + } + + if (event.delta.groupings !== undefined) { + this.groupedHistograms_ = undefined; + } + + if (event.delta.displayStatistic !== undefined && + this.$.table.sortColumnIndex > 0) { + // Force re-sort. + this.$.table.sortColumnIndex = undefined; + } + + if (event.delta.referenceDisplayLabel !== undefined || + event.delta.displayStatisticName !== undefined) { + // Force this.$.table.bodyDirty_ = true; + this.$.table.tableRows = this.$.table.tableRows; + } + + // updateContents_() always copies sortColumnIndex and sortDescending + // from the viewState to the table. The table will only re-sort if + // they change. + + // Name-cells listen to this.viewState to handle updates to + // constrainNameColumn. + + if (event.delta.tableRowStates) { + if (this.tableRows_.length !== + this.viewState.tableRowStates.size) { + throw new Error( + 'Only histogram-set-table may update tableRowStates'); + } + for (const row of this.tableRows_) { + if (this.viewState.tableRowStates.get(row.name) !== row.viewState) { + throw new Error( + 'Only histogram-set-table may update tableRowStates'); + } + } + return; // No need to re-enter updateContents_(). + } + + await this.updateContents_(); + }, + + onSortColumnChanged_(event) { + tr.b.Timing.instant('histogram-set-table', 'sortColumn'); + this.viewState.update({ + sortColumnIndex: event.sortColumnIndex, + sortDescending: event.sortDescending, + }); + }, + + onRequestSelectionChange_(event) { + // This event may reference an EventSet or an array of Histogram names. + // If EventSet, let the BrushingStateController handle it. + if (event.selection instanceof tr.model.EventSet) return; + + event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table', 'selectHistogramNames'); + + let histogramNames = event.selection; + histogramNames.sort(); + histogramNames = histogramNames.map(escapeRegExp).join('|'); + this.viewState.update({ + showAll: true, + searchQuery: `^(${histogramNames})$`, + }); + }, + + /** + * @return {!tr.v.HistogramSet} + */ + get leafHistograms() { + const histograms = new tr.v.HistogramSet(); + for (const row of + tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows)) { + if (row.subRows.length) continue; + for (const hist of row.columns.values()) { + if (!(hist instanceof tr.v.Histogram)) continue; + + histograms.addHistogram(hist); + } + } + return histograms; + } + }); + + return { + MIDLINE_HORIZONTAL_ELLIPSIS, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html new file mode 100644 index 00000000000..8a1d158d021 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html @@ -0,0 +1,396 @@ +<!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/timing.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/base/name_line_chart.html"> +<link rel="import" href="/tracing/value/ui/histogram_span.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id="tr-v-ui-histogram-set-table-cell"> + <template> + <style> + #histogram_container { + display: flex; + flex-direction: row; + } + + #missing, #empty, #unmergeable, #scalar { + flex-grow: 1; + } + + #open_histogram, #close_histogram, #open_histogram svg, #close_histogram svg { + height: 1em; + } + + #open_histogram svg { + margin-left: 4px; + stroke-width: 0; + stroke: blue; + fill: blue; + } + :host(:hover) #open_histogram svg { + background: blue; + stroke: white; + fill: white; + } + + #scalar { + flex-grow: 1; + white-space: nowrap; + } + + #histogram { + flex-grow: 1; + } + + #close_histogram svg line { + stroke-width: 18; + stroke: black; + } + #close_histogram:hover svg { + background: black; + } + #close_histogram:hover svg line { + stroke: white; + } + + #overview_container { + display: none; + } + </style> + + <div id="histogram_container"> + <span id="missing">(missing)</span> + <span id="empty">(empty)</span> + <span id="unmergeable">(unmergeable)</span> + + <tr-v-ui-scalar-span id="scalar" on-click="openHistogram_"></tr-v-ui-scalar-span> + + <span id="open_histogram" on-click="openHistogram_"> + <svg viewbox="0 0 128 128"> + <rect x="16" y="24" width="32" height="16"/> + <rect x="16" y="56" width="96" height="16"/> + <rect x="16" y="88" width="64" height="16"/> + </svg> + </span> + + <span id="histogram"></span> + + <span id="close_histogram" on-click="closeHistogram_"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </div> + + <div id="overview_container"> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-histogram-set-table-cell', + + created() { + this.viewState_ = undefined; + this.rootListener_ = this.onRootStateUpdate_.bind(this); + this.row_ = undefined; + this.displayLabel_ = ''; + this.histogram_ = undefined; + this.histogramSpan_ = undefined; + this.overviewChart_ = undefined; + this.mwuResult_ = undefined; + }, + + ready() { + this.addEventListener('click', this.onClick_.bind(this)); + }, + + attached() { + if (this.row) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + }, + + detached() { + this.row.rootViewState.removeUpdateListener(this.rootListener_); + // Don't need to removeUpdateListener for the row and cells; their + // lifetimes are the same as |this|. + }, + + updateMwu_() { + const referenceHistogram = this.referenceHistogram; + this.mwuResult_ = undefined; + if (!(this.histogram instanceof tr.v.Histogram)) return; + if (!this.histogram.canCompare(referenceHistogram)) return; + this.mwuResult_ = tr.b.math.Statistics.mwu( + this.histogram.sampleValues, + referenceHistogram.sampleValues, + this.row.rootViewState.alpha); + }, + + build(row, displayLabel, viewState) { + this.row_ = row; + this.displayLabel_ = displayLabel; + this.viewState_ = viewState; + this.histogram_ = this.row.columns.get(displayLabel); + + if (this.viewState) { + // this.viewState is undefined when this.histogram_ is undefined. + // In that case, onViewStateUpdate_ wouldn't be able to do anything + // anyway. + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + } + this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this)); + if (this.isAttached) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + + this.updateMwu_(); + + // this.histogram_ and this.referenceHistogram might be undefined, + // a HistogramSet of unmergeable Histograms, or a Histogram. + this.updateContents_(); + }, + + updateSignificance_() { + if (!this.mwuResult_) return; + this.$.scalar.significance = this.mwuResult_.significance; + }, + + get viewState() { + return this.viewState_; + }, + + get row() { + return this.row_; + }, + + get histogram() { + return this.histogram_; + }, + + get referenceHistogram() { + const referenceDisplayLabel = + this.row.rootViewState.referenceDisplayLabel; + if (!referenceDisplayLabel) return undefined; + if (referenceDisplayLabel === this.displayLabel_) return undefined; + return this.row.columns.get(referenceDisplayLabel); + }, + + get isHistogramOpen() { + return (this.histogramSpan_ !== undefined) && + (this.$.histogram.style.display === 'block'); + }, + + set isHistogramOpen(open) { + if (!(this.histogram instanceof tr.v.Histogram) || + (this.histogram.numValues === 0)) { + return; + } + + // Unfortunately, we can't use a css attribute for this since this stuff + // is tied up in all the possible states of this.histogram. See + // updateContents_(). + + this.$.scalar.style.display = open ? 'none' : 'flex'; + this.$.open_histogram.style.display = open ? 'none' : 'block'; + + this.$.close_histogram.style.display = open ? 'block' : 'none'; + this.$.histogram.style.display = open ? 'block' : 'none'; + + // Wait to create the histogram-span until the user wants to display it + // in order to speed up creating lots of histogram-set-table-cells when + // building the table. + if (open && this.histogramSpan_ === undefined) { + this.histogramSpan_ = document.createElement('tr-v-ui-histogram-span'); + this.histogramSpan_.viewState = this.viewState; + this.histogramSpan_.rowState = this.row.viewState; + this.histogramSpan_.rootState = this.row.rootViewState; + this.histogramSpan_.build(this.histogram, this.referenceHistogram); + this.$.histogram.appendChild(this.histogramSpan_); + } + + this.viewState.isOpen = open; + }, + + onViewStateUpdate_(event) { + if (event.delta.isOpen) { + this.isHistogramOpen = this.viewState.isOpen; + } + }, + + onRowStateUpdate_(event) { + if (event.delta.isOverviewed === undefined) return; + if (this.row.viewState.isOverviewed) { + this.showOverview(); + } else { + this.hideOverview(); + } + }, + + onRootStateUpdate_(event) { + if (event.delta.referenceDisplayLabel && + this.histogramSpan_) { + this.histogramSpan_.build(this.histogram, this.referenceHistogram); + } + + if (event.delta.displayStatisticName || + event.delta.referenceDisplayLabel) { + this.updateMwu_(); + this.updateContents_(); + } else if (event.delta.alpha && this.mwuResult_) { + this.mwuResult_.compare(this.row.rootViewState.alpha); + this.updateSignificance_(); + } + + if (this.row.viewState.isOverviewed && + (event.delta.sortColumnIndex || + event.delta.sortDescending || + event.delta.displayStatisticName || + event.delta.referenceDisplayLabel)) { + if (this.overviewChart_ !== undefined) { + this.$.overview_container.removeChild(this.overviewChart_); + this.overviewChart_ = undefined; + } + this.showOverview(); + } + }, + + onClick_(event) { + // Since the histogram-set-table's table doesn't support any kind of + // selection, clicking anywhere within a row that has subRows will + // expand/collapse that row, which can relayout the table and move things + // around. Prevent table relayout by preventing the tr-ui-b-table from + // receiving the click event. + event.stopPropagation(); + }, + + openHistogram_() { + this.isHistogramOpen = true; + tr.b.Timing.instant('histogram-set-table-cell', 'open'); + }, + + closeHistogram_() { + this.isHistogramOpen = false; + tr.b.Timing.instant('histogram-set-table-cell', 'close'); + }, + + updateContents_() { + const isOpen = this.isHistogramOpen; + + this.$.empty.style.display = 'none'; + this.$.unmergeable.style.display = 'none'; + this.$.scalar.style.display = 'none'; + this.$.histogram.style.display = 'none'; + this.$.close_histogram.style.display = 'none'; + this.$.open_histogram.style.visibility = 'hidden'; + + if (!this.histogram) { + this.$.missing.style.display = 'block'; + return; + } + + this.$.missing.style.display = 'none'; + + if (this.histogram instanceof tr.v.HistogramSet) { + this.$.unmergeable.style.display = 'block'; + return; + } + + if (!(this.histogram instanceof tr.v.Histogram)) { + throw new Error('Invalid Histogram: ' + this.histogram); + } + + if (this.histogram.numValues === 0) { + this.$.empty.style.display = 'block'; + return; + } + + this.$.open_histogram.style.display = 'block'; + this.$.open_histogram.style.visibility = 'visible'; + this.$.scalar.style.display = 'flex'; + + this.updateSignificance_(); + + const referenceHistogram = this.referenceHistogram; + const statName = this.histogram.getAvailableStatisticName( + this.row.rootViewState.displayStatisticName, referenceHistogram); + const statisticScalar = this.histogram.getStatisticScalar( + statName, referenceHistogram); + this.$.scalar.setValueAndUnit( + statisticScalar.value, statisticScalar.unit); + + this.isHistogramOpen = isOpen; + }, + + showOverview() { + this.$.overview_container.style.display = 'block'; + if (this.overviewChart_ !== undefined) return; + + this.row.sortSubRows(); + let referenceDisplayLabel = + this.row.rootViewState.referenceDisplayLabel; + if (referenceDisplayLabel === this.displayLabel_) { + referenceDisplayLabel = undefined; + } + const displayStatisticName = this.row.rootViewState.displayStatisticName; + const data = []; + let unit; + + for (const subRow of this.row.subRows) { + const subHist = subRow.columns.get(this.displayLabel_); + if (!(subHist instanceof tr.v.Histogram)) continue; + + if (unit === undefined) { + unit = subHist.unit; + } else if (unit !== subHist.unit) { + // The subrows have different units, so the overview chart cannot + // use a single unit to format all of the values, so don't display + // an overview chart at all. + data.splice(0); + break; + } + + const refHist = subRow.columns.get(referenceDisplayLabel); + const statName = subHist.getAvailableStatisticName( + displayStatisticName, refHist); + const statScalar = subHist.getStatisticScalar( + statName, refHist); + + if (statScalar !== undefined) { + data.push({ + x: subRow.name, + y: statScalar.value, + }); + } + } + if (data.length < 2) return; + + this.overviewChart_ = new tr.ui.b.NameLineChart(); + this.$.overview_container.appendChild(this.overviewChart_); + this.overviewChart_.displayXInHover = true; + this.overviewChart_.hideLegend = true; + this.overviewChart_.unit = unit; + this.overviewChart_.overrideDataRange = this.row.overviewDataRange; + this.overviewChart_.data = data; + }, + + hideOverview() { + this.$.overview_container.style.display = 'none'; + } + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html new file mode 100644 index 00000000000..f0dec062018 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html @@ -0,0 +1,361 @@ +<!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/timing.html"> +<link rel="import" href="/tracing/ui/base/name_line_chart.html"> + +<dom-module id="tr-v-ui-histogram-set-table-name-cell"> + <template> + <style> + #name_container { + display: flex; + } + + #name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + #show_overview, #hide_overview, #show_overview svg, #hide_overview svg { + height: 1em; + margin-left: 5px; + } + + #show_overview svg { + stroke: blue; + stroke-width: 16; + } + + #show_overview:hover svg { + background: blue; + stroke: white; + } + + #hide_overview { + display: none; + } + + #hide_overview svg { + stroke-width: 18; + stroke: black; + } + + #hide_overview:hover svg { + background: black; + stroke: white; + } + + #open_histograms, #close_histograms, #open_histograms svg, #close_histograms svg { + height: 1em; + } + + #close_histograms { + display: none; + } + + #open_histograms svg { + margin-left: 4px; + stroke-width: 0; + stroke: blue; + fill: blue; + } + #open_histograms:hover svg { + background: blue; + stroke: white; + fill: white; + } + + #close_histograms line { + stroke-width: 18; + stroke: black; + } + #close_histograms:hover { + background: black; + } + #close_histograms:hover line { + stroke: white; + } + + #overview_container { + display: none; + } + </style> + + <div id="name_container"> + <span id="name"></span> + + <span id="show_overview" on-click="showOverview_"> + <svg viewbox="0 0 128 128"> + <line x1="19" y1="109" x2="49" y2="49"/> + <line x1="49" y1="49" x2="79" y2="79"/> + <line x1="79" y1="79" x2="109" y2="19"/> + </svg> + </span> + + <span id="hide_overview" on-click="hideOverview_"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + + <span id="open_histograms" on-click="openHistograms_"> + <svg viewbox="0 0 128 128"> + <rect x="16" y="24" width="32" height="16"/> + <rect x="16" y="56" width="96" height="16"/> + <rect x="16" y="88" width="64" height="16"/> + </svg> + </span> + + <span id="close_histograms" on-click="closeHistograms_"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </div> + + <div id="overview_container"> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const NAME_COLUMN_WIDTH_PX = 300; + + Polymer({ + is: 'tr-v-ui-histogram-set-table-name-cell', + + created() { + this.row_ = undefined; + this.overviewChart_ = undefined; + this.cellListener_ = this.onCellStateUpdate_.bind(this); + this.rootListener_ = this.onRootStateUpdate_.bind(this); + }, + + attached() { + if (this.row) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + }, + + detached() { + this.row.rootViewState.removeUpdateListener(this.rootListener_); + // Don't need to removeUpdateListener for the row and cells; their + // lifetimes are the same as |this|. + }, + + get row() { + return this.row_; + }, + + build(row) { + if (this.row_ !== undefined) { + throw new Error('row must be set exactly once.'); + } + this.row_ = row; + this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this)); + this.constrainWidth = this.row.rootViewState.constrainNameColumn; + if (this.isAttached) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + + for (const cellState of this.row.viewState.cells.values()) { + cellState.addUpdateListener(this.cellListener_); + } + + Polymer.dom(this.$.name).textContent = this.row.name; + + this.title = this.row.name; + if (this.row.description) { + this.title += '\n' + this.row.description; + } + + if (this.row.overviewDataRange.isEmpty || + this.row.overviewDataRange.min === this.row.overviewDataRange.max) { + // TODO(#3744) Also hide this button when column or subrow units don't + // match. + this.$.show_overview.style.display = 'none'; + } + + let histogramCount = 0; + for (const cell of this.row.columns.values()) { + if (cell instanceof tr.v.Histogram && + cell.numValues > 0) { + ++histogramCount; + } + } + if (histogramCount <= 1) { + this.$.open_histograms.style.display = 'none'; + } + }, + + set constrainWidth(constrain) { + this.$.name.style.maxWidth = constrain ? + (this.nameWidthPx + 'px') : 'none'; + }, + + get nameWidthPx() { + // tr-ui-b-table adds 16px of padding for each additional level of subRows + // nesting, so outer nameDivs can be wider than inner nameDivs. + return NAME_COLUMN_WIDTH_PX - (16 * this.row.depth); + }, + + get isOverflowing() { + return this.$.name.style.maxWidth !== 'none' && + this.$.name.getBoundingClientRect().width === this.nameWidthPx; + }, + + get isOverviewed() { + return this.$.overview_container.style.display === 'block'; + }, + + set isOverviewed(isOverviewed) { + if (isOverviewed === this.isOverviewed) return; + if (isOverviewed) { + this.showOverview_(); + } else { + this.hideOverview_(); + } + }, + + hideOverview_(opt_event) { + this.$.overview_container.style.display = 'none'; + this.$.hide_overview.style.display = 'none'; + this.$.show_overview.style.display = 'block'; + + if (opt_event !== undefined) { + opt_event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'hideOverview'); + this.row.viewState.isOverviewed = this.isOverviewed; + } + }, + + showOverview_(opt_event) { + if (opt_event !== undefined) { + opt_event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'showOverview'); + this.row.viewState.isOverviewed = true; + } + + this.$.overview_container.style.display = 'block'; + this.$.hide_overview.style.display = 'block'; + this.$.show_overview.style.display = 'none'; + + if (this.overviewChart_ === undefined) { + const displayStatisticName = + this.row.rootViewState.displayStatisticName; + const data = []; + let unit; + + for (const [displayLabel, hist] of this.row.sortedColumns()) { + if (!(hist instanceof tr.v.Histogram)) continue; + + if (unit === undefined) { + unit = hist.unit; + } else if (unit !== hist.unit) { + // The columns have different units, so the overview chart cannot + // use a single unit to format all of the values, so don't display + // an overview chart at all. + data.splice(0); + break; + } + + const statName = hist.getAvailableStatisticName(displayStatisticName); + const statScalar = hist.getStatisticScalar(statName); + + if (statScalar !== undefined) { + data.push({ + x: displayLabel, + y: statScalar.value, + }); + } + } + if (data.length < 2) { + return; + } + + this.overviewChart_ = new tr.ui.b.NameLineChart(); + this.$.overview_container.appendChild(this.overviewChart_); + this.overviewChart_.displayXInHover = true; + this.overviewChart_.hideLegend = true; + this.overviewChart_.unit = unit; + this.overviewChart_.overrideDataRange = this.row.overviewDataRange; + this.overviewChart_.data = data; + } + }, + + openHistograms_(event) { + event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'openHistograms'); + for (const cell of this.row.cells.values()) { + cell.isHistogramOpen = true; + } + this.$.close_histograms.style.display = 'block'; + this.$.open_histograms.style.display = 'none'; + }, + + closeHistograms_(event) { + event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'closeHistograms'); + for (const cell of this.row.cells.values()) { + cell.isHistogramOpen = false; + } + this.$.open_histograms.style.display = 'block'; + this.$.close_histograms.style.display = 'none'; + }, + + onRootStateUpdate_(event) { + if (event.delta.constrainNameColumn) { + this.constrainWidth = this.row.rootViewState.constrainNameColumn; + } + if (this.row.viewState.isOverviewed && + event.delta.displayStatisticName) { + this.row.resetOverviewDataRange(); + if (this.overviewChart_ !== undefined) { + this.$.overview_container.removeChild(this.overviewChart_); + this.overviewChart_ = undefined; + } + this.showOverview_(); + } + }, + + onRowStateUpdate_(event) { + if (event.delta.isOverviewed) { + this.isOverviewed = this.row.viewState.isOverviewed; + } + // This assumes that cell states are not updated. + }, + + onCellStateUpdate_(event) { + if (!event.delta.isOpen) return; + + let cellCount = 0; + let openCellCount = 0; + for (const cell of this.row.cells.values()) { + if (!(cell.histogram instanceof tr.v.Histogram) || + (cell.histogram.numValues === 0)) { + continue; + } + ++cellCount; + if (cell.isHistogramOpen) ++openCellCount; + } + if (cellCount <= 1) return; + const mostlyOpen = openCellCount > (cellCount / 2); + this.$.open_histograms.style.display = mostlyOpen ? 'none' : 'block'; + this.$.close_histograms.style.display = mostlyOpen ? 'block' : 'none'; + } + }); + + return { + NAME_COLUMN_WIDTH_PX, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html new file mode 100644 index 00000000000..b4cb1a54020 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html @@ -0,0 +1,299 @@ +<!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/value/ui/histogram_set_table_cell.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_table_name_cell.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + class HistogramSetTableRow { + /** + * @param {!tr.v.HistogramSetHierarchy} hierarchy + * @param {!Element} baseTable tr-ui-b-table + * @param {!tr.v.ui.HistogramSetViewState} rootViewState + */ + constructor(hierarchy, baseTable, rootViewState) { + this.hierarchy_ = hierarchy; + this.baseTable_ = baseTable; + this.rootViewState_ = rootViewState; + this.viewState_ = new tr.v.ui.HistogramSetTableRowState(); + this.viewState_.addUpdateListener(this.onViewStateUpdate_.bind(this)); + this.overviewDataRange_ = undefined; + this.nameCell_ = undefined; + this.cells_ = new Map(); + this.subRows_ = []; + + // Don't assign viewState.subRows or cells. There can't be anything + // listening to viewState, so avoid the overhead of dispatching an event. + for (const subHierarchy of hierarchy.subRows) { + const subRow = new HistogramSetTableRow( + subHierarchy, baseTable, rootViewState); + this.subRows_.push(subRow); + this.viewState.subRows.set(subRow.name, subRow.viewState); + } + for (const columnName of this.columns.keys()) { + this.viewState.cells.set( + columnName, new tr.v.ui.HistogramSetTableCellState()); + } + } + + /** + * @return {string} + */ + get name() { + return this.hierarchy_.name; + } + + /** + * @return {number} + */ + get depth() { + return this.hierarchy_.depth; + } + + /** + * @return {string} + */ + get description() { + return this.hierarchy_.description; + } + + /** + * @return {!Map.<string, !(undefined|tr.v.Histogram|tr.v.HistogramSet)>} + */ + get columns() { + return this.hierarchy_.columns; + } + + * sortedColumns() { + for (const col of this.baseTable_.tableColumns) { + yield [ + col.displayLabel, + this.hierarchy_.columns.get(col.displayLabel), + ]; + } + } + + /** + * @return {!tr.b.Range} + */ + get overviewDataRange() { + if (this.overviewDataRange_ === undefined) { + this.overviewDataRange_ = new tr.b.math.Range(); + + const displayStatisticName = + this.rootViewState.displayStatisticName; + const referenceDisplayLabel = + this.rootViewState.referenceDisplayLabel; + + for (const [displayLabel, hist] of this.columns) { + if (hist instanceof tr.v.Histogram) { + const statName = hist.getAvailableStatisticName( + displayStatisticName); + const statScalar = hist.getStatisticScalar(statName); + if (statScalar !== undefined) { + this.overviewDataRange_.addValue(statScalar.value); + } + } + + for (const subRow of this.subRows) { + const subHist = subRow.columns.get(displayLabel); + if (!(subHist instanceof tr.v.Histogram)) continue; + + const refHist = subRow.columns.get(referenceDisplayLabel); + const statName = subHist.getAvailableStatisticName( + displayStatisticName, refHist); + const statScalar = subHist.getStatisticScalar( + statName, refHist); + + if (statScalar !== undefined) { + this.overviewDataRange_.addValue(statScalar.value); + } + } + } + } + return this.overviewDataRange_; + } + + /** + * overviewDataRange is used by histogram-set-table-cell (hstc) and + * histogram-set-table-name-cell (hstnc) to display overview line charts + * with consistent y-axes. + * overviewDataRange depends on HistogramSetViewState.displayStatisticName + * and referenceDisplayLabel, so it must be recomputed when either of those + * changes. + * overviewDataRange should not be recomputed for each hstc in the row; it + * should only be computed once when necessary, and cached. + * HistogramSetTableRow (HSTR) cannot listen to HistogramSetViewState + * (HSVS) updates because there is no way for it to remove the listener. + * However, Polymer has detached callbacks, so dom-modules can listen to + * HSVS updates without leaking memory. + * overviewDataRange should be recomputed only once whenever + * displayStatisticName or referenceDisplayLabel changes. + * There is exactly one hstnc per row. + * histogram-set-table-name-cell resets overviewDataRange when + * displayStatisticName or referenceDisplayLabel changes. + */ + resetOverviewDataRange() { + this.overviewDataRange_ = undefined; + } + + /** + * @return {!tr.v.ui.HistogramSetViewState} + */ + get rootViewState() { + return this.rootViewState_; + } + + /** + * @return {!Map.<string, !Element>} tr-v-ui-histogram-set-table-cell + */ + get cells() { + return this.cells_; + } + + /** + * @return {!Array.<tr.v.ui.HistogramSetTableRow>} + */ + get subRows() { + return this.subRows_; + } + + /** + * @return {!Array.<tr.v.ui.HistogramSetTableRowState>} + */ + get viewState() { + return this.viewState_; + } + + * walk() { + yield this; + for (const row of this.subRows) yield* row.walk(); + } + + static* walkAll(rootRows) { + for (const rootRow of rootRows) yield* rootRow.walk(); + } + + get nameCell() { + if (this.nameCell_ === undefined) { + this.nameCell_ = document.createElement( + 'tr-v-ui-histogram-set-table-name-cell'); + this.nameCell_.build(this); + } + return this.nameCell_; + } + + getCell(columnName) { + if (this.cells.has(columnName)) return this.cells.get(columnName); + const cell = document.createElement('tr-v-ui-histogram-set-table-cell'); + cell.build(this, columnName, this.viewState.cells.get(columnName)); + this.cells.set(columnName, cell); + return cell; + } + + compareNames(other) { + return this.name.localeCompare(other.name); + } + + compareCells(other, displayLabel) { + // If a reference column is selected, compare the absolute deltas + // between the two cells and their references. + const referenceDisplayLabel = this.rootViewState.referenceDisplayLabel; + let referenceCellA; + let referenceCellB; + if (referenceDisplayLabel && + referenceDisplayLabel !== displayLabel) { + referenceCellA = this.columns.get(referenceDisplayLabel); + referenceCellB = other.columns.get(referenceDisplayLabel); + } + + const cellA = this.columns.get(displayLabel); + let valueA = 0; + if (cellA instanceof tr.v.Histogram) { + const statisticA = cellA.getAvailableStatisticName( + this.rootViewState.displayStatisticName, referenceCellA); + const scalarA = cellA.getStatisticScalar(statisticA, referenceCellA); + if (scalarA) { + valueA = scalarA.value; + } + } + + const cellB = other.columns.get(displayLabel); + let valueB = 0; + if (cellB instanceof tr.v.Histogram) { + const statisticB = cellB.getAvailableStatisticName( + this.rootViewState.displayStatisticName, referenceCellB); + const scalarB = cellB.getStatisticScalar(statisticB, referenceCellB); + if (scalarB) { + valueB = scalarB.value; + } + } + + return valueA - valueB; + } + + onViewStateUpdate_(event) { + if (event.delta.isExpanded) { + this.baseTable_.setExpandedForTableRow(this, this.viewState.isExpanded); + } + + if (event.delta.subRows) { + throw new Error('HistogramSetTableRow.subRows must not be reassigned.'); + } + + if (event.delta.cells) { + // Only validate the cells that have already been built. + // Cells may not have been built yet, so only validate the cells that + // have been built. + for (const [displayLabel, cell] of this.cells) { + if (cell.viewState !== this.viewState.cells.get(displayLabel)) { + throw new Error('Only HistogramSetTableRow may update cells'); + } + } + } + } + + async restoreState(vs) { + // Don't use updateFromViewState() because it would overwrite cells and + // subRows, but we just want to restore them. + await this.viewState.update({ + isExpanded: vs.isExpanded, + isOverviewed: vs.isOverviewed, + }); + + // If cells haven't been built yet, then their state will be restored when + // they are built. + for (const [displayLabel, cell] of this.cells) { + const previousState = vs.cells.get(displayLabel); + if (!previousState) continue; + await cell.viewState.updateFromViewState(previousState); + } + for (const row of this.subRows) { + const previousState = vs.subRows.get(row.name); + if (!previousState) continue; + await row.restoreState(previousState); + } + } + + sortSubRows() { + const sortColumn = this.baseTable_.tableColumns[ + this.rootViewState_.sortColumnIndex]; + if (sortColumn === undefined) return; + this.subRows_.sort(sortColumn.cmp); + if (this.rootViewState_.sortDescending) { + this.subRows_.reverse(); + } + } + } + + return { + HistogramSetTableRow, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html new file mode 100644 index 00000000000..f8d76afc72b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html @@ -0,0 +1,1679 @@ +<!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/assert_utils.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + // TODO(#3811) Clean up these tests. + + const TEST_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 20); + + async function buildTable(test, histograms) { + // This should mirror HistogramImporter in order to be as similar to + // results.html as possible. + const table = document.createElement('tr-v-ui-histogram-set-table'); + + table.viewState = new tr.v.ui.HistogramSetViewState(); + await table.viewState.update({ + displayStatisticName: 'avg', + groupings: [tr.v.HistogramGrouping.HISTOGRAM_NAME], + }); + + table.style.display = 'none'; + test.addHTMLOutput(table); + + table.addEventListener('display-ready', () => { + table.style.display = ''; + }); + + const collector = new tr.v.HistogramParameterCollector(); + collector.process(histograms); + + await table.build( + histograms, + histograms.sourceHistograms, + collector.labels, + async message => { + await tr.b.animationFrame(); + }); + return table; + } + + function range(start, end) { + const result = []; + for (let i = start; i < end; ++i) result.push(i); + return result; + } + + function getBaseTable(table) { + return tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.tagName === 'TR-UI-B-TABLE'); + } + + function getNameCells(table) { + return tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL'); + } + + function getTableCells(table) { + return tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL'); + } + + test('viewSearchQuery', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + + await table.viewState.update({searchQuery: 'a'}); + let cells = getTableCells(table); + assert.lengthOf(cells, 1); + + await table.viewState.update({searchQuery: '[z-'}); + cells = getTableCells(table); + assert.lengthOf(cells, 2); + + await table.viewState.update({searchQuery: 'x'}); + cells = getTableCells(table); + assert.lengthOf(cells, 0); + + await table.viewState.update({searchQuery: ''}); + cells = getTableCells(table); + assert.lengthOf(cells, 2); + }); + + test('controlSearchQuery', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, + {value: 1, diagnostics: {r: tr.v.d.Breakdown.fromEntries([['0', 1]])}}); + const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const related = new tr.v.d.RelatedNameMap(); + related.set('0', bHist.name); + aHist.diagnostics.set('r', related); + const table = await buildTable(this, histograms); + await table.viewState.tableRowStates.get('a').cells.get('Value').update( + {isOpen: true}); + const link = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.tagName === 'TR-UI-A-ANALYSIS-LINK'); + link.click(); + assert.strictEqual('^(b)$', table.viewState.searchQuery); + }); + + test('viewReferenceDisplayLabel', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']) + ]]), + }); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']) + ]]), + }); + const table = await buildTable(this, histograms); + const baseTable = getBaseTable(table); + assert.isUndefined(baseTable.selectedTableColumnIndex); + + await table.viewState.update({referenceDisplayLabel: 'A'}); + assert.strictEqual(1, baseTable.selectedTableColumnIndex); + + await table.viewState.update({referenceDisplayLabel: 'B'}); + assert.strictEqual(2, baseTable.selectedTableColumnIndex); + }); + + test('viewDisplayStatisticName', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 10), { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']) + ]]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(10, 20), { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']) + ]]), + }); + const table = await buildTable(this, histograms); + let scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-SCALAR-SPAN'); + assert.lengthOf(scalarSpans, 2); + assert.strictEqual('5', scalarSpans[0].unit.format(scalarSpans[0].value)); + assert.strictEqual('15', scalarSpans[1].unit.format(scalarSpans[1].value)); + + await table.viewState.update({displayStatisticName: 'std'}); + scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-SCALAR-SPAN'); + assert.lengthOf(scalarSpans, 2); + assert.strictEqual('3', scalarSpans[0].unit.format(scalarSpans[0].value)); + assert.strictEqual('3', scalarSpans[1].unit.format(scalarSpans[1].value)); + }); + + test('autoShowAll', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const related = new tr.v.d.RelatedNameMap(); + related.set('0', bHist.name); + aHist.diagnostics.set('r', related); + const table = await buildTable(this, histograms); + + let cells = getNameCells(table); + assert.lengthOf(cells, 2); + assert.strictEqual('a', cells[0].row.name); + + await table.viewState.update({searchQuery: 'b'}); + assert.isTrue(table.viewState.showAll); + cells = getNameCells(table); + assert.lengthOf(cells, 1); + assert.strictEqual('b', cells[0].row.name); + }); + + test('viewShowAll', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const related = new tr.v.d.RelatedNameMap(); + related.set('0', bHist.name); + aHist.diagnostics.set('r', related); + const table = await buildTable(this, histograms); + + let cells = getNameCells(table); + assert.lengthOf(cells, 2); + assert.strictEqual('a', cells[0].row.name); + assert.strictEqual('b', cells[1].row.name); + + await table.viewState.update({showAll: false}); + cells = getNameCells(table); + assert.lengthOf(cells, 1); + assert.strictEqual('a', cells[0].row.name); + }); + + test('viewSortColumnIndex', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + const baseTable = getBaseTable(table); + assert.strictEqual(baseTable.sortColumnIndex, 0); + assert.isFalse(baseTable.sortDescending); + + await table.viewState.update({sortColumnIndex: 1, sortDescending: true}); + assert.isTrue(baseTable.sortDescending); + assert.strictEqual(baseTable.sortColumnIndex, 1); + }); + + test('controlSortColumnIndex', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + + assert.strictEqual(0, table.viewState.sortColumnIndex); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click(); + assert.strictEqual(0, table.viewState.sortColumnIndex); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[1].click(); + assert.strictEqual(1, table.viewState.sortColumnIndex); + }); + + test('viewSortDescending', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + + await table.viewState.update({sortColumnIndex: 0}); + + await table.viewState.update({sortDescending: true}); + + await table.viewState.update({sortDescending: false}); + }); + + test('controlSortDescending', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + await table.viewState.update({sortColumnIndex: 0}); + + assert.isFalse(table.viewState.sortDescending); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click(); + assert.isTrue(table.viewState.sortDescending); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click(); + assert.isFalse(table.viewState.sortDescending); + }); + + test('sortUndefinedStatistics', async function() { + // The 'avg' statistic Scalar of an empty histogram is undefined, so + // HistogramSetTableRow.compareCells must not throw when it encounters + // undefined Scalars. + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + await table.viewState.update({sortColumnIndex: 1}); + }); + + test('sortByDeltaStatistic', async function() { + const histograms0 = new tr.v.HistogramSet(); + const histograms1 = new tr.v.HistogramSet(); + const a0Hist = histograms0.createHistogram( + 'a', tr.b.Unit.byName.count, [0]); + const b0Hist = histograms0.createHistogram( + 'b', tr.b.Unit.byName.count, [0]); + const c0Hist = histograms0.createHistogram( + 'c', tr.b.Unit.byName.count, [3]); + const a1Hist = histograms1.createHistogram( + 'a', tr.b.Unit.byName.count, [1]); + const b1Hist = histograms1.createHistogram( + 'b', tr.b.Unit.byName.count, [2]); + const c1Hist = histograms1.createHistogram( + 'c', tr.b.Unit.byName.count, [3]); + histograms0.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L0'])); + histograms0.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(0)); + histograms1.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L1'])); + histograms1.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1)); + + const table = await buildTable(this, new tr.v.HistogramSet( + Array.from(histograms0).concat(Array.from(histograms1)))); + await table.viewState.update({ + displayStatisticName: tr.v.DELTA + 'avg', + referenceDisplayLabel: 'L0', + sortColumnIndex: 2, + }); + const nameCells = getNameCells(table); + assert.strictEqual('c', nameCells[0].row.name); + assert.strictEqual('a', nameCells[1].row.name); + assert.strictEqual('b', nameCells[2].row.name); + }); + + test('sortMissing', async function() { + // Missing cells should be treated as zero for sorting purposes. The + // comparator must not return undefined or NaN. + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])], + ]), + }); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])], + ]), + }); + // 'c','x' intentionally missing + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])], + ]), + }); + // 'b','y' intentionally missing + histograms.createHistogram('c', tr.b.Unit.byName.count, [-1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({sortColumnIndex: 2}); + let cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('c', cells[0].row.name); + assert.strictEqual('b', cells[1].row.name); + assert.strictEqual('a', cells[2].row.name); + await table.viewState.update({sortDescending: true}); + cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('a', cells[0].row.name); + assert.strictEqual('b', cells[1].row.name); + assert.strictEqual('c', cells[2].row.name); + await table.viewState.update({sortColumnIndex: 1}); + cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('b', cells[0].row.name); + assert.strictEqual('a', cells[1].row.name); + assert.strictEqual('c', cells[2].row.name); + await table.viewState.update({sortDescending: false}); + cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('c', cells[0].row.name); + assert.strictEqual('a', cells[1].row.name); + assert.strictEqual('b', cells[2].row.name); + }); + + test('viewConstrainNameColumn', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + assert.isTrue(table.viewState.constrainNameColumn); + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('block', dots.style.display); + + await table.viewState.update({constrainNameColumn: false}); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + + await table.viewState.update({constrainNameColumn: true}); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('controlConstrainNameColumn', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + assert.isTrue(table.viewState.constrainNameColumn); + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('block', dots.style.display); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click(); + assert.isFalse(table.viewState.constrainNameColumn); + await tr.b.animationFrame(); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click(); + assert.isTrue(table.viewState.constrainNameColumn); + await tr.b.animationFrame(); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('viewRowExpanded', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['A'])); + const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + assert.lengthOf(getTableCells(table), 1); + + await table.viewState.tableRowStates.get('a').update({isExpanded: true}); + assert.lengthOf(getTableCells(table), 3); + + await table.viewState.tableRowStates.get('a').update({isExpanded: false}); + assert.lengthOf(getTableCells(table), 1); + }); + + test('controlRowExpanded', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['A'])); + const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded); + + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + nameCell.click(); + assert.isTrue(table.viewState.tableRowStates.get('a').isExpanded); + + nameCell.click(); + assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded); + }); + + test('viewIsOverviewed', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + const nameCells = getNameCells(table); + const cells = getTableCells(table); + assert.isFalse(nameCells[0].isOverviewed); + + await table.viewState.tableRowStates.get('a').update({isOverviewed: true}); + assert.isTrue(nameCells[0].isOverviewed); + + await table.viewState.tableRowStates.get('a').update({isOverviewed: false}); + assert.isFalse(nameCells[0].isOverviewed); + }); + + test('overviewSorted', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [4], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['D'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [2], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [3], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['C'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({ + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: 0, + sortDescending: true, + }); + await table.viewState.tableRowStates.get('a').update({isOverviewed: true}); + + let cells = getTableCells(table); + let chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.strictEqual('D', chart.data[0].x); + assert.strictEqual('C', chart.data[1].x); + assert.strictEqual('B', chart.data[2].x); + assert.strictEqual('A', chart.data[3].x); + + await table.viewState.update({ + sortDescending: false, + }); + cells = getTableCells(table); + chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.strictEqual('A', chart.data[0].x); + assert.strictEqual('B', chart.data[1].x); + assert.strictEqual('C', chart.data[2].x); + assert.strictEqual('D', chart.data[3].x); + }); + + test('controlIsOverviewed', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed); + + const nameCells = getNameCells(table); + tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e => + e.id === 'show_overview').click(); + assert.isTrue(table.viewState.tableRowStates.get('a').isOverviewed); + + tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e => + e.id === 'hide_overview').click(); + assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed); + }); + + test('overviewStatistic', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({ + displayStatisticName: 'count', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + }); + await table.viewState.tableRowStates.get('a').update({isOverviewed: true}); + + let charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.lengthOf(charts, 3); + assert.lengthOf(charts[0].data, 2); + assert.lengthOf(charts[1].data, 2); + assert.lengthOf(charts[2].data, 2); + assert.strictEqual(charts[0].data[0].y, 3); + assert.strictEqual(charts[0].data[1].y, 7); + assert.strictEqual(charts[1].data[0].y, 1); + assert.strictEqual(charts[1].data[1].y, 2); + assert.strictEqual(charts[2].data[0].y, 3); + assert.strictEqual(charts[2].data[1].y, 4); + + await table.viewState.update({ + displayStatisticName: tr.v.DELTA + 'count', + referenceDisplayLabel: 'A', + }); + charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.lengthOf(charts, 3); + assert.lengthOf(charts[0].data, 2); + assert.lengthOf(charts[1].data, 2); + assert.lengthOf(charts[2].data, 2); + assert.strictEqual(charts[0].data[0].y, 3); + assert.strictEqual(charts[0].data[1].y, 7); + assert.strictEqual(charts[1].data[0].y, 1); + assert.strictEqual(charts[1].data[1].y, 2); + assert.strictEqual(charts[2].data[0].y, 2); + assert.strictEqual(charts[2].data[1].y, 2); + }); + + test('overviewUnits', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + histograms.createHistogram( + 'a', tr.b.Unit.byName.unitlessNumber, [1, 1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({ + displayStatisticName: 'count', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + }); + await table.viewState.tableRowStates.get('a').update({ + isExpanded: true, + isOverviewed: true, + }); + await table.viewState.tableRowStates.get('a').subRows.get('X').update({ + isOverviewed: true, + }); + + const nameCells = getNameCells(table); + + // Check there is no overviewChart in name-cell when column units mismatch. + assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[0], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[2], + '#overview_container svg')); + + // When column units match, the overview chart should exist and have the + // correct unit. + const nameOverviewChart = tr.ui.b.findDeepElementMatching(nameCells[1], + '#overview_container svg'); + assert.isDefined(nameOverviewChart); + assert.strictEqual(nameOverviewChart.unit, tr.b.Unit.byName.count); + + const cells = getTableCells(table); + + // When subrow units match, the overview chart should exist and have the + // correct unit. + const overviewChart = tr.ui.b.findDeepElementMatching(cells[0], + '#overview_container svg'); + assert.isDefined(overviewChart); + assert.strictEqual(overviewChart.unit, tr.b.Unit.byName.count); + + // Check there is no overviewChart in table-cell when subrow units mismatch. + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[1], + '#overview_container svg')); + + // Check there is no overviewChart in table-cell when there are no subrows. + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[2], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[3], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[4], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[5], + '#overview_container svg')); + }); + + test('viewCellOpen', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + const table = await buildTable(this, histograms); + const cell = tr.b.getOnlyElement(getTableCells(table)); + assert.isFalse(cell.isHistogramOpen); + + await table.viewState.tableRowStates.get('a').cells.get('Value').update( + {isOpen: true}); + assert.isTrue(cell.isHistogramOpen); + + await table.viewState.tableRowStates.get('a').cells.get('Value').update( + {isOpen: false}); + assert.isFalse(cell.isHistogramOpen); + }); + + test('controlCellOpen', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + const cells = getTableCells(table); + + tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.tagName === 'TR-V-UI-SCALAR-SPAN').click(); + assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + + tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.id === 'close_histogram').click(); + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.id === 'open_histograms').click(); + assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('B') + .isOpen); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.id === 'close_histograms').click(); + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('B') + .isOpen); + }); + + test('rebin', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 100), { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(50, 150), { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + const table = await buildTable(this, histograms); + + const cells = getTableCells(table); + assert.lengthOf(cells, 2); + assert.lengthOf(cells[0].histogram.allBins, + 2 + tr.v.DEFAULT_REBINNED_COUNT); + assert.lengthOf(cells[1].histogram.allBins, + 2 + tr.v.DEFAULT_REBINNED_COUNT); + assert.strictEqual(cells[0].histogram.allBins[0].range.max, 0); + assert.strictEqual(cells[1].histogram.allBins[0].range.max, 0); + assert.strictEqual(cells[0].histogram.allBins[41].range.min, 200); + assert.strictEqual(cells[1].histogram.allBins[41].range.min, 200); + }); + + test('leafHistograms', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + assert.lengthOf(table.leafHistograms, 1); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + assert.lengthOf(table.leafHistograms, 2); + }); + + test('nameCellOverflow', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('block', dots.style.display); + dots.click(); + + await tr.b.animationFrame(); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('nameCellOverflowOnExpand', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['0'.repeat(100)])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['1'.repeat(100)])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('none', dots.style.display); + + const baseTable = getBaseTable(table); + await table.viewState.tableRowStates.get('a').update({isExpanded: true}); + + const nameCell = tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL' && + e.row.name === '0'.repeat(100)); + await tr.b.animationFrame(); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + + assert.strictEqual('block', dots.style.display); + dots.click(); + + await tr.b.animationFrame(); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('overviewCharts', async function() { + const binBoundaries = tr.v.HistogramBinBoundaries.createLinear(0, 150, 10); + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [0], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [10], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [100], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [110], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [0], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [9], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [90], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [99], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + const now = new Date().getTime(); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + table.viewState.tableRowStates.values())) { + await row.update({isOverviewed: true}); + } + + let charts = tr.ui.b.findDeepElementsMatchingPredicate( + table, e => ((e.id === 'overview_container') && + (e.style.display !== 'none'))); + charts = charts.map(div => div.children[0]); + assert.lengthOf(charts, 6); + + assert.deepEqual(JSON.stringify(charts[0].data), + JSON.stringify([{x: '0', y: 45}, {x: '1', y: 54}])); + tr.b.assertRangeEquals( + charts[0].dataRange, tr.b.math.Range.fromExplicitRange(0, 99)); + + assert.deepEqual( + charts[1].data, [{x: 'story0', y: 0}, {x: 'story1', y: 90}]); + tr.b.assertRangeEquals( + charts[1].dataRange, tr.b.math.Range.fromExplicitRange(0, 99)); + + assert.deepEqual( + charts[2].data, [{x: 'story0', y: 9}, {x: 'story1', y: 99}]); + tr.b.assertRangeEquals( + charts[2].dataRange, tr.b.math.Range.fromExplicitRange(0, 99)); + + assert.deepEqual(charts[3].data, [{x: '0', y: 50}, {x: '1', y: 60}]); + tr.b.assertRangeEquals( + charts[3].dataRange, tr.b.math.Range.fromExplicitRange(0, 110)); + + assert.deepEqual( + charts[4].data, [{x: 'story0', y: 0}, {x: 'story1', y: 100}]); + tr.b.assertRangeEquals( + charts[4].dataRange, tr.b.math.Range.fromExplicitRange(0, 110)); + + assert.deepEqual( + charts[5].data, [{x: 'story0', y: 10}, {x: 'story1', y: 110}]); + tr.b.assertRangeEquals( + charts[5].dataRange, tr.b.math.Range.fromExplicitRange(0, 110)); + + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + table.viewState.tableRowStates.values())) { + await row.update({isOverviewed: false}); + } + + charts = tr.ui.b.findDeepElementsMatchingPredicate( + table, e => ((e.id === 'overview_container') && + (e.style.display !== 'none'))); + assert.lengthOf(charts, 0); + }); + + test('sortByDisplayStatistic', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram( + 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [0, 10], { + binBoundaries: TEST_BOUNDARIES, + }); + histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [5], { + binBoundaries: TEST_BOUNDARIES, + }); + + const table = await buildTable(this, histograms); + await table.viewState.update({ + sortColumnIndex: 1, + sortDescending: false, + displayStatisticName: 'min', + }); + + let nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'bar'); + assert.strictEqual(nameCells[1].row.name, 'foo'); + + await table.viewState.update({sortDescending: true}); + + nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'foo'); + assert.strictEqual(nameCells[1].row.name, 'bar'); + + await table.viewState.update({displayStatisticName: 'max'}); + + nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'bar'); + assert.strictEqual(nameCells[1].row.name, 'foo'); + + await table.viewState.update({sortDescending: false}); + + nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'foo'); + assert.strictEqual(nameCells[1].row.name, 'bar'); + }); + + test('displayStatistic', async function() { + const histograms = new tr.v.HistogramSet(); + const now = new Date().getTime(); + const barHist = histograms.createHistogram( + 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1, 2, 3], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['bar'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + const fooHist = histograms.createHistogram( + 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [10, 20, 30], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + // Add a Histogram with another name so that the table displays the scalars. + const quxHist = histograms.createHistogram( + 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const table = await buildTable(this, histograms); + + function histogramsEqual(a, b) { + // This is not an exhaustive equality check. This only tests the fields + // that are distinguishing for this test(). + if (a.name !== b.name) return false; + return tr.v.HistogramGrouping.DISPLAY_LABEL.callback(a) === + tr.v.HistogramGrouping.DISPLAY_LABEL.callback(b); + } + + let fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, fooHist))); + assert.isDefined(fooCell); + + let fooContent = tr.ui.b.findDeepElementMatchingPredicate( + fooCell, elem => elem.id === 'content'); + assert.isDefined(fooContent); + + let barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, barHist))); + assert.isDefined(barCell); + + let barContent = tr.ui.b.findDeepElementMatchingPredicate( + barCell, elem => elem.id === 'content'); + assert.isDefined(barContent); + + assert.strictEqual(table.viewState.displayStatisticName, 'avg'); + assert.strictEqual('20.000 ms', fooContent.textContent); + assert.strictEqual('2.000 ms', barContent.textContent); + + await table.viewState.update({referenceDisplayLabel: 'foo'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, fooHist))); + assert.isDefined(fooCell); + + fooContent = tr.ui.b.findDeepElementMatchingPredicate( + fooCell, elem => elem.id === 'content'); + assert.isDefined(fooContent); + + barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, barHist))); + assert.isDefined(barCell); + + barContent = tr.ui.b.findDeepElementMatchingPredicate( + barCell, elem => elem.id === 'content'); + assert.isDefined(barContent); + + await table.viewState.update({displayStatisticName: `${tr.v.DELTA}avg`}); + assert.strictEqual('20.000 ms', fooContent.textContent); + assert.strictEqual('-18.000 ms', barContent.textContent); + + await table.viewState.update({displayStatisticName: `%${tr.v.DELTA}avg`}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, fooHist))); + assert.isDefined(fooCell); + + fooContent = tr.ui.b.findDeepElementMatchingPredicate( + fooCell, elem => elem.id === 'content'); + assert.isDefined(fooContent); + + barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, barHist))); + assert.isDefined(barCell); + + barContent = tr.ui.b.findDeepElementMatchingPredicate( + barCell, elem => elem.id === 'content'); + assert.isDefined(barContent); + + assert.strictEqual(table.viewState.displayStatisticName, + `%${tr.v.DELTA}avg`); + assert.strictEqual('20.000 ms', fooContent.textContent); + assert.strictEqual('-90.0%', barContent.textContent); + }); + + test('requestSelectionChange', async function() { + const histograms = new tr.v.HistogramSet(); + const barHist = histograms.createHistogram( + 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], { + binBoundaries: TEST_BOUNDARIES, + }); + + const fooHist = histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, { + value: 1, + diagnostics: { + breakdown: tr.v.d.Breakdown.fromEntries([ + ['bar', 1], + ['qux', 0], + ]), + }, + }, { + binBoundaries: TEST_BOUNDARIES, + }); + + const quxHist = histograms.createHistogram( + 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TEST_BOUNDARIES, + }); + const breakdown = new tr.v.d.RelatedNameMap(); + breakdown.set('bar', barHist.name); + breakdown.set('qux', quxHist.name); + fooHist.diagnostics.set('breakdown', breakdown); + + const table = await buildTable(this, histograms); + await table.viewState.update({showAll: false}); + + let fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + let barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isUndefined(barCell); + + fooCell.isHistogramOpen = true; + + const barLink = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => elem.tagName === 'TR-UI-A-ANALYSIS-LINK'); + assert.isDefined(barLink); + barLink.click(); + + await tr.b.animationFrame(); + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + await table.viewState.update({searchQuery: ''}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + fooCell.isHistogramOpen = true; + + const selectAllLink = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-UI-A-ANALYSIS-LINK') && + (elem.textContent === 'Select All'))); + assert.isDefined(selectAllLink); + selectAllLink.click(); + + assert.strictEqual(table.viewState.searchQuery, '^(bar|qux)$'); + + await tr.b.animationFrame(); + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isUndefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + const quxCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'qux'))); + assert.isDefined(quxCell); + }); + + test('search', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram( + 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], { + binBoundaries: TEST_BOUNDARIES, + }); + histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], { + binBoundaries: TEST_BOUNDARIES, + }); + + const table = await buildTable(this, histograms); + + let fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + let barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + await table.viewState.update({searchQuery: 'bar'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isUndefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + await table.viewState.update({searchQuery: 'foo'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isUndefined(barCell); + + // As users type in regexes, some intermediate forms may be invalid regexes. + // When the search is an invalid regex, just ignore it. + await table.viewState.update({searchQuery: '[a-'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + }); + + test('emptyAndMissing', async function() { + const now = new Date().getTime(); + const histograms = new tr.v.HistogramSet(); + + const histA = histograms.createHistogram( + 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['iteration A'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const histB = histograms.createHistogram( + 'histogram B', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['iteration B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const histC = histograms.createHistogram( + 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['iteration B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const table = await buildTable(this, histograms); + + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === '(empty)')); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === '(missing)')); + }); + + test('instantiate_1x1', async function() { + const histograms = new tr.v.HistogramSet(); + const hist = histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + }); + const table = await buildTable(this, histograms); + + const baseTable = getBaseTable(table); + assert.strictEqual(baseTable.tableRows.length, 1); + + const cell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => + elem.tagName === 'TR-V-UI-SCALAR-SPAN'); + cell.click(); + + const yAxisText = tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.tagName === 'text' && e.textContent === '<0.000 ms'); + assert.isBelow(0, yAxisText.getBBox().width); + }); + + test('merge_unmergeable', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [], { + binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 21), + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])], + ]), + }); + const table = await buildTable(this, histograms); + assert.strictEqual(table.viewState.tableRowStates.size, 1); + assert.instanceOf(tr.b.getOnlyElement(getTableCells(table)).histogram, + tr.v.HistogramSet); + }); + + test('instantiate_1x5', async function() { + const histograms = new tr.v.HistogramSet(); + + for (let i = 0; i < 5; ++i) { + histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['' + i])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + } + const table = await buildTable(this, histograms); + }); + + test('instantiate_2x2', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('foo', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + const table = await buildTable(this, histograms); + const baseTable = getBaseTable(table); + + assert.lengthOf(baseTable.tableColumns, 3); + assert.strictEqual('Name', + baseTable.tableColumns[0].title.children[0].textContent); + assert.strictEqual('A', + baseTable.tableColumns[1].title.textContent); + assert.strictEqual('B', + baseTable.tableColumns[2].title.textContent); + + await table.viewState.update({referenceDisplayLabel: 'A'}); + baseTable.rebuild(); + assert.strictEqual(1, baseTable.selectedTableColumnIndex); + let cells = getTableCells(table); + assert.strictEqual(cells[1].referenceHistogram, cells[0].histogram); + assert.strictEqual(cells[3].referenceHistogram, cells[2].histogram); + + await table.viewState.update({referenceDisplayLabel: 'B'}); + cells = getTableCells(table); + assert.strictEqual(2, baseTable.selectedTableColumnIndex); + assert.strictEqual(cells[0].referenceHistogram, cells[1].histogram); + assert.strictEqual(cells[2].referenceHistogram, cells[3].histogram); + + // Test sorting by the reference column when the displayStatistic is a delta + // statistic. + await table.viewState.update({sortColumnIndex: 2}); + let nameCell = getNameCells(table)[0]; + const originalFirstRow = nameCell.row.name; + // This is either 'foo' or 'bar' depending on Math.random() above. + + await table.viewState.update({ + sortDescending: !table.viewState.sortDescending, + }); + baseTable.rebuild(); + nameCell = getNameCells(table)[0]; + assert.notEqual(originalFirstRow, nameCell.row.name); + }); + + test('merged', async function() { + const histograms = new tr.v.HistogramSet(); + // Add 2^8=256 Histograms, all with the same name, with different metadata. + const benchmarkNames = ['bm A', 'bm B']; + const storyGroupingKeys0 = ['A', 'B']; + const storyGroupingKeys1 = ['C', 'D']; + const storyNames = ['story A', 'story B']; + const starts = [1439708400000, 1439794800000]; + const labels = ['label A', 'label B']; + const name = 'name '.repeat(20); + const unit = tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; + + for (const benchmarkName of benchmarkNames) { + for (const storyGroupingKey0 of storyGroupingKeys0) { + for (const storyGroupingKey1 of storyGroupingKeys1) { + for (const storyName of storyNames) { + for (const startMs of starts) { + for (let storysetCounter = 0; storysetCounter < 2; + ++storysetCounter) { + for (const label of labels) { + const samples = range(0, 100).map(i => { + return { + value: Math.random() * 1e3, + diagnostics: {i: new tr.v.d.GenericSet([i])}, + }; + }); + histograms.createHistogram(name, unit, samples, { + description: 'The best description.', + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet([label])], + [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, + new tr.v.d.GenericSet([storysetCounter])], + [tr.v.d.RESERVED_NAMES.BENCHMARKS, + new tr.v.d.GenericSet([benchmarkName])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(startMs)], + [tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet([storyName])], + [tr.v.d.RESERVED_NAMES.STORY_TAGS, + new tr.v.d.GenericSet([ + `storyGroupingKey0:${storyGroupingKey0}`, + `storyGroupingKey1:${storyGroupingKey1}`, + ])], + ]), + }); + } + } + } + } + } + } + } + histograms.buildGroupingsFromTags([tr.v.d.RESERVED_NAMES.STORY_TAGS]); + + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARKS), + tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey0Tag'), + tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey1Tag'), + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARK_START), + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORYSET_REPEATS), + ]}); + const baseTable = getBaseTable(table); + + assert.lengthOf(baseTable.tableColumns, 3); + const nameHeaderCell = baseTable.tableColumns[0].title; + assert.strictEqual('Name', nameHeaderCell.children[0].textContent); + assert.strictEqual('label A', baseTable.tableColumns[1].title.textContent); + assert.strictEqual('label B', baseTable.tableColumns[2].title.textContent); + + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.closeTo(346, nameCell.getBoundingClientRect().width, 1); + + nameHeaderCell.children[1].click(); + // toggleNameColumnWidth_ does not await viewState.update() + await tr.b.animationFrame(); + assert.isBelow(322, nameCell.getBoundingClientRect().width); + + nameHeaderCell.children[1].click(); + await tr.b.animationFrame(); + assert.closeTo(346, nameCell.getBoundingClientRect().width, 1); + + assert.lengthOf(baseTable.tableRows, 1); + assert.strictEqual(name, baseTable.tableRows[0].name); + assert.lengthOf(baseTable.tableRows[0].subRows, 2); + + // assertions only report their arguments, which is not enough information + // to diagnose problems with nested structures like tableRows -- the path to + // the particular row is needed. This code would be a bit simpler if each + // row were given a named variable, but the path to each subRow would still + // need to be tracked in order to provide for diagnosing. + const subRowPath = []; + function getSubRow() { + let row = baseTable.tableRows[0]; + for (const index of subRowPath) { + row = row.subRows[index]; + } + return row; + } + + for (let i = 0; i < benchmarkNames.length; ++i) { + subRowPath.push(i); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(benchmarkNames[i], getSubRow().name, subRowPath); + + for (let s = 0; s < storyGroupingKeys0.length; ++s) { + subRowPath.push(s); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(storyGroupingKeys0[s], getSubRow().name, subRowPath); + + for (let t = 0; t < storyGroupingKeys1.length; ++t) { + subRowPath.push(t); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(storyGroupingKeys1[t], getSubRow().name, + subRowPath); + + for (let j = 0; j < storyNames.length; ++j) { + subRowPath.push(j); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(storyNames[j], getSubRow().name, subRowPath); + + for (let k = 0; k < starts.length; ++k) { + subRowPath.push(k); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(tr.b.formatDate(new Date(starts[k])), + getSubRow().name, subRowPath); + + for (let l = 0; l < 2; ++l) { + subRowPath.push(l); + assert.lengthOf(getSubRow().subRows, 0, subRowPath); + assert.strictEqual('' + l, getSubRow().name, subRowPath); + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html new file mode 100644 index 00000000000..18ba824811f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html @@ -0,0 +1,210 @@ +<!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/timing.html"> +<link rel="import" href="/tracing/ui/null_brushing_state_controller.html"> +<link rel="import" href="/tracing/value/csv_builder.html"> +<link rel="import" href="/tracing/value/histogram_parameter_collector.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_controls.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_table.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<dom-module id="tr-v-ui-histogram-set-view"> + <template> + <style> + :host { + font-family: sans-serif; + } + + #zero { + color: red; + /* histogram-set-table is used by both metrics-side-panel and results.html. + * This font-size rule has no effect in results.html, but improves + * legibility in the metrics-side-panel, which sets font-size in order to + * make this table denser. + */ + font-size: initial; + } + + #container { + display: none; + } + + #visualizations{ + display: none; + } + </style> + + <div id="zero">zero Histograms</div> + + <div id="container"> + <tr-v-ui-histogram-set-controls id="controls"> + </tr-v-ui-histogram-set-controls> + + <tr-v-ui-visualizations-data-container id="visualizations"> + </tr-v-ui-visualizations-data-container> + + <tr-v-ui-histogram-set-table id="table"></tr-v-ui-histogram-set-table> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-histogram-set-view', + + listeners: { + export: 'onExport_', + loadVisualization: 'onLoadVisualization_' + }, + + created() { + this.brushingStateController_ = new tr.ui.NullBrushingStateController(); + this.viewState_ = new tr.v.ui.HistogramSetViewState(); + this.visualizationLoaded_ = false; + }, + + ready() { + this.$.table.viewState = this.viewState; + this.$.controls.viewState = this.viewState; + }, + + attached() { + this.brushingStateController.parentController = + tr.c.BrushingStateController.getControllerForElement(this.parentNode); + }, + + get brushingStateController() { + return this.brushingStateController_; + }, + + get viewState() { + return this.viewState_; + }, + + get histograms() { + return this.$.table.histograms; + }, + + /** + * @param {!tr.v.HistogramSet} histograms + * @param {!Object=} opt_options + * @param {function(string):!Promise=} opt_options.progressconst + * @param {string=} opt_options.helpHref + * @param {string=} opt_options.feedbackHref + */ + async build(histograms, opt_options) { + const options = opt_options || {}; + const progress = options.progress || (() => Promise.resolve()); + + if (options.helpHref) this.$.controls.helpHref = options.helpHref; + if (options.feedbackHref) { + this.$.controls.feedbackHref = options.feedbackHref; + } + + if (histograms === undefined || histograms.length === 0) { + this.$.container.style.display = 'none'; + this.$.zero.style.display = 'block'; + this.style.display = 'block'; + return; + } + this.$.zero.style.display = 'none'; + this.$.container.style.display = 'block'; + this.$.container.style.maxHeight = (window.innerHeight - 16) + 'px'; + + const buildMark = tr.b.Timing.mark('histogram-set-view', 'build'); + await progress('Finding important Histograms...'); + const sourceHistogramsMark = tr.b.Timing.mark( + 'histogram-set-view', 'sourceHistograms'); + const sourceHistograms = histograms.sourceHistograms; + sourceHistogramsMark.end(); + // Disable show_all if all values are sourceHistograms. + this.$.controls.showAllEnabled = ( + sourceHistograms.length !== histograms.length); + + await progress('Collecting parameters...'); + const collectParametersMark = tr.b.Timing.mark( + 'histogram-set-view', 'collectParameters'); + const parameterCollector = new tr.v.HistogramParameterCollector(); + parameterCollector.process(histograms); + this.$.controls.baseStatisticNames = parameterCollector.statisticNames; + this.$.controls.possibleGroupings = parameterCollector.possibleGroupings; + const displayLabels = parameterCollector.labels; + this.$.controls.displayLabels = displayLabels; + collectParametersMark.end(); + + // Only show visualizer button if values are from rendering benchmarks + const hist = [...histograms][0]; + const benchmarks = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS); + let enable = false; + if (benchmarks !== undefined && benchmarks.length > 0) { + for (const benchmark of benchmarks) { + if (benchmark.includes('rendering')) { + enable = true; + break; + } + } + } + this.$.controls.enableVisualization = enable; + + // Table.build() displays its own progress. + await this.$.table.build( + histograms, sourceHistograms, displayLabels, progress); + + buildMark.end(); + }, + + onExport_(event) { + const mark = tr.b.Timing.mark('histogram-set-view', 'export' + + (event.merged ? 'Merged' : 'Raw') + event.format.toUpperCase()); + + const histograms = event.merged ? this.$.table.leafHistograms : + this.histograms; + + let blob; + if (event.format === 'csv') { + const csv = new tr.v.CSVBuilder(histograms); + csv.build(); + blob = new window.Blob([csv.toString()], {type: 'text/csv'}); + } else if (event.format === 'json') { + blob = new window.Blob([JSON.stringify(histograms.asDicts())], + {type: 'text/json'}); + } else { + throw new Error(`Unable to export format "${event.format}"`); + } + + const path = window.location.pathname.split('/'); + const basename = path[path.length - 1].split('.')[0] || 'histograms'; + + const anchor = document.createElement('a'); + anchor.download = `${basename}.${event.format}`; + anchor.href = window.URL.createObjectURL(blob); + anchor.click(); + mark.end(); + }, + + onLoadVisualization_(event) { + if (!this.visualizationLoaded_) { // Initial loading + this.$.visualizations.style.display = 'block'; + this.$.visualizations.build(this.$.table.leafHistograms, + this.histograms); + this.visualizationLoaded_ = true; + } else if (this.$.visualizations.style.display === 'none') { + // Toggle to hide + this.$.visualizations.style.display = 'block'; + } else { + this.$.visualizations.style.display = 'none'; + } + }, + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html new file mode 100644 index 00000000000..ade4ef2a9c4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html @@ -0,0 +1,144 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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/view_state.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + class HistogramSetViewState extends tr.b.ViewState { + constructor() { + super(); + this.define('searchQuery', ''); + this.define('referenceDisplayLabel', ''); + this.define('displayStatisticName', ''); + this.define('showAll', true); + this.define('groupings', []); + this.define('sortColumnIndex', 0); + this.define('sortDescending', false); + this.define('constrainNameColumn', true); + this.define('tableRowStates', new Map()); + this.define('alpha', 0.01); + } + } + + tr.b.ViewState.register(HistogramSetViewState); + + class HistogramSetTableRowState extends tr.b.ViewState { + constructor() { + super(); + this.define('isExpanded', false); + this.define('isOverviewed', false); + this.define('cells', new Map()); + this.define('subRows', new Map()); + this.define('diagnosticsTab', ''); + } + + asCompactDict() { + const result = {}; + if (this.isExpanded) result.e = '1'; + if (this.isOverviewed) result.o = '1'; + if (this.diagnosticsTab) result.d = this.diagnosticsTab; + const cells = {}; + for (const [name, cell] of this.cells) { + const cellDict = cell.asCompactDict(); + if (cellDict === undefined) continue; + cells[name] = cellDict; + } + if (Object.keys(cells).length > 0) result.c = cells; + + const subRows = {}; + for (const [name, row] of this.subRows) { + const rowDict = row.asCompactDict(); + if (rowDict === undefined) continue; + subRows[name] = rowDict; + } + if (Object.keys(subRows).length > 0) result.r = subRows; + + if (Object.keys(result).length === 0) return undefined; + + return result; + } + + async updateFromCompactDict(dict) { + await this.update({ + isExpanded: dict.e === '1', + isOverviewed: dict.o === '1', + diagnosticsTab: dict.d || '', + }); + + for (const [name, cellDict] of Object.entries(dict.c || {})) { + const cell = this.cells.get(name); + if (cell === undefined) continue; + await cell.updateFromCompactDict(cellDict); + } + + for (const [name, subRowDict] of Object.entries(dict.r || {})) { + const subRow = this.subRows.get(name); + if (subRow === undefined) continue; + await subRow.updateFromCompactDict(subRowDict); + } + } + + * walk() { + yield this; + for (const row of this.subRows.values()) yield* row.walk(); + } + + static* walkAll(rootRows) { + for (const rootRow of rootRows) yield* rootRow.walk(); + } + } + + tr.b.ViewState.register(HistogramSetTableRowState); + + class HistogramSetTableCellState extends tr.b.ViewState { + constructor() { + super(); + this.define('isOpen', false); + this.define('brushedBinRange', new tr.b.math.Range()); + this.define('mergeSampleDiagnostics', true); + } + + asCompactDict() { + const result = {}; + if (this.isOpen) result.o = '1'; + if (!this.mergeSampleDiagnostics) result.m = '0'; + if (!this.brushedBinRange.isEmpty) { + result.b = this.brushedBinRange.min + '_' + this.brushedBinRange.max; + } + if (Object.keys(result).length === 0) return undefined; + return result; + } + + async updateFromCompactDict(dict) { + let binRange = this.brushedBinRange; + if (dict.b) { + let [bMin, bMax] = dict.b.split('_'); + bMin = parseInt(bMin); + bMax = parseInt(bMax); + if (bMin !== binRange.min || bMax !== binRange.max) { + binRange = tr.b.math.Range.fromExplicitRange(bMin, bMax); + } + } + await this.update({ + isOpen: dict.o === '1', + brushedBinRange: binRange, + mergeSampleDiagnostics: dict.m !== '0', + }); + } + } + + tr.b.ViewState.register(HistogramSetTableCellState); + + return { + HistogramSetTableCellState, + HistogramSetTableRowState, + HistogramSetViewState, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html new file mode 100644 index 00000000000..ede486c98d5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html @@ -0,0 +1,72 @@ +<!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/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate0', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + const histograms = new tr.v.HistogramSet(); + + const hist = new tr.v.Histogram('a', tr.b.Unit.byName.normalizedPercentage); + for (let i = 0; i < 1e2; ++i) { + hist.addSample(Math.random()); + } + histograms.addHistogram(hist); + + this.addHTMLOutput(view); + await view.build(histograms); + + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); + + test('implicitUndefinedHistogramSet', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + this.addHTMLOutput(view); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); + + test('explicitUndefinedHistogramSet', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + this.addHTMLOutput(view); + view.build(undefined); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); + + test('emptyHistogramSet', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + this.addHTMLOutput(view); + view.build(new tr.v.HistogramSet()); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html new file mode 100644 index 00000000000..c0382f66b93 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html @@ -0,0 +1,599 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/timing.html"> +<link rel="import" href="/tracing/ui/base/box_chart.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/name_bar_chart.html"> +<link rel="import" href="/tracing/ui/base/tab_view.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_map_table.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> +<link rel="import" href="/tracing/value/ui/scalar_map_table.html"> + +<dom-module id="tr-v-ui-histogram-span"> + <template> + <style> + #container { + display: flex; + flex-direction: row; + justify-content: space-between; + } + #chart { + flex-grow: 1; + display: none; + } + #drag_handle, #diagnostics_tab_templates { + display: none; + } + #chart svg { + display: block; + } + #stats_container { + overflow-y: auto; + } + </style> + + <div id="container"> + <div id="chart"></div> + <div id="stats_container"> + <tr-v-ui-scalar-map-table id="stats"></tr-v-ui-scalar-map-table> + </div> + </div> + <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle> + + <tr-ui-b-tab-view id="diagnostics"></tr-ui-b-tab-view> + + <div id="diagnostics_tab_templates"> + <tr-v-ui-diagnostic-map-table id="metric_diagnostics"></tr-v-ui-diagnostic-map-table> + + <tr-v-ui-diagnostic-map-table id="metadata_diagnostics"></tr-v-ui-diagnostic-map-table> + + <div id="sample_diagnostics_container"> + <div id="merge_sample_diagnostics_container"> + <input type="checkbox" id="merge_sample_diagnostics" checked on-change="updateDiagnostics_"> + <label for="merge_sample_diagnostics">Merge Sample Diagnostics</label> + </div> + <tr-v-ui-diagnostic-map-table id="sample_diagnostics"></tr-v-ui-diagnostic-map-table> + </div> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const DEFAULT_BAR_HEIGHT_PX = 5; + const TRUNCATE_BIN_MARGIN = 0.15; + const IGNORE_DELTA_STATISTICS_NAMES = [ + `${tr.v.DELTA}min`, + `%${tr.v.DELTA}min`, + `${tr.v.DELTA}max`, + `%${tr.v.DELTA}max`, + `${tr.v.DELTA}sum`, + `%${tr.v.DELTA}sum`, + `${tr.v.DELTA}count`, + `%${tr.v.DELTA}count`, + ]; + + Polymer({ + is: 'tr-v-ui-histogram-span', + + created() { + this.viewStateListener_ = this.onViewStateUpdate_.bind(this); + this.viewState = new tr.v.ui.HistogramSetTableCellState(); + this.rowStateListener_ = this.onRowStateUpdate_.bind(this); + this.rowState = new tr.v.ui.HistogramSetTableRowState(); + this.rootStateListener_ = this.onRootStateUpdate_.bind(this); + this.rootState = new tr.v.ui.HistogramSetViewState(); + + this.histogram_ = undefined; + this.referenceHistogram_ = undefined; + this.graphWidth_ = undefined; + this.graphHeight_ = undefined; + this.mouseDownBin_ = undefined; + this.prevBrushedBinRange_ = new tr.b.math.Range(); + this.anySampleDiagnostics_ = false; + this.canMergeSampleDiagnostics_ = true; + this.mwuResult_ = undefined; + }, + + get rowState() { + return this.rowState_; + }, + + set rowState(rs) { + if (this.rowState) { + this.rowState.removeUpdateListener(this.rowStateListener_); + } + this.rowState_ = rs; + this.rowState.addUpdateListener(this.rowStateListener_); + if (this.isAttached) this.updateContents_(); + }, + + get viewState() { + return this.viewState_; + }, + + set viewState(vs) { + if (this.viewState) { + this.viewState.removeUpdateListener(this.viewStateListener_); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.viewStateListener_); + if (this.isAttached) this.updateContents_(); + }, + + get rootState() { + return this.rootState_; + }, + + set rootState(vs) { + if (this.rootState) { + this.rootState.removeUpdateListener(this.rootStateListener_); + } + this.rootState_ = vs; + this.rootState.addUpdateListener(this.rootStateListener_); + if (this.isAttached) this.updateContents_(); + }, + + build(histogram, opt_referenceHistogram) { + this.histogram_ = histogram; + this.$.metric_diagnostics.histogram = histogram; + this.$.sample_diagnostics.histogram = histogram; + this.referenceHistogram_ = opt_referenceHistogram; + + if (this.histogram.canCompare(this.referenceHistogram)) { + this.mwuResult_ = tr.b.math.Statistics.mwu( + this.histogram.sampleValues, + this.referenceHistogram.sampleValues, + this.rootState.alpha); + } + + this.anySampleDiagnostics_ = false; + for (const bin of this.histogram.allBins) { + if (bin.diagnosticMaps.length > 0) { + this.anySampleDiagnostics_ = true; + break; + } + } + + if (this.isAttached) this.updateContents_(); + }, + + onViewStateUpdate_(event) { + if (event.delta.brushedBinRange) { + if (this.chart_ !== undefined) { + this.chart_.brushedRange = this.viewState.brushedBinRange; + } + this.updateDiagnostics_(); + } + + if (event.delta.mergeSampleDiagnostics && + (this.viewState.mergeSampleDiagnostics !== + this.$.merge_sample_diagnostics.checked)) { + this.$.merge_sample_diagnostics.checked = + this.canMergeSampleDiagnostics && + this.viewState.mergeSampleDiagnostics; + this.updateDiagnostics_(); + } + }, + + updateSignificance_() { + if (!this.mwuResult_) return; + this.$.stats.setSignificanceForKey( + `${tr.v.DELTA}avg`, this.mwuResult_.significance); + }, + + onRootStateUpdate_(event) { + if (event.delta.alpha && this.mwuResult_) { + this.mwuResult_.compare(this.rootState.alpha); + this.updateSignificance_(); + } + }, + + onRowStateUpdate_(event) { + if (event.delta.diagnosticsTab) { + if (this.rowState.diagnosticsTab === + this.$.sample_diagnostics_container.tabLabel) { + this.updateDiagnostics_(); + } else { + for (const tab of this.$.diagnostics.subViews) { + if (this.rowState.diagnosticsTab === tab.tabLabel) { + this.$.diagnostics.selectedSubView = tab; + break; + } + } + } + } + }, + + ready() { + this.$.metric_diagnostics.tabLabel = 'histogram diagnostics'; + this.$.sample_diagnostics_container.tabLabel = 'sample diagnostics'; + this.$.metadata_diagnostics.tabLabel = 'metadata'; + this.$.metadata_diagnostics.isMetadata = true; + this.$.diagnostics.addEventListener( + 'selected-tab-change', this.onSelectedDiagnosticsChanged_.bind(this)); + this.$.drag_handle.target = this.$.container; + this.$.drag_handle.addEventListener( + 'drag-handle-resize', this.onResize_.bind(this)); + }, + + attached() { + if (this.histogram_ !== undefined) this.updateContents_(); + }, + + get canMergeSampleDiagnostics() { + return this.canMergeSampleDiagnostics_; + }, + + set canMergeSampleDiagnostics(merge) { + this.canMergeSampleDiagnostics_ = merge; + if (!merge) this.viewState.mergeSampleDiagnostics = false; + this.$.merge_sample_diagnostics_container.style.display = ( + merge ? '' : 'none'); + }, + + onResize_(event) { + event.stopPropagation(); + let heightPx = parseInt(this.$.container.style.height); + if (heightPx < this.defaultGraphHeight) { + heightPx = this.defaultGraphHeight; + this.$.container.style.height = this.defaultGraphHeight + 'px'; + } + this.chart_.graphHeight = heightPx - (this.chart_.margin.top + + this.chart_.margin.bottom); + this.$.stats_container.style.maxHeight = + this.chart_.getBoundingClientRect().height + 'px'; + }, + + /** + * Get the width in pixels of the widest bar in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @return {number} + */ + get graphWidth() { + return this.graphWidth_ || this.defaultGraphWidth; + }, + + /** + * Set the width in pixels of the widest bar in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @param {number} width + */ + set graphWidth(width) { + this.graphWidth_ = width; + }, + + /** + * Get the height in pixels of the bars in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @return {number} + */ + get graphHeight() { + return this.graphHeight_ || this.defaultGraphHeight; + }, + + /** + * Set the height in pixels of the bars in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @param {number} height + */ + set graphHeight(height) { + this.graphHeight_ = height; + }, + + /** + * Get the height in pixels of one bar in the bar chart. + * + * @return {number} + */ + get barHeight() { + return this.chart_.barHeight; + }, + + /** + * Set the height in pixels of one bar in the bar chart. + * + * @param {number} px + */ + set barHeight(px) { + this.graphHeight = this.computeChartHeight_(px); + }, + + computeChartHeight_(barHeightPx) { + return (this.chart_.margin.top + + this.chart_.margin.bottom + + (barHeightPx * this.histogram.allBins.length)); + }, + + get defaultGraphHeight() { + if (this.histogram && this.histogram.allBins.length === 1) { + return 150; + } + return this.computeChartHeight_(DEFAULT_BAR_HEIGHT_PX); + }, + + get defaultGraphWidth() { + if (this.histogram.allBins.length === 1) { + return 100; + } + return 300; + }, + + get brushedBins() { + const bins = []; + if (this.histogram && !this.viewState.brushedBinRange.isEmpty) { + for (let i = this.viewState.brushedBinRange.min; + i < this.viewState.brushedBinRange.max; ++i) { + bins.push(this.histogram.allBins[i]); + } + } + return bins; + }, + + async updateBrushedRange_(binIndex) { + const brushedBinRange = new tr.b.math.Range(); + brushedBinRange.addValue(tr.b.math.clamp( + this.mouseDownBinIndex_, 0, this.histogram.allBins.length - 1)); + brushedBinRange.addValue(tr.b.math.clamp( + binIndex, 0, this.histogram.allBins.length - 1)); + brushedBinRange.max += 1; + await this.viewState.update({brushedBinRange}); + }, + + onMouseDown_(chartEvent) { + chartEvent.stopPropagation(); + if (!this.histogram) return; + this.prevBrushedBinRange_ = this.viewState.brushedBinRange; + this.mouseDownBinIndex_ = chartEvent.y; + this.updateBrushedRange_(chartEvent.y); + }, + + onMouseMove_(chartEvent) { + chartEvent.stopPropagation(); + if (!this.histogram) return; + this.updateBrushedRange_(chartEvent.y); + }, + + onMouseUp_(chartEvent) { + chartEvent.stopPropagation(); + if (!this.histogram) return; + this.updateBrushedRange_(chartEvent.y); + if (this.prevBrushedBinRange_.range === 1 && + this.viewState.brushedBinRange.range === 1 && + (this.prevBrushedBinRange_.min === + this.viewState.brushedBinRange.min)) { + tr.b.Timing.instant('histogram-span', 'clearBrushedBins'); + this.viewState.update({brushedBinRange: new tr.b.math.Range()}); + } else { + tr.b.Timing.instant('histogram-span', 'brushBins'); + } + this.mouseDownBinIndex_ = undefined; + }, + + async onSelectedDiagnosticsChanged_() { + await this.rowState.update({ + diagnosticsTab: this.$.diagnostics.selectedSubView.tabLabel, + }); + if ((this.$.diagnostics.selectedSubView === + this.$.sample_diagnostics_container) && + this.histogram && + this.viewState.brushedBinRange.isEmpty) { + // When the user selects the sample diagnostics tab, if they haven't + // already brushed any bins, then automatically brush all bins. + const brushedBinRange = tr.b.math.Range.fromExplicitRange( + 0, this.histogram.allBins.length); + await this.viewState.update({brushedBinRange}); + this.updateDiagnostics_(); + } + }, + + updateDiagnostics_() { + let maps = []; + for (const bin of this.brushedBins) { + for (const map of bin.diagnosticMaps) { + maps.push(map); + } + } + + if (this.$.merge_sample_diagnostics.checked !== + this.viewState.mergeSampleDiagnostics) { + this.viewState.update({ + mergeSampleDiagnostics: this.$.merge_sample_diagnostics.checked}); + } + + if (this.viewState.mergeSampleDiagnostics) { + const merged = new tr.v.d.DiagnosticMap(); + for (const map of maps) { + merged.addDiagnostics(map); + } + maps = [merged]; + } + + const mark = tr.b.Timing.mark('histogram-span', + (this.viewState.mergeSampleDiagnostics ? 'merge' : 'split') + + 'SampleDiagnostics'); + this.$.sample_diagnostics.diagnosticMaps = maps; + mark.end(); + + if (this.anySampleDiagnostics_) { + this.$.diagnostics.selectedSubView = + this.$.sample_diagnostics_container; + } + }, + + get histogram() { + return this.histogram_; + }, + + get referenceHistogram() { + return this.referenceHistogram_; + }, + + getDeltaScalars_(statNames, scalarMap) { + if (!this.histogram.canCompare(this.referenceHistogram)) return; + + for (const deltaStatName of tr.v.Histogram.getDeltaStatisticsNames( + statNames)) { + if (IGNORE_DELTA_STATISTICS_NAMES.includes(deltaStatName)) continue; + const scalar = this.histogram.getStatisticScalar( + deltaStatName, this.referenceHistogram, this.mwuResult_); + if (scalar === undefined) continue; + scalarMap.set(deltaStatName, scalar); + } + }, + + set isYLogScale(logScale) { + this.chart_.isYLogScale = logScale; + }, + + async updateContents_() { + this.$.chart.style.display = 'none'; + this.$.drag_handle.style.display = 'none'; + this.$.container.style.justifyContent = ''; + + while (Polymer.dom(this.$.chart).lastChild) { + Polymer.dom(this.$.chart).removeChild( + Polymer.dom(this.$.chart).lastChild); + } + + if (!this.histogram) return; + this.$.container.style.display = ''; + + const scalarMap = new Map(); + this.getDeltaScalars_(this.histogram.statisticsNames, scalarMap); + for (const [name, scalar] of this.histogram.statisticsScalars) { + scalarMap.set(name, scalar); + } + this.$.stats.scalarMap = scalarMap; + this.updateSignificance_(); + + const metricDiagnosticMap = new tr.v.d.DiagnosticMap(); + const metadataDiagnosticMap = new tr.v.d.DiagnosticMap(); + for (const [key, diagnostic] of this.histogram.diagnostics) { + // Hide implementation details. + if (diagnostic instanceof tr.v.d.RelatedNameMap) continue; + + if (tr.v.d.RESERVED_NAMES_SET.has(key)) { + metadataDiagnosticMap.set(key, diagnostic); + } else { + metricDiagnosticMap.set(key, diagnostic); + } + } + + const diagnosticTabs = []; + if (metricDiagnosticMap.size) { + this.$.metric_diagnostics.diagnosticMaps = [metricDiagnosticMap]; + diagnosticTabs.push(this.$.metric_diagnostics); + } + if (this.anySampleDiagnostics_) { + diagnosticTabs.push(this.$.sample_diagnostics_container); + } + if (metadataDiagnosticMap.size) { + this.$.metadata_diagnostics.diagnosticMaps = [metadataDiagnosticMap]; + diagnosticTabs.push(this.$.metadata_diagnostics); + } + this.$.diagnostics.resetSubViews(diagnosticTabs); + this.$.diagnostics.set('tabsHidden', diagnosticTabs.length < 2); + + if (this.histogram.numValues <= 1) { + await this.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange( + 0, this.histogram.allBins.length)}); + this.$.container.style.justifyContent = 'flex-end'; + return; + } + + this.$.chart.style.display = 'block'; + this.$.drag_handle.style.display = 'block'; + + if (this.histogram.allBins.length === 1) { + if (this.histogram.min !== this.histogram.max) { + this.chart_ = new tr.ui.b.BoxChart(); + Polymer.dom(this.$.chart).appendChild(this.chart_); + this.chart_.graphWidth = this.graphWidth; + this.chart_.graphHeight = this.graphHeight; + this.chart_.hideXAxis = true; + this.chart_.data = [ + { + x: '', + color: 'blue', + percentile_0: this.histogram.running.min, + percentile_25: this.histogram.getApproximatePercentile(0.25), + percentile_50: this.histogram.getApproximatePercentile(0.5), + percentile_75: this.histogram.getApproximatePercentile(0.75), + percentile_100: this.histogram.running.max, + } + ]; + } + this.$.stats_container.style.maxHeight = + this.chart_.getBoundingClientRect().height + 'px'; + await this.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange( + 0, this.histogram.allBins.length)}); + return; + } + + this.chart_ = new tr.ui.b.NameBarChart(); + Polymer.dom(this.$.chart).appendChild(this.chart_); + this.chart_.graphWidth = this.graphWidth; + this.chart_.graphHeight = this.graphHeight; + this.chart_.addEventListener('item-mousedown', + this.onMouseDown_.bind(this)); + this.chart_.addEventListener('item-mousemove', + this.onMouseMove_.bind(this)); + this.chart_.addEventListener('item-mouseup', + this.onMouseUp_.bind(this)); + this.chart_.hideLegend = true; + this.chart_.getDataSeries('y').color = 'blue'; + this.chart_.xAxisLabel = '#'; + this.chart_.brushedRange = this.viewState.brushedBinRange; + if (!this.viewState.brushedBinRange.isEmpty) { + this.updateDiagnostics_(); + } + + const chartData = []; + const binCounts = []; + for (const bin of this.histogram.allBins) { + let x = bin.range.min; + if (x === -Number.MAX_VALUE) { + x = '<' + new tr.b.Scalar( + this.histogram.unit, bin.range.max).toString(); + } else { + x = new tr.b.Scalar(this.histogram.unit, x).toString(); + } + chartData.push({x, y: bin.count}); + binCounts.push(bin.count); + } + + // If the largest 1 or 2 bins are more than twice as large as the next + // largest bin, then set the dataRange max to TRUNCATE_BIN_MARGIN% more + // than that next largest bin. + binCounts.sort((x, y) => y - x); + const dataRange = tr.b.math.Range.fromExplicitRange(0, binCounts[0]); + if (binCounts[1] > 0 && binCounts[0] > (binCounts[1] * 2)) { + dataRange.max = binCounts[1] * (1 + TRUNCATE_BIN_MARGIN); + } + if (binCounts[2] > 0 && binCounts[1] > (binCounts[2] * 2)) { + dataRange.max = binCounts[2] * (1 + TRUNCATE_BIN_MARGIN); + } + this.chart_.overrideDataRange = dataRange; + + this.chart_.data = chartData; + this.$.stats_container.style.maxHeight = + this.chart_.getBoundingClientRect().height + 'px'; + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html new file mode 100644 index 00000000000..2ca360b3348 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html @@ -0,0 +1,300 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/assert_utils.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/histogram_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basic', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + h.addSample(-1, {foo: new tr.v.d.GenericSet(['a'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['c'])}); + h.addSample(500, {foo: new tr.v.d.GenericSet(['c'])}); + h.addSample(999, {foo: new tr.v.d.GenericSet(['d'])}); + h.addSample(1000, {foo: new tr.v.d.GenericSet(['d'])}); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('emptyHistogram', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('viewBrushedBinRange', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count, + [0, 1, 2, 3, 4].map(value => { + return {value, diagnostics: new Map([ + ['i', new tr.v.d.GenericSet([value])]])}; + }))); + assert.isTrue(span.viewState.brushedBinRange.isEmpty); + + await span.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange(5, 10), + }); + const chart = tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.tagName === 'svg'); + assert.strictEqual(5, chart.brushedRange.min); + assert.strictEqual(10, chart.brushedRange.max); + }); + + test('controlBrushedBinRange', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count, + [0, 1, 2, 3, 4])); + assert.isTrue(span.viewState.brushedBinRange.isEmpty); + + span.onMouseDown_({ + stopPropagation: () => undefined, + y: 21, + }); + span.onMouseUp_({ + stopPropagation: () => undefined, + y: 0, + }); + tr.b.assertRangeEquals(span.viewState.brushedBinRange, + tr.b.math.Range.fromExplicitRange(0, 22)); + }); + + test('viewMergeSampleDiagnostics', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + const samples = []; + for (let i = 0; i < 5; ++i) { + samples.push({ + value: i, + diagnostics: { + breakdown: tr.v.d.Breakdown.fromDict({ + values: { + a: 5 - i, + b: i + 5, + c: i, + }, + }), + }, + }); + } + span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples)); + await span.viewState.update({brushedBinRange: + tr.b.math.Range.fromExplicitRange(0, 10)}); + const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e => + e.id === 'merge_sample_diagnostics'); + assert.isTrue(merge.checked); + + await span.viewState.update({mergeSampleDiagnostics: false}); + assert.isFalse(merge.checked); + + await span.viewState.update({mergeSampleDiagnostics: true}); + assert.isTrue(merge.checked); + }); + + test('controlMergeSampleDiagnostics', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + const samples = []; + for (let i = 0; i < 5; ++i) { + samples.push({ + value: i, + diagnostics: { + breakdown: tr.v.d.Breakdown.fromDict({ + values: { + a: 5 - i, + b: i + 5, + c: i, + }, + }), + }, + }); + } + span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples)); + await span.viewState.update({brushedBinRange: + tr.b.math.Range.fromExplicitRange(0, 10)}); + const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e => + e.id === 'merge_sample_diagnostics'); + assert.isTrue(merge.checked); + + merge.click(); + assert.isFalse(span.viewState.mergeSampleDiagnostics); + + merge.click(); + assert.isTrue(span.viewState.mergeSampleDiagnostics); + }); + + test('mergeSampleDiagnostics', async function() { + // Add several samples with sample diagnostics to a Histogram, brush all of + // the bins, test that the sample diagnostics are merged. + const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage); + h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})}); + h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})}); + h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})}); + h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})}); + h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})}); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + await span.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)}); + let breakdowns = tr.ui.b.findDeepElementsMatchingPredicate( + span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN'); + assert.lengthOf(breakdowns, 1); + + const merge = tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.id === 'merge_sample_diagnostics'); + merge.click(); + breakdowns = tr.ui.b.findDeepElementsMatchingPredicate( + span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN'); + assert.lengthOf(breakdowns, 5); + }); + + test('cannotMergeSampleDiagnostics', async function() { + // Add several samples with sample diagnostics to a Histogram, brush all of + // the bins, test that the sample diagnostics are not merged. + const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage); + h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})}); + h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})}); + h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})}); + h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})}); + h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})}); + + const span = document.createElement('tr-v-ui-histogram-span'); + span.canMergeSampleDiagnostics = false; + this.addHTMLOutput(span); + span.build(h); + await span.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)}); + const breakdowns = tr.ui.b.findDeepElementsMatchingPredicate( + span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN'); + assert.lengthOf(breakdowns, 5); + }); + + test('singleSample', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + h.addSample(100, { + sample_diagnostic_0: new tr.v.d.GenericSet(['foo']), + sample_diagnostic_1: new tr.v.d.GenericSet(['bar']), + }); + h.diagnostics.set('histogram diagnostic 0', new tr.v.d.GenericSet(['baz'])); + h.diagnostics.set('histogram diagnostic 1', new tr.v.d.GenericSet(['qux'])); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('nans', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + h.addSample(undefined, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(NaN, {foo: new tr.v.d.GenericSet(['c'])}); + h.customizeSummaryOptions({nans: true}); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('singleBin', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber, + tr.v.HistogramBinBoundaries.SINGULAR); + h.addSample(0); + h.addSample(25); + h.addSample(100); + h.addSample(100); + h.addSample(25); + h.addSample(50); + h.addSample(75); + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('referenceHistogram', function() { + const span = document.createElement('tr-v-ui-histogram-span'); + span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, [1, 10, 100], { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + }), tr.v.Histogram.create('', tr.b.Unit.byName.count, [2, 20, 200], { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + })); + this.addHTMLOutput(span); + }); + + test('breakdownUnit', async function() { + const root = new tr.v.Histogram('root', tr.b.Unit.byName.sizeInBytes); + const sampleBreakdown = new tr.v.d.Breakdown(); + sampleBreakdown.set('x', 30 << 20); + sampleBreakdown.set('y', 70 << 20); + root.addSample(100 << 20, {sampleBreakdown}); + const rhb = new tr.v.d.RelatedNameMap(); + root.diagnostics.set('rhb', rhb); + const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.sizeInBytes); + rhb.set('a', aHist.name); + aHist.addSample(10 << 20); + const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.sizeInBytes); + rhb.set('b', bHist.name); + bHist.addSample(90 << 20); + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(root); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.textContent === '100.0 MiB')); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.textContent === '30.0 MiB')); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.textContent === '70.0 MiB')); + }); + + test('diagnosticsTabs', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + span.build(tr.v.Histogram.create( + '', tr.b.Unit.byName.count, [ + {value: 1, diagnostics: new Map([ + ['sample diagnostic', new tr.v.d.GenericSet(['value1'])], + ])}, + {value: 10, diagnostics: new Map([ + ['sample diagnostic', new tr.v.d.GenericSet(['value10'])], + ])}, + ], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet([ + 'system_health.common_desktop'])], + ]), + })); + this.addHTMLOutput(span); + + const sample = tr.ui.b.findDeepElementMatching( + span, '#sample_diagnostics_container'); + assert.strictEqual(span.rowState.diagnosticsTab, sample.tabLabel); + const tabs = tr.ui.b.findDeepElementMatching( + span, 'TR-UI-B-TAB-VIEW'); + assert.strictEqual(tabs.selectedSubView, sample); + + const metadata = tr.ui.b.findDeepElementMatching( + span, '#metadata_diagnostics'); + await span.rowState.update({diagnosticsTab: metadata.tabLabel}); + assert.strictEqual(tabs.selectedSubView, metadata); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html new file mode 100644 index 00000000000..1bc6ea696b3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html @@ -0,0 +1,353 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 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/math.html"> +<link rel="import" href="/tracing/base/math/running_statistics.html"> +<link rel="import" href="/tracing/ui/base/name_bar_chart.html"> +<link rel="import" href="/tracing/ui/base/name_column_chart.html"> +<dom-module id='tr-v-ui-metrics-visualization'> + <template> + <style> + button { + padding: 5px; + font-size: 14px; + } + + .text_input { + width: 50px; + padding: 4px; + font-size: 14px; + } + + .error { + color: red; + display: none; + } + + .container { + position: relative; + display: inline-block; + margin-left: 15px; + } + + #title { + font-size: 20px; + font-weight: bold; + padding-bottom: 5px; + } + + #selectors { + display: block; + padding-bottom: 10px; + } + + #search_page { + width: 200px; + margin-left: 30px; + } + + #close { + display: none; + vertical-align: top; + } + + #close svg{ + height: 1em; + } + + #close svg line { + stroke-width: 18; + stroke: black; + } + + #close:hover svg { + background: black; + } + + #close:hover svg line { + stroke: white; + } + </style> + <span id="aggregateContainer" class="container"> + </span> + <span id="pageByPageContainer" class="container"> + <span id="selectors"> + <span id="percentile_label">Percentile Range:</span> + <input id="start" class="text_input" placeholder="0"> + <input id="end" class="text_input" placeholder="100"> + <button id="filter" on-tap="filterByPercentile_">Filter</button> + <input id="search_page" class="text_input" placeholder="Page Name"> + <button id="search" on-tap="searchByPage_">Search</button> + <span id="search_error" class="error">Sorry, could not find that page!</span> + </span> + </span> + <div id="submetricsContainer" display="block"> + <span id="close"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + const PAGE_BREAKDOWN_KEY = 'pageBreakdown'; + + Polymer({ + is: 'tr-v-ui-metrics-visualization', + + created() { + this.charts_ = new Map(); + }, + + ready() { + this.$.start.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.filterByPercentile_(); + }); + + this.$.end.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.filterByPercentile_(); + }); + + this.$.search_page.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.searchByPage_(); + }); + }, + + build(chartData) { + this.title_ = chartData.title; + this.aggregateData_ = chartData.aggregate; + this.data_ = chartData.page; + this.submetricsData_ = chartData.submetrics; + this.benchmarkCount_ = chartData.aggregate.length; + + // build aggregate chart + const aggregateChart = this.initializeColumnChart(this.title_); + Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart); + this.charts_.set(tr.v.ui.AGGREGATE_KEY, aggregateChart); + this.setChartColors_(tr.v.ui.AGGREGATE_KEY); + aggregateChart.data = chartData.aggregate; + this.setChartSize_(tr.v.ui.AGGREGATE_KEY); + + // build page by page + const newChart = this.initializeColumnChart(this.title_ + ' Breakdown'); + newChart.enableToolTip = true; + newChart.toolTipCallBack = (rect) => + this.openChildChart_(rect); + Polymer.dom(this.$.pageByPageContainer).appendChild(newChart); + this.charts_.set(PAGE_BREAKDOWN_KEY, newChart); + this.setChartColors_(PAGE_BREAKDOWN_KEY); + newChart.data = this.data_; + this.setChartSize_(PAGE_BREAKDOWN_KEY); + }, + + setChartSize_(page) { + const chart = this.charts_.get(page); + const pageCount = chart.data.length; + chart.graphHeight = tr.b.math.clamp(pageCount * 20, 400, 600); + chart.graphWidth = tr.b.math.clamp(pageCount * 30, 200, 1000); + }, + + // Assign color gradient to series in chart + setChartColors_(page) { + const chart = this.charts_.get(page); + const metrics = tr.v.ui.METRICS.get(this.title_); + for (let i = 0; i < this.benchmarkCount_; ++i) { + for (let j = 0; j < metrics.length; ++j) { + const mainColorIndex = j % tr.v.ui.COLORS.length; + const subColorIndex = i % tr.v.ui.COLORS[mainColorIndex].length; + const color = tr.v.ui.COLORS[mainColorIndex][subColorIndex]; + const series = metrics[j] + '-' + this.aggregateData_[i].x; + chart.getDataSeries(series).color = color; + if (i === 0) { + chart.getDataSeries(series).title = metrics[j]; + } else { + chart.getDataSeries(series).title = ''; + } + } + } + }, + + // Element creation + initializeColumnChart(title) { + const newChart = new tr.ui.b.NameColumnChart(); + newChart.hideLegend = false; + newChart.isStacked = true; + newChart.yAxisLabel = 'ms'; + newChart.hideXAxis = true; + newChart.displayXInHover = true; + newChart.isGrouped = true; + newChart.showTitleInLegend = true; + newChart.chartTitle = title; + newChart.titleHeight = '14pt'; + return newChart; + }, + + initializeChildChart_(title, height, width) { + const div = document.createElement('div'); + div.classList.add('container'); + Polymer.dom(this.$.submetricsContainer). + insertBefore(div, this.$.submetricsContainer.firstChild); + + const childChart = new tr.ui.b.NameBarChart(); + childChart.xAxisLabel = 'ms'; + childChart.chartTitle = title; + childChart.graphHeight = height; + childChart.graphWidth = width; + childChart.titleHeight = '14pt'; + childChart.isStacked = true; + childChart.hideLegend = true; + childChart.isGrouped = true; + childChart.isWaterfall = true; + + div.appendChild(childChart); + + const button = this.initializeCloseButton_(div, + this.$.submetricsContainer); + div.appendChild(button); + return childChart; + }, + + initializeCloseButton_(div, parent) { + const button = this.$.close.cloneNode(true); + button.style.display = 'inline-block'; + button.addEventListener ('click', () => { + Polymer.dom(parent).removeChild(div); + }); + return button; + }, + + // Create child chart and populate it + openChildChart_(rect) { + // Find main metric and corresponding sub-metrics + const metrics = tr.v.ui.METRICS.get(this.title_); + let metric; + let metricIndex; + for (let i = 0; i < metrics.length; ++i) { + if (rect.key.startsWith(metrics[i])) { + metric = metrics[i]; + metricIndex = i; + break; + } + } + + // Create child chart + const page = rect.datum.group; + const title = this.title_ + ' ' + metric + ': ' + page; + const submetrics = this.submetricsData_.get(page).get(metric); + const width = tr.b.math.clamp(submetrics.size * 150, 300, 700); + const height = tr.b.math.clamp(submetrics.size * + this.benchmarkCount_ * 50, 300, 700); + + const childChart = this.initializeChildChart_(title, height, width); + + // Get breakdown data for main step + childChart.data = this.processSubmetrics_(childChart, + submetrics, 0, metricIndex).data; + }, + + processSubmetrics_(chart, submetrics, hideValue, metricIndex) { + const finalData = []; + let submetricIndex = 0; + for (const submetric of submetrics.values()) { + let benchmarkIndex = 0; + for (const benchmark of submetric.values()) { + benchmark.hide = !hideValue ? 0 : hideValue; + const series = benchmark.x + '-' + benchmark.group; + const mainColorIndex = metricIndex % tr.v.ui.COLORS.length; + const subColorIndex = benchmarkIndex % + tr.v.ui.COLORS[mainColorIndex].length; + chart.getDataSeries(series).color = + tr.v.ui.COLORS[mainColorIndex][subColorIndex]; + if (benchmarkIndex === (this.benchmarkCount_ - 1)) { + hideValue += benchmark[series]; + } + finalData.push(benchmark); + benchmarkIndex++; + } + submetricIndex++; + } + return {data: finalData, hide: hideValue}; + }, + + // Handle filtering by start and end percentiles + filterByPercentile_() { + const startPercentile = this.$.start.value; + const endPercentile = this.$.end.value; + + if (startPercentile === '' || endPercentile === '') return; + + const length = this.data_.length / (this.benchmarkCount_ + 1); + const startIndex = this.getPercentileIndex_(startPercentile, length); + const endIndex = this.getPercentileIndex_(endPercentile, length); + this.charts_.get(PAGE_BREAKDOWN_KEY).data = + this.data_.slice(startIndex, endIndex); + }, + + // Get index of x percentile value + getPercentileIndex_(percentile, arrayLength) { + const index = Math.ceil(arrayLength * (percentile / 100.0)); + if (index === -1) return 0; + if (index >= arrayLength) return arrayLength; + return index * this.benchmarkCount_; + }, + + // Handle searching by page name + searchByPage_() { + const criteria = this.$.search_page.value; + if (criteria === '') return; + + const query = new RegExp(criteria); + + const filteredData = [...this.data_] + .filter(group => { + if (group.group) return group.group.match(query); + return false; + }); + + if (filteredData.length < 1) { + this.$.search_error.style.display = 'block'; + return; + } + + // Create child chart with breakdown data + const page = filteredData[0].group; + const title = this.title_ + ' Breakdown: ' + page; + const metricToSubmetricMap = this.submetricsData_.get(page); + + let totalSubmetrics = 0; + for (const submetrics of metricToSubmetricMap.values()) { + for (const benchmark of submetrics.values()) { + totalSubmetrics += benchmark.length; + } + } + const width = tr.b.math.clamp(totalSubmetrics * 150, 300, 700); + const height = tr.b.math.clamp(totalSubmetrics * + this.benchmarkCount_ * 30, 300, 700); + + const childChart = this.initializeChildChart_(title, height, width); + + const childData = []; + let hide = 0; + let metricIndex = 0; + for (const submetrics of metricToSubmetricMap.values()) { + const submetricsData = this.processSubmetrics_(childChart, submetrics, + hide, metricIndex); + childData.push(...submetricsData.data); + hide = submetricsData.hide; + metricIndex++; + } + childChart.data = childData; + }, + + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html new file mode 100644 index 00000000000..0d062557f63 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 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/value/ui/metrics_visualization.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + function generateChartBar(metrics, benchmark, page) { + const data = {x: benchmark, group: page}; + for (const metric of metrics) { + const key = metric + '-' + benchmark; + const mean = Math.random() * 100; + data[key] = Math.round(mean * 100) / 100; + } + return data; + } + + function generateSubmetricBar(submetric, benchmark, page, + metricToSubmetricMap) { + let submetricToBenchmarkMap = metricToSubmetricMap.get(submetric); + if (!submetricToBenchmarkMap) { + submetricToBenchmarkMap = []; + metricToSubmetricMap.set(submetric, submetricToBenchmarkMap); + } + const data = {x: submetric, hide: 0, group: benchmark}; + const mean = Math.random() * 100; + data[submetric + '-' + benchmark] = Math.round(mean * 100) / 100; + submetricToBenchmarkMap.push(data); + } + + test('instantiate', function() { + const mv = document.createElement('tr-v-ui-metrics-visualization'); + this.addHTMLOutput(mv); + + const testMetrics = tr.v.ui.METRICS.get('Thread'); + + // generate aggregate chart + const aggregateChart = []; + for (let i = 1; i <= 5; i++) { + aggregateChart.push(generateChartBar(testMetrics, + 'Run ' + i, 'aggregate')); + } + + // generate chart with individual page metrics + const chartData = []; + for (let i = 1; i <= 5; i++) { + for (let j = 1; j <= 5; j++) { + chartData.push(generateChartBar(testMetrics, + 'Run ' + i, 'Page ' + j)); + } + } + + // generate submetrics + const submetricsData = new Map(); + for (const metric in testMetrics) { + const testSubmetrics = [metric + 'a', metric + 'b', metric + 'c']; + for (let i = 1; i <= 5; i++) { + const page = 'Page ' + i; + const pageToMetricMap = tr.v.ui.getValueFromMap(page, + submetricsData); + const metricToSubmetricMap = tr.v.ui.getValueFromMap(metric, + pageToMetricMap); + for (let j = 1; j <= 5; j++) { + for (const submetric in testSubmetrics) { + generateSubmetricBar(submetric, 'Run ' + j, page, + metricToSubmetricMap); + } + } + } + } + + mv.build({ + title: 'Thread', + aggregate: aggregateChart, + page: chartData, + submetrics: submetricsData + }); + }); +}); +</script>
\ No newline at end of file diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html new file mode 100644 index 00000000000..b546041de50 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/unit.html"> + +<script> + 'use strict'; + Polymer({ + is: 'tr-v-ui-preferred-display-unit', + + ready() { + this.preferredTimeDisplayMode_ = undefined; + }, + + attached() { + tr.b.Unit.didPreferredTimeDisplayUnitChange(); + }, + + detached() { + tr.b.Unit.didPreferredTimeDisplayUnitChange(); + }, + + // null means no-preference + get preferredTimeDisplayMode() { + return this.preferredTimeDisplayMode_; + }, + + set preferredTimeDisplayMode(v) { + if (this.preferredTimeDisplayMode_ === v) return; + this.preferredTimeDisplayMode_ = v; + tr.b.Unit.didPreferredTimeDisplayUnitChange(); + } + + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html new file mode 100644 index 00000000000..382dc5963fe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/time_display_modes.html"> +<link rel="import" href="/tracing/value/ui/preferred_display_unit.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const unit = document.createElement('tr-v-ui-preferred-display-unit'); + const ms = tr.b.TimeDisplayModes.ms; + unit.preferredDisplayUnit = ms; + assert.strictEqual(unit.preferredDisplayUnit, ms); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html new file mode 100644 index 00000000000..d3253d8eff9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html @@ -0,0 +1,274 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 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. +--> +<dom-module id='tr-v-ui-raster-visualization'> + <template> + <style> + button { + padding: 5px; + font-size: 14px; + } + .error { + color: red; + display: none; + } + + .text_input { + width: 200px; + padding: 4px; + font-size: 14px; + } + + .selector_container{ + padding: 5px; + } + + #search { + display: inline-block; + padding-bottom: 10px; + } + + #search_page { + width: 200px; + } + + #pageSelector { + display: inline-block; + font-size: 12pt; + } + + #close { + display: none; + vertical-align: top; + } + + #close svg{ + height: 1em; + } + + #close svg line { + stroke-width: 18; + stroke: black; + } + + #close:hover svg { + background: black; + } + + #close:hover svg line { + stroke: white; + } + </style> + <span id="aggregateContainer"> + <div> + <div class="selector_container"> + <span id="select_page_label">Individual Page Results:</span> + <select id="pageSelector"> + <option id="select_page" value="">Select a page</option> + </select> + </div> + <div class="selector_container"> + <div id="search_page_label">Search for a page:</div> + <input id="search_page" class="text_input" placeholder="Page Name"> + <button id="search_button">Search</button> + <div id="search_error" class="error">Sorry, could not find that page!</div> + </div> + </div> + </span> + <span id="pageContainer"> + <span id="close"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </span> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-v-ui-raster-visualization', + + ready() { + this.$.pageSelector.addEventListener ('click', () => { + this.selectPage_(); + }); + + this.$.search_page.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.searchByPage_(); + }); + + this.$.search_button.addEventListener ('click', () => { + this.searchByPage_(); + }); + }, + + + build(chartData) { + this.data_ = chartData; + const aggregateChart = this.createChart_('Aggregate Data by Run'); + Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart); + aggregateChart.enableToolTip = true; + aggregateChart.toolTipCallBack = (rect) => + this.openBenchmarkChart_(rect); + this.setChartColors_(aggregateChart, this.data_.get(tr.v.ui.AGGREGATE_KEY)); + aggregateChart.data = this.data_.get(tr.v.ui.AGGREGATE_KEY); + this.setChartSize_(aggregateChart, + this.data_.get(tr.v.ui.AGGREGATE_KEY).length); + + for (const page of this.data_.keys()) { + if (page === tr.v.ui.AGGREGATE_KEY) continue; + const option = document.createElement('option'); + option.textContent = page; + option.value = page; + this.$.pageSelector.appendChild(option); + } + }, + + setChartSize_(chart, pageCount, dataLength) { + chart.graphHeight = tr.b.math.clamp(pageCount * 25, 175, 1000); + chart.graphWidth = tr.b.math.clamp(pageCount * 25, 500, 1000); + }, + + setChartColors_(chart, data) { + const metrics = new Map(); + let count = 0; + for (const thread of tr.v.ui.FRAME.values()) { + for (const metric of thread.keys()) { + metrics.set(metric, count); + count++; + } + } + for (let i = 0; i < Math.floor(data.length / tr.v.ui.FRAME.length); ++i) { + let j = 0; + for (const [threadName, thread] of tr.v.ui.FRAME.entries()) { + for (const metric of thread.keys()) { + let color = 'transparent'; + if (thread.get(metric)) { + const mainColorIndex = metrics.get(metric) % tr.v.ui.COLORS.length; + const subColorIndex = i % tr.v.ui.COLORS[mainColorIndex].length; + color = tr.v.ui.COLORS[mainColorIndex][subColorIndex]; + } + const series = metric + '-' + data[i * 2 + j].x + '-' + threadName; + chart.getDataSeries(series).color = color; + chart.getDataSeries(series).title = !i ? metric : ''; + } + j++; + } + } + }, + + createChart_(title) { + const newChart = new tr.ui.b.NameBarChart(); + newChart.chartTitle = title; + newChart.xAxisLabel = 'ms'; + newChart.hideLegend = false; + newChart.showTitleInLegend = true; + newChart.hideYAxis = true; + newChart.isStacked = true; + newChart.displayXInHover = true; + newChart.isGrouped = true; + return newChart; + }, + + openBenchmarkChart_(rect) { + // Find main metric and corresponding sub-metrics + const benchmarkIndex = Math.floor(rect.index / tr.v.ui.FRAME.length); + const title = rect.datum.x; + + // Create child chart with breakdown data + const div = document.createElement('div'); + Polymer.dom(this.$.pageContainer). + insertBefore(div, this.$.pageContainer.firstChild); + + const chart = this.createChart_(title); + + div.appendChild(chart); + const button = this.initializeCloseButton_(div, this.$.pageContainer); + div.appendChild(button); + + const newDataSet = []; + + for (const page of this.data_.keys()) { + if (page === tr.v.ui.AGGREGATE_KEY) continue; + for (let i = 0; i < tr.v.ui.FRAME.length; i++) { + newDataSet.push(this.data_ + .get(page)[benchmarkIndex * tr.v.ui.FRAME.length + i]); + } + } + + this.setChartColors_(chart, newDataSet); + chart.data = newDataSet; + this.setChartSize_(chart, newDataSet.length); + }, + + selectPage_() { + // Create child chart with breakdown data + const div = document.createElement('div'); + const page = this.$.pageSelector.value; + if (page === '') return; + Polymer.dom(this.$.pageContainer). + insertBefore(div, this.$.pageContainer.firstChild); + + const pageChart = this.createChart_(page); + + div.appendChild(pageChart); + const button = this.initializeCloseButton_(div, this.$.pageContainer); + div.appendChild(button); + + const pageData = this.data_.get(page); + + this.setChartColors_(pageChart, pageData); + pageChart.data = pageData; + this.setChartSize_(pageChart, pageData.length); + }, + + searchByPage_() { + const criteria = this.$.search_page.value; + if (criteria === '') return; + + const query = new RegExp(criteria); + + const filteredData = [...this.data_.keys()] + .filter(page => page.match(query)); + + if (filteredData.length < 1) { + this.$.search_error.style.display = 'block'; + return; + } + + // Create child chart with breakdown data + const page = filteredData[0]; + + const div = document.createElement('div'); + Polymer.dom(this.$.pageContainer). + insertBefore(div, this.$.pageContainer.firstChild); + + const pageChart = this.createChart_(page); + + div.appendChild(pageChart); + const button = this.initializeCloseButton_(div, this.$.pageContainer); + div.appendChild(button); + + const pageData = this.data_.get(page); + + this.setChartColors_(pageChart, pageData); + pageChart.data = pageData; + this.setChartSize_(pageChart, pageData.length); + }, + + initializeCloseButton_(div, parent) { + const button = this.$.close.cloneNode(true); + button.style.display = 'inline-block'; + button.addEventListener('click', () => { + Polymer.dom(parent).removeChild(div); + }); + return button; + }, +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html new file mode 100644 index 00000000000..eaf3ee61842 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 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/value/ui/raster_visualization.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function generateBars(page, benchmark) { + const benchmarkData = []; + for (const [threadName, thread] of tr.v.ui.FRAME.entries()) { + const data = {x: benchmark, hide: 0}; + if (page !== tr.v.ui.AGGREGATE_KEY) data.group = page; + for (const metric of thread.keys()) { + const key = metric + '-' + data.x + '-' + threadName; + const mean = Math.random() * 100; + data[key] = Math.round(mean * 100) / 100; + } + benchmarkData.push(data); + } + return benchmarkData; + } + + test('instantiate', function() { + const rv = document.createElement('tr-v-ui-raster-visualization'); + this.addHTMLOutput(rv); + + const allChartData = new Map(); + + // generate aggregate data + let aggregateData = []; + for (let i = 1; i <= 5; i++) { + aggregateData = aggregateData.concat(generateBars(tr.v.ui.AGGREGATE_KEY, + 'Run ' + i)); + } + allChartData.set(tr.v.ui.AGGREGATE_KEY, aggregateData); + + // generate data per page + for (let i = 1; i <= 5; i++) { + const page = 'Page ' + i; + let chartData = []; + for (let j = 1; j <= 5; j++) { + chartData = chartData.concat(generateBars(page, 'Run ' + j)); + } + allChartData.set(page, chartData); + } + + rv.build(allChartData); + }); +}); +</script>
\ No newline at end of file diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html new file mode 100644 index 00000000000..ac52e51aba2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html @@ -0,0 +1,40 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-related-event-set-span"> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-related-event-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + Polymer.dom(this).textContent = ''; + const events = new tr.model.EventSet([...this.diagnostic]); + const link = document.createElement('tr-ui-a-analysis-link'); + let label = events.length + ' events'; + if (events.length === 1) { + const event = tr.b.getOnlyElement(events); + label = event.title + ' '; + label += tr.b.Unit.byName.timeDurationInMs.format( + event.duration); + } + link.setSelectionAndContent(events, label); + Polymer.dom(this).appendChild(link); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html new file mode 100644 index 00000000000..b0058a3c97b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html @@ -0,0 +1,58 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/related_event_set.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_RelatedEventSet0', function() { + const diagnostic = new tr.v.d.RelatedEventSet(); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + assert.strictEqual('0 events', span.textContent); + }); + + test('instantiate_RelatedEventSet1', function() { + const diagnostic = new tr.v.d.RelatedEventSet(); + tr.c.TestUtils.newModel(function(model) { + const proc = model.getOrCreateProcess(1); + const thread = proc.getOrCreateThread(2); + const event = tr.c.TestUtils.newSliceEx( + {title: 'a', start: 0, duration: 1}); + thread.sliceGroup.pushSlice(event); + diagnostic.add(event); + }); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + assert.strictEqual('a 1.000 ms', span.textContent); + }); + + test('instantiate_RelatedEventSet2', function() { + const diagnostic = new tr.v.d.RelatedEventSet(); + tr.c.TestUtils.newModel(function(model) { + const proc = model.getOrCreateProcess(1); + const thread = proc.getOrCreateThread(2); + let event = tr.c.TestUtils.newSliceEx({start: 0, duration: 1}); + thread.sliceGroup.pushSlice(event); + diagnostic.add(event); + event = tr.c.TestUtils.newSliceEx({start: 1, duration: 1}); + thread.sliceGroup.pushSlice(event); + diagnostic.add(event); + }); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + assert.strictEqual('2 events', span.textContent); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html new file mode 100644 index 00000000000..ac71cd7a1d7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html @@ -0,0 +1,204 @@ +<!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/event.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/raf.html"> + +<dom-module id="tr-v-ui-scalar-context-controller"> + <template></template> +</dom-module> + +<!-- +@fileoverview Polymer element for controlling common context across scalar +spans. To facilitate multiple separate contexts (e.g. a separate context for +each table column), each scalar span has to specify which "context group" +it belongs to: + + +============ some container element (e.g. <div>) ============+ + | | + | <tr-v-ui-scalar-context-controller> | + | ^ ^ | + | | | | + | v v | + | .... Context group 1 .... .... Context group 2 .... | + | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : | + | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : . . . | + | : . . . : : . . . : | + | :.......................: :.......................: | + +=============================================================+ + +An element can find its enclosing context controller using the +getScalarContextControllerForElement(node) defined in this file. Scalar spans +can push their state to the controller using the following three methods: + + 1. onScalarSpanAdded(contextGroup, span) + This method should be called when a span is attached to the DOM tree (or + afterwards when added to a context group). + + 2. onScalarSpanRemoved(contextGroup, span) + This method should be called when a span is detached from the DOM tree (or + beforehand when removed from a context group). + + 3. onScalarSpanUpdated(contextGroup, span) + This method should be called when the value of a span changes. + +Note: If a span wants to change its context group, it should first call +onScalarSpanRemoved with the old group and then onScalarSpanAdded with the new +group. + +If one or more group contexts are modified (due to one of the three methods +above), the controller will asynchronously (at the next RAF) update them and +fire a 'context-updated' event. Scalar spans can listen for this event and +update their UI accordingly. + +The context currently consists of the range of values of the associated spans. +This allows automatic display of relative sizes using sparklines. + +The controller design is based on: +https://docs.google.com/document/d/16ih8yYK8kF8MMlPnB-5KlyfS_AjjtbyAfi3pkxoZ8xs/edit?usp=sharing +--> +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-scalar-context-controller', + + created() { + this.host_ = undefined; + this.groupToContext_ = new Map(); + this.dirtyGroups_ = new Set(); + }, + + attached() { + if (this.host_) { + throw new Error( + 'Scalar context controller is already attached to a host'); + } + + const host = findParentOrHost(this); + if (host.__scalarContextController) { + throw new Error( + 'Multiple scalar context controllers attached to this host'); + } + + host.__scalarContextController = this; + this.host_ = host; + }, + + detached() { + if (!this.host_) { + throw new Error('Scalar context controller is not attached to a host'); + } + if (this.host_.__scalarContextController !== this) { + throw new Error( + 'Scalar context controller is not attached to its host'); + } + + delete this.host_.__scalarContextController; + this.host_ = undefined; + }, + + getContext(group) { + return this.groupToContext_.get(group); + }, + + onScalarSpanAdded(group, span) { + let context = this.groupToContext_.get(group); + if (context === undefined) { + context = { + spans: new Set(), + range: new tr.b.math.Range() + }; + this.groupToContext_.set(group, context); + } + if (context.spans.has(span)) { + throw new Error('Scalar span already registered with group: ' + group); + } + context.spans.add(span); + this.markGroupDirtyAndScheduleUpdate_(group); + }, + + onScalarSpanRemoved(group, span) { + const context = this.groupToContext_.get(group); + if (!context.spans.has(span)) { + throw new Error('Scalar span not registered with group: ' + group); + } + context.spans.delete(span); + this.markGroupDirtyAndScheduleUpdate_(group); + }, + + onScalarSpanUpdated(group, span) { + const context = this.groupToContext_.get(group); + if (!context.spans.has(span)) { + throw new Error('Scalar span not registered with group: ' + group); + } + this.markGroupDirtyAndScheduleUpdate_(group); + }, + + markGroupDirtyAndScheduleUpdate_(group) { + const alreadyDirty = this.dirtyGroups_.size > 0; + this.dirtyGroups_.add(group); + if (!alreadyDirty) { + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContext, this); + } + }, + + updateContext() { + const groups = this.dirtyGroups_; + if (groups.size === 0) return; + this.dirtyGroups_ = new Set(); + + for (const group of groups) { + this.updateGroup_(group); + } + + const event = new tr.b.Event('context-updated'); + event.groups = groups; + this.dispatchEvent(event); + }, + + updateGroup_(group) { + const context = this.groupToContext_.get(group); + if (context.spans.size === 0) { + this.groupToContext_.delete(group); + return; + } + context.range.reset(); + for (const span of context.spans) { + context.range.addValue(span.value); + } + } + }); + + function getScalarContextControllerForElement(element) { + while (element) { + if (element.__scalarContextController) { + return element.__scalarContextController; + } + element = findParentOrHost(element); + } + return undefined; + } + + function findParentOrHost(node) { + if (node.parentElement) { + return node.parentElement; + } + while (Polymer.dom(node).parentNode) { + node = Polymer.dom(node).parentNode; + } + return node.host; + } + + return { + getScalarContextControllerForElement, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html new file mode 100644 index 00000000000..ccb3f61d6f4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html @@ -0,0 +1,312 @@ +<!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/raf.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> + +<dom-module id="tr-v-ui-scalar-context-controller-mock-host"> + <template> + <tr-v-ui-scalar-context-controller id="controller"> + </tr-v-ui-scalar-context-controller> + <content></content> + </template> +</dom-module> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const getScalarContextControllerForElement = + tr.v.ui.getScalarContextControllerForElement; + + Polymer({ + is: 'tr-v-ui-scalar-context-controller-mock-host' + }); + + test('getScalarContextControllerForElement', function() { + const root = document.createElement('div'); + Polymer.dom(document.body).appendChild(root); + try { + assert.isUndefined(getScalarContextControllerForElement(root)); + + // <div> root + // |__<div> host1 + // |__<tr-v-ui-scalar-context-controller> c1 + const host1 = document.createElement('div'); + Polymer.dom(root).appendChild(host1); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1)); + const c1 = document.createElement('tr-v-ui-scalar-context-controller'); + Polymer.dom(host1).appendChild(c1); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + + // <div> root + // |__<div> host1 + // | |__<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // :..<tr-v-ui-scalar-context-controller> c2 + const host2 = document.createElement( + 'tr-v-ui-scalar-context-controller-mock-host'); + const c2 = host2.$.controller; + Polymer.dom(root).appendChild(host2); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + + // <div> root + // |__<div> host1 + // | |__<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :..<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // |__<div> divB + const divA = document.createElement('div'); + Polymer.dom(host2).appendChild(divA); + assert.strictEqual(getScalarContextControllerForElement(divA), c2); + const divB = document.createElement('div'); + Polymer.dom(divA).appendChild(divB); + assert.strictEqual(getScalarContextControllerForElement(divB), c2); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :.-<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + // :..<tr-v-ui-scalar-context-controller> c3 + const host3 = document.createElement( + 'tr-v-ui-scalar-context-controller-mock-host'); + Polymer.dom(divB).appendChild(host3); + const c3 = host3.$.controller; + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + assert.strictEqual(getScalarContextControllerForElement(divA), c2); + assert.strictEqual(getScalarContextControllerForElement(divB), c2); + assert.strictEqual(getScalarContextControllerForElement(host3), c3); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :.-<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + // :..<tr-v-ui-scalar-context-controller> c3 + const c4 = document.createElement('tr-v-ui-scalar-context-controller'); + Polymer.dom(divA).appendChild(c4); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c3); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :.-<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + Polymer.dom(host3.root).removeChild(c3); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c4); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + Polymer.dom(host2.root).removeChild(c2); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.isUndefined(getScalarContextControllerForElement(host2)); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c4); + + // <div> root + // | :.<tr-v-ui-scalar-context-controller> c3 + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + Polymer.dom(root).appendChild(c3); + assert.strictEqual(getScalarContextControllerForElement(root), c3); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c3); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c4); + } finally { + Polymer.dom(document.body).removeChild(root); + } + }); + + function contextTest(name, testCallback) { + test('context_' + name, function() { + const root = document.createElement('div'); + Polymer.dom(document.body).appendChild(root); + try { + const c = document.createElement('tr-v-ui-scalar-context-controller'); + Polymer.dom(root).appendChild(c); + + let updatedGroups = []; // Fail if event fires unexpectedly. + c.addEventListener('context-updated', function(e) { + if (updatedGroups) { + assert.fail('Unexpected context-updated event fired.'); + } + updatedGroups = Array.from(e.groups); + }); + + c.expectContextUpdatedEventForTesting = + function(expectedUpdatedGroups) { + updatedGroups = undefined; + tr.b.forceAllPendingTasksToRunForTest(); + assert.sameMembers(updatedGroups, expectedUpdatedGroups); + }; + + testCallback.call(this, c); + } finally { + Polymer.dom(document.body).removeChild(root); + } + }); + } + + contextTest('singleGroup', function(c) { + assert.isUndefined(c.getContext('G')); + + const s1 = {value: 10}; + c.onScalarSpanAdded('G', s1); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(10, 10))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1]); + + const s2 = {value: 15}; + c.onScalarSpanAdded('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(10, 15))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]); + + s1.value = 5; + c.onScalarSpanUpdated('G', s1); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(5, 15))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]); + + c.onScalarSpanRemoved('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(5, 5))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1]); + + const s3 = {value: 0}; + c.onScalarSpanAdded('G', s3); + s2.value = 14; + c.onScalarSpanAdded('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(0, 14))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2, s3]); + + c.onScalarSpanRemoved('G', s1); + c.onScalarSpanRemoved('G', s2); + c.onScalarSpanRemoved('G', s3); + c.expectContextUpdatedEventForTesting(['G']); + assert.isUndefined(c.getContext('G')); + + c.onScalarSpanAdded('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(14, 14))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s2]); + }); + + contextTest('multipleGroups', function(c) { + assert.isUndefined(c.getContext('G1')); + assert.isUndefined(c.getContext('G2')); + + const s1 = {value: 0}; + c.onScalarSpanAdded('G1', s1); + c.expectContextUpdatedEventForTesting(['G1']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(0, 0))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1]); + + const s2 = {value: 1}; + c.onScalarSpanAdded('G2', s2); + c.expectContextUpdatedEventForTesting(['G2']); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 1))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]); + + const s3 = {value: 2}; + const s4 = {value: -1}; + c.onScalarSpanAdded('G2', s3); + c.onScalarSpanAdded('G1', s4); + c.expectContextUpdatedEventForTesting(['G1', 'G2']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(-1, 0))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s4]); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 2))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2, s3]); + + c.onScalarSpanRemoved('G2', s3); + c.onScalarSpanAdded('G1', s3); + c.expectContextUpdatedEventForTesting(['G1', 'G2']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(-1, 2))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 1))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]); + + s4.value = 3; + c.onScalarSpanUpdated('G1', s4); + s1.value = 1; + c.onScalarSpanUpdated('G1', s1); + c.expectContextUpdatedEventForTesting(['G1']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(1, 3))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 1))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]); + + c.onScalarSpanRemoved('G2', s2); + c.expectContextUpdatedEventForTesting(['G2']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(1, 3))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]); + assert.isUndefined(c.getContext('G2')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html new file mode 100644 index 00000000000..631c3696f8c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html @@ -0,0 +1,32 @@ +<!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/value/ui/diagnostic_span_behavior.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id="tr-v-ui-scalar-diagnostic-span"> + <template> + <tr-v-ui-scalar-span id="scalar"></tr-v-ui-scalar-span> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-scalar-diagnostic-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + this.$.scalar.setValueAndUnit(this.diagnostic.value.value, + this.diagnostic.value.unit); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html new file mode 100644 index 00000000000..9f6167f1acb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html @@ -0,0 +1,23 @@ +<!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/value/diagnostics/scalar.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const diagnostic = new tr.v.d.Scalar(new tr.b.Scalar( + tr.b.Unit.byName.timeDurationInMs, 123.456)); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-SCALAR-DIAGNOSTIC-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html new file mode 100644 index 00000000000..24a5cf3ac0f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html @@ -0,0 +1,89 @@ +<!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/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id="tr-v-ui-scalar-map-table"> + <template> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-v-ui-scalar-map-table', + + created() { + /** @type {!Map.<string, !tr.b.Scalar>} */ + this.scalarMap_ = new Map(); + + /** @type {!Map.<string, !tr.b.math.Statistics.Significance>} */ + this.significance_ = new Map(); + }, + + ready() { + this.$.table.showHeader = false; + this.$.table.tableColumns = [ + { + value(row) { + return row.name; + } + }, + { + value(row) { + const span = tr.v.ui.createScalarSpan(row.value); + if (row.significance !== undefined) { + span.significance = row.significance; + } else if (row.anyRowsHaveSignificance) { + // Ensure vertical alignment. + span.style.marginRight = '18px'; + } + span.style.whiteSpace = 'nowrap'; + return span; + } + } + ]; + }, + + get scalarMap() { + return this.scalarMap_; + }, + + /** + * @param {!Map.<string,!tr.b.Scalar>} map + */ + set scalarMap(map) { + this.scalarMap_ = map; + this.updateContents_(); + }, + + /** + * @param {string} key + * @param {!tr.b.math.Statistics.Significance} significance + */ + setSignificanceForKey(key, significance) { + this.significance_.set(key, significance); + this.updateContents_(); + }, + + updateContents_() { + const rows = []; + for (const [key, scalar] of this.scalarMap) { + rows.push({ + name: key, + value: scalar, + significance: this.significance_.get(key), + anyRowsHaveSignificance: (this.significance_.size > 0) + }); + } + this.$.table.tableRows = rows; + this.$.table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html new file mode 100644 index 00000000000..4c9a50c2314 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html @@ -0,0 +1,30 @@ +<!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/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/scalar_map_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const span = document.createElement('tr-v-ui-scalar-map-table'); + + const histogram = new tr.v.Histogram('', tr.b.Unit.byName.energyInJoules); + for (let i = 0; i < 1e2; ++i) { + histogram.addSample(Math.random() * 1000); + } + + histogram.addSample('foo'); + histogram.customizeSummaryOptions({nans: true}); + + span.scalarMap = histogram.statisticsScalars; + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html new file mode 100644 index 00000000000..50d89653ed1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html @@ -0,0 +1,626 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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/unit.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + /** + * One common simple way to use this function is + * createScalarSpan(number, {unit: tr.b.Unit.byName.whatever}) + * + * This function can also take a Scalar, undefined, or a Histogram plus + * significance, contextGroup, customContextRange, leftAlign and/or inline. + * + * @param {undefined|tr.b.Scalar|tr.v.Histogram} value + * @param {Object=} opt_config + * @param {!tr.b.math.Range=} opt_config.customContextRange + * @param {boolean=} opt_config.leftAlign + * @param {boolean=} opt_config.inline + * @param {!tr.b.Unit=} opt_config.unit + * @param {tr.b.math.Statistics.Significance=} opt_config.significance + * @param {string=} opt_config.contextGroup + * @return {(string|!HTMLElement)} + */ + function createScalarSpan(value, opt_config) { + if (value === undefined) return ''; + + const config = opt_config || {}; + const ownerDocument = config.ownerDocument || document; + + const span = ownerDocument.createElement('tr-v-ui-scalar-span'); + + let numericValue; + if (value instanceof tr.b.Scalar) { + span.value = value; + numericValue = value.value; + } else if (value instanceof tr.v.Histogram) { + numericValue = value.average; + if (numericValue === undefined) return ''; + span.setValueAndUnit(numericValue, value.unit); + } else { + const unit = config.unit; + if (unit === undefined) { + throw new Error( + 'Unit must be provided in config when value is a number'); + } + span.setValueAndUnit(value, unit); + numericValue = value; + } + + if (config.context) { + span.context = config.context; + } + + if (config.customContextRange) { + span.customContextRange = config.customContextRange; + } + + if (config.leftAlign) { + span.leftAlign = true; + } + + if (config.inline) { + span.inline = true; + } + + if (config.significance !== undefined) { + span.significance = config.significance; + } + + if (config.contextGroup !== undefined) { + span.contextGroup = config.contextGroup; + } + + return span; + } + + return { + createScalarSpan, + }; +}); +</script> + +<dom-module id="tr-v-ui-scalar-span"> + <template> + <style> + :host { + display: flex; + flex-direction: row; + justify-content: flex-end; + position: relative; + /* Limit the sparkline's negative z-index to the span only. */ + isolation: isolate; + } + + :host(.left-align) { + justify-content: flex-start; + } + + :host(.inline) { + display: inline-flex; + } + + #sparkline { + width: 0%; + position: absolute; + bottom: 0; + display: none; + height: 100%; + background-color: hsla(216, 100%, 94.5%, .75); + border-color: hsl(216, 100%, 89%); + box-sizing: border-box; + z-index: -1; + } + #sparkline.positive { + border-right-style: solid; + /* The border width must be kept in sync with buildSparklineStyle_(). */ + border-right-width: 1px; + } + #sparkline:not(.positive) { + border-left-style: solid; + /* The border width must be kept in sync with buildSparklineStyle_(). */ + border-left-width: 1px; + } + #sparkline.better { + background-color: hsla(115, 100%, 93%, .75); + border-color: hsl(118, 60%, 80%); + } + #sparkline.worse { + background-color: hsla(0, 100%, 88%, .75); + border-color: hsl(0, 100%, 80%); + } + + #content { + white-space: nowrap; + } + #content, #significance, #warning { + flex-grow: 0; + } + #content.better { + color: green; + } + #content.worse { + color: red; + } + + #significance svg { + margin-left: 4px; + display: none; + height: 1em; + vertical-align: text-top; + stroke-width: 4; + fill: rgba(0, 0, 0, 0); + } + #significance #insignificant { + stroke: black; + } + #significance #significantly_better { + stroke: green; + } + #significance #significantly_worse { + stroke: red; + } + + #warning { + display: none; + margin-left: 4px; + height: 1em; + vertical-align: text-top; + stroke-width: 0; + } + #warning path { + fill: rgb(255, 185, 185); + } + #warning rect { + fill: red; + } + </style> + + <span id="sparkline"></span> + + <span id="content"></span> + + <span id="significance"> + <!-- Neutral face --> + <svg viewbox="0 0 128 128" id="insignificant"> + <circle r="60" cx="64" cy="64"/> + <circle r="4" cx="44" cy="44"/> + <circle r="4" cx="84" cy="44"/> + <line x1="36" x2="92" y1="80" y2="80"/> + </svg> + + <!-- Smiling face --> + <svg viewbox="0 0 128 128" id="significantly_better"> + <circle r="60" cx="64" cy="64"/> + <circle r="4" cx="44" cy="44"/> + <circle r="4" cx="84" cy="44"/> + <path d="M 28 64 Q 64 128 100 64"/> + </svg> + + <!-- Frowning face --> + <svg viewbox="0 0 128 128" id="significantly_worse"> + <circle r="60" cx="64" cy="64"/> + <circle r="4" cx="44" cy="44"/> + <circle r="4" cx="84" cy="44"/> + <path d="M 36 96 Q 64 48 92 96"/> + </svg> + </span> + + <svg viewbox="0 0 128 128" id="warning"> + <path d="M 64 0 L 128 128 L 0 128 L 64 0"/> + <rect x="60" width="8" y="0" height="84"/> + <rect x="60" width="8" y="100" height="24"/> + </svg> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-v-ui-scalar-span', + + properties: { + /** + * String identifier for grouping scalar spans with common context (e.g. + * all scalar spans in a single table column would typically share a common + * context and, thus, have the same context group identifier). If falsy, + * the scalar span will NOT be associated with any context. + */ + contextGroup: { + type: String, + reflectToAttribute: true, + observer: 'contextGroupChanged_' + } + }, + + created() { + this.value_ = undefined; + this.unit_ = undefined; + + // TODO(petrcermak): Merge this into the context controller. + this.context_ = undefined; + + this.warning_ = undefined; + this.significance_ = tr.b.math.Statistics.Significance.DONT_CARE; + + // To avoid unnecessary DOM traversal, search for the context controller + // only when necessary (when the span is attached and has a context group). + this.shouldSearchForContextController_ = false; + this.lazyContextController_ = undefined; + this.onContextUpdated_ = this.onContextUpdated_.bind(this); + this.updateContents_ = this.updateContents_.bind(this); + + // The span can specify a custom context range, which will override the + // values from the context controller. + this.customContextRange_ = undefined; + }, + + get significance() { + return this.significance_; + }, + + set significance(s) { + this.significance_ = s; + this.updateContents_(); + }, + + set contentTextDecoration(deco) { + this.$.content.style.textDecoration = deco; + }, + + get value() { + return this.value_; + }, + + set value(value) { + if (value instanceof tr.b.Scalar) { + this.value_ = value.value; + this.unit_ = value.unit; + } else { + this.value_ = value; + } + this.updateContents_(); + if (this.hasContext_(this.contextGroup)) { + this.contextController_.onScalarSpanUpdated(this.contextGroup, this); + } else { + this.updateSparkline_(); + } + }, + + get contextController_() { + if (this.shouldSearchForContextController_) { + this.lazyContextController_ = + tr.v.ui.getScalarContextControllerForElement(this); + this.shouldSearchForContextController_ = false; + } + return this.lazyContextController_; + }, + + hasContext_(contextGroup) { + // The ordering here is important. It ensures that we avoid a DOM traversal + // when the span doesn't have a context group. + return !!(contextGroup && this.contextController_); + }, + + contextGroupChanged_(newContextGroup, oldContextGroup) { + this.detachFromContextControllerIfPossible_(oldContextGroup); + if (!this.attachToContextControllerIfPossible_(newContextGroup)) { + // If the span failed to attach to a controller, it won't receive a + // context-updated event, so we trigger it manually. + this.onContextUpdated_(); + } + }, + + attachToContextControllerIfPossible_(contextGroup) { + if (!this.hasContext_(contextGroup)) return false; + + this.contextController_.addEventListener( + 'context-updated', this.onContextUpdated_); + this.contextController_.onScalarSpanAdded(contextGroup, this); + return true; + }, + + detachFromContextControllerIfPossible_(contextGroup) { + if (!this.hasContext_(contextGroup)) return; + + this.contextController_.removeEventListener( + 'context-updated', this.onContextUpdated_); + this.contextController_.onScalarSpanRemoved(contextGroup, this); + }, + + attached() { + tr.b.Unit.addEventListener( + 'display-mode-changed', this.updateContents_); + this.shouldSearchForContextController_ = true; + this.attachToContextControllerIfPossible_(this.contextGroup); + }, + + detached() { + tr.b.Unit.removeEventListener( + 'display-mode-changed', this.updateContents_); + this.detachFromContextControllerIfPossible_(this.contextGroup); + this.shouldSearchForContextController_ = false; + this.lazyContextController_ = undefined; + }, + + onContextUpdated_() { + this.updateSparkline_(); + }, + + get context() { + return this.context_; + }, + + set context(context) { + this.context_ = context; + this.updateContents_(); + }, + + get unit() { + return this.unit_; + }, + + set unit(unit) { + this.unit_ = unit; + this.updateContents_(); + this.updateSparkline_(); + }, + + setValueAndUnit(value, unit) { + this.value_ = value; + this.unit_ = unit; + this.updateContents_(); + }, + + get customContextRange() { + return this.customContextRange_; + }, + + set customContextRange(customContextRange) { + this.customContextRange_ = customContextRange; + this.updateSparkline_(); + }, + + get inline() { + return Polymer.dom(this).classList.contains('inline'); + }, + + set inline(inline) { + if (inline) { + Polymer.dom(this).classList.add('inline'); + } else { + Polymer.dom(this).classList.remove('inline'); + } + }, + + get leftAlign() { + return Polymer.dom(this).classList.contains('left-align'); + }, + + set leftAlign(leftAlign) { + if (leftAlign) { + Polymer.dom(this).classList.add('left-align'); + } else { + Polymer.dom(this).classList.remove('left-align'); + } + }, + + updateSparkline_() { + Polymer.dom(this.$.sparkline).classList.remove('positive'); + Polymer.dom(this.$.sparkline).classList.remove('better'); + Polymer.dom(this.$.sparkline).classList.remove('worse'); + Polymer.dom(this.$.sparkline).classList.remove('same'); + this.$.sparkline.style.display = 'none'; + this.$.sparkline.style.left = '0'; + this.$.sparkline.style.width = '0'; + + // Custom context range takes precedence over controller context range. + let range = this.customContextRange_; + if (!range && this.hasContext_(this.contextGroup)) { + const context = this.contextController_.getContext(this.contextGroup); + if (context) { + range = context.range; + } + } + if (!range || range.isEmpty) return; + + const leftPoint = Math.min(range.min, 0); + const rightPoint = Math.max(range.max, 0); + const pointDistance = rightPoint - leftPoint; + if (pointDistance === 0) { + // This can happen, for example, when all spans within the context have + // zero values (so |range| is [0, 0]). + return; + } + + // Draw the sparkline. + this.$.sparkline.style.display = 'block'; + let left; + let width; + if (this.value > 0) { + width = Math.min(this.value, rightPoint); + left = -leftPoint; + Polymer.dom(this.$.sparkline).classList.add('positive'); + } else if (this.value <= 0) { + width = -Math.max(this.value, leftPoint); + left = (-leftPoint) - width; + } + this.$.sparkline.style.left = this.buildSparklineStyle_( + left / pointDistance, false); + this.$.sparkline.style.width = this.buildSparklineStyle_( + width / pointDistance, true); + + // Set the sparkline color (if applicable). + const changeClass = this.changeClassName_; + if (changeClass) { + Polymer.dom(this.$.sparkline).classList.add(changeClass); + } + }, + + buildSparklineStyle_(ratio, isWidth) { + // To avoid visual glitches around the zero value bar, we subtract 1 pixel + // from the width of the element and multiply the remainder (100% - 1px) by + // |ratio|. The extra pixel is used for the sparkline border. This allows + // us to align zero sparklines with both positive and negative values: + // + // ::::::::::| +10 MiB + // :::::| +5 MiB + // | 0 MiB + // |::::: -5 MiB + // |:::::::::: -10 MiB + // + let position = 'calc(' + ratio + ' * (100% - 1px)'; + if (isWidth) { + position += ' + 1px'; // Extra pixel for sparkline border. + } + position += ')'; + return position; + }, + + updateContents_() { + Polymer.dom(this.$.content).textContent = ''; + Polymer.dom(this.$.content).classList.remove('better'); + Polymer.dom(this.$.content).classList.remove('worse'); + Polymer.dom(this.$.content).classList.remove('same'); + this.$.insignificant.style.display = ''; + this.$.significantly_better.style.display = ''; + this.$.significantly_worse.style.display = ''; + + if (this.unit_ === undefined) return; + + this.$.content.title = ''; + Polymer.dom(this.$.content).textContent = + this.unit_.format(this.value, this.context); + this.updateDelta_(); + }, + + updateDelta_() { + let changeClass = this.changeClassName_; + if (!changeClass) { + this.$.significance.style.display = 'none'; + return; // Not a delta or we don't care. + } + + this.$.significance.style.display = 'inline'; + + let title; + switch (changeClass) { + case 'better': + title = 'improvement'; + break; + + case 'worse': + title = 'regression'; + break; + + case 'same': + title = 'no change'; + break; + + default: + throw new Error('Unknown change class: ' + changeClass); + } + + // Set the content class separately from the significance class so that + // the Neutral face is always a neutral color. + Polymer.dom(this.$.content).classList.add(changeClass); + + switch (this.significance) { + case tr.b.math.Statistics.Significance.DONT_CARE: + break; + + case tr.b.math.Statistics.Significance.INSIGNIFICANT: + if (changeClass !== 'same') title = 'insignificant ' + title; + this.$.insignificant.style.display = 'inline'; + changeClass = 'same'; + break; + + case tr.b.math.Statistics.Significance.SIGNIFICANT: + if (changeClass === 'same') { + throw new Error('How can no change be significant?'); + } + this.$['significantly_' + changeClass].style.display = 'inline'; + title = 'significant ' + title; + break; + + default: + throw new Error('Unknown significance ' + this.significance); + } + + this.$.significance.title = title; + this.$.content.title = title; + }, + + get changeClassName_() { + if (!this.unit_ || !this.unit_.isDelta) return undefined; + + switch (this.unit_.improvementDirection) { + case tr.b.ImprovementDirection.DONT_CARE: + return undefined; + + case tr.b.ImprovementDirection.BIGGER_IS_BETTER: + if (this.value === 0) return 'same'; + return this.value > 0 ? 'better' : 'worse'; + + case tr.b.ImprovementDirection.SMALLER_IS_BETTER: + if (this.value === 0) return 'same'; + return this.value < 0 ? 'better' : 'worse'; + + default: + throw new Error('Unknown improvement direction: ' + + this.unit_.improvementDirection); + } + }, + + get warning() { + return this.warning_; + }, + + set warning(warning) { + this.warning_ = warning; + const warningEl = this.$.warning; + if (this.warning_) { + warningEl.title = warning; + warningEl.style.display = 'inline'; + } else { + warningEl.title = ''; + warningEl.style.display = ''; + } + }, + + // tr-v-ui-time-stamp-span property + get timestamp() { + return this.value; + }, + + set timestamp(timestamp) { + if (timestamp instanceof tr.b.u.TimeStamp) { + this.value = timestamp; + return; + } + this.setValueAndUnit(timestamp, tr.b.u.Units.timeStampInMs); + }, + + // tr-v-ui-time-duration-span property + get duration() { + return this.value; + }, + + set duration(duration) { + if (duration instanceof tr.b.u.TimeDuration) { + this.value = duration; + return; + } + this.setValueAndUnit(duration, tr.b.u.Units.timeDurationInMs); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html new file mode 100644 index 00000000000..56ca6917d71 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html @@ -0,0 +1,1027 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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/raf.html"> +<link rel="import" href="/tracing/base/time_display_modes.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Scalar = tr.b.Scalar; + const Unit = tr.b.Unit; + const THIS_DOC = document.currentScript.ownerDocument; + + const EXAMPLE_MEMORY_FORMATTING_CONTEXT = { + unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI, + }; + const EXAMPLE_MEMORY_NUMERIC = new Scalar( + Unit.byName.sizeInBytesDelta_smallerIsBetter, 256 * 1024 * 1024); + + function checkSignificance(span, expectedSignificance) { + assert.strictEqual(span.$.insignificant.style.display, + expectedSignificance === 'insignificant' ? 'inline' : ''); + assert.strictEqual(span.$.significantly_better.style.display, + expectedSignificance === 'significantly_better' ? 'inline' : ''); + assert.strictEqual(span.$.significantly_worse.style.display, + expectedSignificance === 'significantly_worse' ? 'inline' : ''); + } + + function checkScalarSpan(test, value, unit, expectedContent, opt_options) { + const options = opt_options || {}; + const span = tr.v.ui.createScalarSpan(new tr.b.Scalar(unit, value), + {significance: options.significance}); + + test.addHTMLOutput(span); + assert.strictEqual( + Polymer.dom(span.$.content).textContent, expectedContent); + assert.strictEqual(window.getComputedStyle(span.$.content).color, + options.expectedColor || 'rgb(0, 0, 0)'); + + if (options.expectedTitle) { + assert.strictEqual(span.$.content.title, options.expectedTitle); + } + + if (options.significance !== undefined) { + checkSignificance(span, options.expectedEmoji); + if (options.expectedTitle) { + assert.strictEqual(span.$.significance.title, options.expectedTitle); + } + } + } + + test('instantiate_significance', function() { + const countD = Unit.byName.count.correspondingDeltaUnit; + const countSIBD = Unit.byName.count_smallerIsBetter.correspondingDeltaUnit; + const countBIBD = Unit.byName.count_biggerIsBetter.correspondingDeltaUnit; + + const zero = String.fromCharCode(177) + '0'; + + checkScalarSpan(this, 0, countSIBD, zero, { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedTitle: 'no change', + expectedEmoji: '' + }); + + checkScalarSpan(this, 0, countSIBD, zero, { + expectedEmoji: 'insignificant', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'no change' + }); + + assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero, + {significance: tr.b.math.Statistics.Significance.SIGNIFICANT})); + + checkScalarSpan(this, 0, countBIBD, zero, { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedTitle: 'no change', + expectedEmoji: '' + }); + + checkScalarSpan(this, 0, countBIBD, zero, { + expectedEmoji: 'insignificant', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'no change' + }); + + assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero, + {significance: tr.b.math.Statistics.Significance.SIGNIFICANT})); + + checkScalarSpan(this, 1, countSIBD, '+1', { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedColor: 'rgb(255, 0, 0)', + expectedTitle: 'regression', + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countSIBD, '+1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant regression' + }); + + checkScalarSpan(this, 1, countSIBD, '+1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'significantly_worse', + expectedEmojiColor: 'rgb(255, 0, 0)', + expectedTitle: 'significant regression' + }); + + checkScalarSpan(this, 1, countBIBD, '+1', { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedColor: 'rgb(0, 128, 0)', + expectedTitle: 'improvement', + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countBIBD, '+1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant improvement' + }); + + checkScalarSpan(this, 1, countBIBD, '+1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'significantly_better', + expectedEmojiColor: 'rgb(0, 128, 0)', + expectedTitle: 'significant improvement' + }); + + checkScalarSpan(this, -1, countSIBD, '-1', { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: '', + expectedEmojiColor: '', + expectedTitle: 'improvement' + }); + + checkScalarSpan(this, -1, countSIBD, '-1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant improvement' + }); + + checkScalarSpan(this, -1, countSIBD, '-1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'significantly_better', + expectedEmojiColor: 'rgb(0, 128, 0)', + expectedTitle: 'significant improvement' + }); + + checkScalarSpan(this, -1, countBIBD, '-1', { + expectedColor: 'rgb(255, 0, 0)', + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countBIBD, '-1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant regression' + }); + + checkScalarSpan(this, -1, countBIBD, '-1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'significantly_worse', + expectedEmojiColor: 'rgb(255, 0, 0)', + expectedTitle: 'significant regression' + }); + + checkScalarSpan(this, 1, countD, '+1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countD, '+1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countD, '+1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countD, '-1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countD, '-1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countD, '-1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedEmoji: '' + }); + }); + + test('instantiate', function() { + checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMs, + '123.457 ms'); + checkScalarSpan(this, 0, Unit.byName.normalizedPercentage, '0.0%'); + checkScalarSpan(this, 1, Unit.byName.normalizedPercentage, '100.0%'); + checkScalarSpan(this, -2560, Unit.byName.sizeInBytes, '-2.5 KiB'); + }); + + test('instantiate_smallerIsBetter', function() { + checkScalarSpan(this, 45097156608, Unit.byName.sizeInBytes_smallerIsBetter, + '42.0 GiB'); + checkScalarSpan(this, 0, Unit.byName.energyInJoules_smallerIsBetter, + '0.000 J'); + checkScalarSpan(this, -0.25, Unit.byName.unitlessNumber_smallerIsBetter, + '-0.250'); + }); + + test('instantiate_biggerIsBetter', function() { + checkScalarSpan(this, 0.07, Unit.byName.powerInWatts_smallerIsBetter, + '70.000 mW'); + checkScalarSpan(this, 0, Unit.byName.timeStampInMs_biggerIsBetter, + '0.000 ms'); + checkScalarSpan(this, -0.003, + Unit.byName.normalizedPercentage_biggerIsBetter, '-0.3%'); + }); + + test('instantiate_delta', function() { + checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMsDelta, + '+123.457 ms'); + checkScalarSpan(this, 0, Unit.byName.normalizedPercentageDelta, + '\u00B10.0%'); + checkScalarSpan(this, -2560, Unit.byName.sizeInBytesDelta, + '-2.5 KiB'); + }); + + test('instantiate_delta_smallerIsBetter', function() { + checkScalarSpan(this, 45097156608, + Unit.byName.sizeInBytesDelta_smallerIsBetter, '+42.0 GiB', + {expectedColor: 'rgb(255, 0, 0)'}); + checkScalarSpan(this, 0, Unit.byName.energyInJoulesDelta_smallerIsBetter, + '\u00B10.000 J'); + checkScalarSpan(this, -0.25, + Unit.byName.unitlessNumberDelta_smallerIsBetter, '-0.250', + {expectedColor: 'rgb(0, 128, 0)'}); + }); + + test('instantiate_delta_biggerIsBetter', function() { + checkScalarSpan(this, 0.07, Unit.byName.powerInWattsDelta_biggerIsBetter, + '+70.000 mW', {expectedColor: 'rgb(0, 128, 0)'}); + checkScalarSpan(this, 0, Unit.byName.timeStampInMsDelta_biggerIsBetter, + '\u00B10.000 ms'); + checkScalarSpan(this, -0.003, + Unit.byName.normalizedPercentageDelta_biggerIsBetter, '-0.3%', + {expectedColor: 'rgb(255, 0, 0)'}); + }); + + test('createScalarSpan', function() { + // No config. + let span = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.powerInWatts, 3.14)); + assert.strictEqual(Polymer.dom(span.$.content).textContent, '3.140 W'); + assert.strictEqual(span.ownerDocument, document); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 3.14); + assert.strictEqual(span.unit, Unit.byName.powerInWatts); + assert.isUndefined(span.context); + assert.isUndefined(span.customContextRange); + assert.isUndefined(span.warning); + assert.isFalse(span.leftAlign); + this.addHTMLOutput(span); + + // Inline. + const div = document.createElement('div'); + this.addHTMLOutput(div); + const inlineSpan = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.powerInWatts, 3.14), + {inline: true}); + assert.strictEqual(Polymer.dom(inlineSpan.$.content).textContent, + '3.140 W'); + assert.strictEqual(inlineSpan.ownerDocument, document); + assert.strictEqual(inlineSpan.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(inlineSpan.value, 3.14); + assert.strictEqual(inlineSpan.unit, Unit.byName.powerInWatts); + assert.isUndefined(inlineSpan.context); + assert.isUndefined(inlineSpan.customContextRange); + assert.isUndefined(inlineSpan.warning); + assert.isFalse(inlineSpan.leftAlign); + div.appendChild(document.createTextNode('prefix ')); + div.appendChild(inlineSpan); + div.appendChild(document.createTextNode(' suffix')); + assert.isBelow(inlineSpan.getBoundingClientRect().width, + span.getBoundingClientRect().width); + + // Custom owner document and right align. + span = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.energyInJoules, 2.72), + { ownerDocument: THIS_DOC, leftAlign: true}); + assert.strictEqual(Polymer.dom(span.$.content).textContent, '2.720 J'); + assert.strictEqual(span.ownerDocument, THIS_DOC); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 2.72); + assert.strictEqual(span.unit, Unit.byName.energyInJoules); + assert.isUndefined(span.context); + assert.isUndefined(span.customContextRange); + assert.isUndefined(span.warning); + assert.isTrue(span.leftAlign); + this.addHTMLOutput(span); + + // Unit and sparkline set via config. + span = tr.v.ui.createScalarSpan(1.62, { + unit: Unit.byName.timeStampInMs, + customContextRange: tr.b.math.Range.fromExplicitRange(0, 3.24) + }); + assert.strictEqual(Polymer.dom(span.$.content).textContent, '1.620 ms'); + assert.strictEqual(span.ownerDocument, document); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 1.62); + assert.strictEqual(span.unit, Unit.byName.timeStampInMs); + assert.isUndefined(span.context); + assert.isTrue(tr.b.math.Range.fromExplicitRange(0, 3.24).equals( + span.customContextRange)); + assert.isUndefined(span.warning); + assert.isFalse(span.leftAlign); + this.addHTMLOutput(span); + + // Custom context. + span = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.sizeInBytesDelta_smallerIsBetter, + 256 * 1024 * 1024), { context: { + unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI, + minimumFractionDigits: 2 + } }); + assert.strictEqual( + Polymer.dom(span.$.content).textContent, '+262,144.00 KiB'); + assert.strictEqual(span.ownerDocument, document); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 256 * 1024 * 1024); + assert.strictEqual(span.unit, Unit.byName.sizeInBytesDelta_smallerIsBetter); + assert.deepEqual(span.context, { + unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI, + minimumFractionDigits: 2 + }); + assert.isUndefined(span.customContextRange); + assert.isUndefined(span.warning); + assert.isFalse(span.leftAlign); + this.addHTMLOutput(span); + }); + + test('instantiate_withWarning', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = 400000000; + span.unit = Unit.byName.sizeInBytes; + span.warning = 'There is a problem with this size'; + this.addHTMLOutput(span); + }); + + test('instantiate_withCustomContextRange', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = new Scalar(Unit.byName.unitlessNumber, 0.99); + span.customContextRange = tr.b.math.Range.fromExplicitRange(0, 3); + this.addHTMLOutput(span); + }); + + test('instantiate_withRightAlign', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = new Scalar(Unit.byName.timeStampInMs, 5.777); + span.leftAlign = true; + this.addHTMLOutput(span); + }); + + test('instantiate_withContext', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = new Scalar( + Unit.byName.unitlessNumberDelta_smallerIsBetter, 42); + span.context = { maximumFractionDigits: 2 }; + assert.strictEqual(Polymer.dom(span.$.content).textContent, '+42.00'); + this.addHTMLOutput(span); + }); + + test('deltaAndNonDeltaHaveSimilarHeights', function() { + const spanA = document.createElement('tr-v-ui-scalar-span'); + spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs); + checkSignificance(spanA, ''); + + const spanB = document.createElement('tr-v-ui-scalar-span'); + spanB.setValueAndUnit(400, Unit.byName.timeDurationInMsDelta); + checkSignificance(spanB, ''); + + const spanC = document.createElement('tr-v-ui-scalar-span'); + spanC.setValueAndUnit( + 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter); + spanC.significance = tr.b.math.Statistics.Significance.SIGNIFICANT; + checkSignificance(spanC, 'significantly_worse'); + + const spanD = document.createElement('tr-v-ui-scalar-span'); + spanD.setValueAndUnit( + 400, Unit.byName.timeDurationInMsDelta_biggerIsBetter); + spanD.significance = tr.b.math.Statistics.Significance.SIGNIFICANT; + checkSignificance(spanD, 'significantly_better'); + + const spanE = document.createElement('tr-v-ui-scalar-span'); + spanE.setValueAndUnit( + 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter); + spanE.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT; + checkSignificance(spanE, 'insignificant'); + + const overall = document.createElement('div'); + overall.style.display = 'flex'; + // These spans must be on separate lines so that Chrome has the option of + // making their heights different. The point of the test is that Chrome + // shouldn't have to make their heights different even when it could. + overall.style.flexDirection = 'column'; + Polymer.dom(overall).appendChild(spanA); + Polymer.dom(overall).appendChild(spanB); + Polymer.dom(overall).appendChild(spanC); + Polymer.dom(overall).appendChild(spanD); + Polymer.dom(overall).appendChild(spanE); + this.addHTMLOutput(overall); + + const expectedHeight = spanA.getBoundingClientRect().height; + assert.strictEqual(expectedHeight, spanB.getBoundingClientRect().height); + assert.strictEqual(expectedHeight, spanC.getBoundingClientRect().height); + assert.strictEqual(expectedHeight, spanD.getBoundingClientRect().height); + assert.strictEqual(expectedHeight, spanE.getBoundingClientRect().height); + }); + + test('warningAndNonWarningHaveSimilarHeights', function() { + const spanA = document.createElement('tr-v-ui-scalar-span'); + spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs); + + const spanB = document.createElement('tr-v-ui-scalar-span'); + spanB.setValueAndUnit(400, Unit.byName.timeDurationInMs); + spanB.warning = 'There is a problem with this time'; + + const overall = document.createElement('div'); + overall.style.display = 'flex'; + // These spans must be on separate lines so that Chrome has the option of + // making their heights different. The point of the test is that Chrome + // shouldn't have to make their heights different even when it could. + overall.style.flexDirection = 'column'; + Polymer.dom(overall).appendChild(spanA); + Polymer.dom(overall).appendChild(spanB); + this.addHTMLOutput(overall); + + const rectA = spanA.getBoundingClientRect(); + const rectB = spanB.getBoundingClientRect(); + assert.strictEqual(rectA.height, rectB.height); + }); + + test('respectCurrentDisplayUnit', function() { + try { + Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ns; + + const span = document.createElement('tr-v-ui-scalar-span'); + span.setValueAndUnit(73, Unit.byName.timeStampInMs); + this.addHTMLOutput(span); + + assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ns') > 0); + Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ms; + assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ms') > 0); + } finally { + Unit.reset(); + } + }); + + function checkSparkline(span, expectation) { + tr.b.forceAllPendingTasksToRunForTest(); + const sparklineEl = span.$.sparkline; + const computedStyle = getComputedStyle(sparklineEl); + + const expectedDisplay = expectation.display || 'block'; + assert.strictEqual(computedStyle.display, expectedDisplay); + if (expectedDisplay === 'none') { + // Test expectation sanity check. + assert.notProperty(expectation, 'left'); + assert.notProperty(expectation, 'width'); + assert.notProperty(expectation, 'classList'); + return; + } + + assert.closeTo(parseFloat(computedStyle.left), expectation.left, 0.1); + assert.closeTo(parseFloat(computedStyle.width), expectation.width, 0.1); + assert.sameMembers(Array.from(sparklineEl.classList), + expectation.classList || []); + } + + test('customContextRange', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + + // No custom context range. + const span1 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs + }); + Polymer.dom(div).appendChild(span1); + checkSparkline(span1, {display: 'none'}); + const span2 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs, + customContextRange: undefined + }); + Polymer.dom(div).appendChild(span2); + checkSparkline(span2, {display: 'none'}); + const span3 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs, + customContextRange: new tr.b.math.Range() // Empty range. + }); + Polymer.dom(div).appendChild(span3); + checkSparkline(span3, {display: 'none'}); + + const range = tr.b.math.Range.fromExplicitRange(-15, 15); + + // Values inside custom context range. + const span4 = tr.v.ui.createScalarSpan(-15, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span4); + checkSparkline(span4, {left: 0, width: 51}); + const span5 = tr.v.ui.createScalarSpan(-14, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span5); + checkSparkline(span5, {left: 3.33, width: 47.67}); + const span6 = tr.v.ui.createScalarSpan(-10, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span6); + checkSparkline(span6, {left: 16.67, width: 34.33}); + const span7 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span7); + checkSparkline(span7, {left: 50, width: 1}); + const span8 = tr.v.ui.createScalarSpan(10, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span8); + checkSparkline(span8, {left: 50, width: 34.33, classList: ['positive']}); + const span9 = tr.v.ui.createScalarSpan(14, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span9); + checkSparkline(span9, {left: 50, width: 47.67, classList: ['positive']}); + const span10 = tr.v.ui.createScalarSpan(15, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span10); + checkSparkline(span10, {left: 50, width: 51, classList: ['positive']}); + + // Values outside custom context range. + const span11 = tr.v.ui.createScalarSpan(-20, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span11); + checkSparkline(span11, {left: 0, width: 51}); + const span12 = tr.v.ui.createScalarSpan(20, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span12); + checkSparkline(span12, {left: 50, width: 51, classList: ['positive']}); + }); + + test('emptyNumeric', function() { + assert.strictEqual(tr.v.ui.createScalarSpan(), ''); + }); + + test('contextControllerChanges', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const s1 = tr.v.ui.createScalarSpan(10, { + unit: Unit.byName.powerInWatts + }); + Polymer.dom(div).appendChild(s1); + checkSparkline(s1, {display: 'none'}); + + const s2 = tr.v.ui.createScalarSpan(20, { + unit: Unit.byName.powerInWatts, + contextGroup: 'A' + }); + Polymer.dom(div).appendChild(s2); + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + + const s3 = tr.v.ui.createScalarSpan(30, { + unit: Unit.byName.powerInWatts, + contextGroup: 'A' + }); + Polymer.dom(div).appendChild(s3); + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + + const s4 = tr.v.ui.createScalarSpan(40, { + unit: Unit.byName.powerInWatts, + contextGroup: 'B' + }); + Polymer.dom(div).appendChild(s4); + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s3.contextGroup = 'B'; + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s1.setAttribute('context-group', 'A'); + checkSparkline(s1, {left: 0, width: 51, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s1.value = 50; + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 150); + checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s4.contextGroup = null; + checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + s1.customContextRange = undefined; + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + s4.value = 0; + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + div.removeChild(s1); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + s1.contextGroup = 'B'; + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + div.appendChild(s1); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 61, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + + s1.removeAttribute('context-group'); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {display: 'none'}); + + s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 100); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {left: 0, width: 51, classList: ['positive']}); + + s3.value = 0; + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {display: 'none'}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {left: 0, width: 51, classList: ['positive']}); + }); + + test('deltaSparkline_noImprovementDirection', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const span1 = tr.v.ui.createScalarSpan(20971520, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span1); + const span2 = tr.v.ui.createScalarSpan(15728640, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span2); + const span3 = tr.v.ui.createScalarSpan(12582912, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span3); + const span4 = tr.v.ui.createScalarSpan(11534336, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span4); + const span5 = tr.v.ui.createScalarSpan(10485760, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span5); + const span6 = tr.v.ui.createScalarSpan(9437184, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span6); + const span7 = tr.v.ui.createScalarSpan(8388608, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span7); + const span8 = tr.v.ui.createScalarSpan(5242880, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span8); + + // We must check the sparklines *after* all spans are appended because new + // values can change the context range. + checkSparkline(span1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(span2, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(span3, {left: 0, width: 61, classList: ['positive']}); + checkSparkline(span4, {left: 0, width: 56, classList: ['positive']}); + checkSparkline(span5, {left: 0, width: 51, classList: ['positive']}); + checkSparkline(span6, {left: 0, width: 46, classList: ['positive']}); + checkSparkline(span7, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(span8, {left: 0, width: 26, classList: ['positive']}); + }); + + test('deltaSparkline_smallerIsBetter', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const span1 = tr.v.ui.createScalarSpan(5242880, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span1); + const span2 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span2); + const span3 = tr.v.ui.createScalarSpan(-3145728, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span3); + const span4 = tr.v.ui.createScalarSpan(-4194304, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span4); + const span5 = tr.v.ui.createScalarSpan(-5242880, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span5); + const span6 = tr.v.ui.createScalarSpan(-6291456, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span6); + const span7 = tr.v.ui.createScalarSpan(-7340032, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span7); + const span8 = tr.v.ui.createScalarSpan(-15728640, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span8); + + // We must check the sparklines *after* all spans are appended because new + // values can change the context range. + checkSparkline(span1, + {left: 75, width: 26, classList: ['positive', 'worse']}); + checkSparkline(span2, {left: 75, width: 1, classList: ['same']}); + checkSparkline(span3, {left: 60, width: 16, classList: ['better']}); + checkSparkline(span4, {left: 55, width: 21, classList: ['better']}); + checkSparkline(span5, {left: 50, width: 26, classList: ['better']}); + checkSparkline(span6, {left: 45, width: 31, classList: ['better']}); + checkSparkline(span7, {left: 40, width: 36, classList: ['better']}); + checkSparkline(span8, {left: 0, width: 76, classList: ['better']}); + }); + + test('deltaSparkline_biggerIsBetter', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const span1 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span1); + const span2 = tr.v.ui.createScalarSpan(-5242880, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span2); + const span3 = tr.v.ui.createScalarSpan(-8388608, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span3); + const span4 = tr.v.ui.createScalarSpan(-9437184, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span4); + const span5 = tr.v.ui.createScalarSpan(-10485760, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span5); + const span6 = tr.v.ui.createScalarSpan(-11534336, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span6); + const span7 = tr.v.ui.createScalarSpan(-12582912, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span7); + const span8 = tr.v.ui.createScalarSpan(-20971520, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span8); + + // We must check the sparklines *after* all spans are appended because new + // values can change the context range. + checkSparkline(span1, {left: 100, width: 1, classList: ['same']}); + checkSparkline(span2, {left: 75, width: 26, classList: ['worse']}); + checkSparkline(span3, {left: 60, width: 41, classList: ['worse']}); + checkSparkline(span4, {left: 55, width: 46, classList: ['worse']}); + checkSparkline(span5, {left: 50, width: 51, classList: ['worse']}); + checkSparkline(span6, {left: 45, width: 56, classList: ['worse']}); + checkSparkline(span7, {left: 40, width: 61, classList: ['worse']}); + checkSparkline(span8, {left: 0, width: 101, classList: ['worse']}); + }); + + test('classListChanges', function() { + const div = document.createElement('div'); + div.style.width = '200px'; + this.addHTMLOutput(div); + + const span = tr.v.ui.createScalarSpan(10, { + unit: Unit.byName.energyInJoulesDelta_smallerIsBetter, + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + customContextRange: tr.b.math.Range.fromExplicitRange(-20, 20) + }); + Polymer.dom(div).appendChild(span); + + assert.sameMembers(Array.from(span.$.content.classList), ['worse']); + checkSignificance(span, 'significantly_worse'); + + span.significance = tr.b.math.Statistics.Significance.DONT_CARE; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'worse']); + assert.sameMembers(Array.from(span.$.content.classList), ['worse']); + checkSignificance(span, ''); + + span.value = -5; + assert.sameMembers(Array.from(span.$.sparkline.classList), ['better']); + assert.sameMembers(Array.from(span.$.content.classList), ['better']); + checkSignificance(span, ''); + + span.unit = Unit.byName.energyInJoules; + assert.sameMembers(Array.from(span.$.sparkline.classList), []); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + + span.value = 20; + assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + + span.unit = Unit.byName.energyInJoulesDelta_biggerIsBetter; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'better']); + assert.sameMembers(Array.from(span.$.content.classList), ['better']); + checkSignificance(span, ''); + + span.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'better']); + assert.sameMembers(Array.from(span.$.content.classList), ['better']); + checkSignificance(span, 'insignificant'); + + span.unit = Unit.byName.energyInJoulesDelta_smallerIsBetter; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'worse']); + assert.sameMembers(Array.from(span.$.content.classList), ['worse']); + checkSignificance(span, 'insignificant'); + + span.unit = Unit.byName.energyInJoulesDelta; + assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + + span.value = 0; + assert.sameMembers(Array.from(span.$.sparkline.classList), []); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + }); + + test('sparkline_uncentered', function() { + const div = document.createElement('div'); + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + }); + + test('sparkline_centered', function() { + const div = document.createElement('div'); + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + }); + + timedPerfTest('memory_scalar_spans', function() { + tr.v.ui.createScalarSpan(EXAMPLE_MEMORY_NUMERIC, { + context: EXAMPLE_MEMORY_FORMATTING_CONTEXT, + inline: true, + }); + }, { + iterations: 1000, + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md new file mode 100644 index 00000000000..3846af6ee0d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md @@ -0,0 +1,78 @@ +This document describes the Google Analytics metrics reported by results.html. + +Measures are recorded by the performance web API, and are visible in the User +Timing track in the devtools Performance timeline. + +Instant events are recorded using console.timestamp(), and are visible as orange +ticks at the top of the devtools Performance timeline. + +Both measures and instant events are recorded as Events in Google Analytics. +[Access these metrics here](https://analytics.google.com/analytics/web/#embed/report-home/a98760012w145165698p149871853/) if you have been granted access. + + * histogram-set-controls + * `alpha` measures response latency of changing the statistical significance + threshold, alpha. + * `hideOverviewCharts` measures response latency of hiding all overview + charts. + * `referenceColumn` measures response latency of changing the reference + column. + * `search` measures response latency of changing the search query to filter + rows. + * `showAll` measures response latency of toggling showing all rows versus + source Histograms only. + * `showOverviewCharts` measures response latency of showing all overview + charts. + * `statistic` measures response latency of changing the statistic that is + displayed in histogram-set-table-cells. + * HistogramSetLocation + * `onPopState` measures response latency of the browser back button. + * `pushState` measures latency of serializing the view state and pushing it + to the HTML5 history API. This happens automatically whenever any part of + the ViewState is updated. + * histogram-set-table + * `columnCount` instant event contains the number of columns, recorded when the + table is built. + * `nameColumnConstrained` instant event recorded when the name column width + is constrained. + * `nameColumnUnconstrained` instant event recorded when the name column width + is unconstrained. + * `rootRowCount` instant event contains the number of root rows, recorded + whenever it changes or the table is built. + * `rowCollapsed` instant event recorded whenever a row is collapsed. + * `rowExpanded` instant event recorded whenever a row is expanded. + * `selectHistogramNames` instant event recorded whenever a breakdown related + histogram name link is clicked. + * `sortColumn` instant event recorded whenever the user changes the sort + column. + * histogram-set-table-cell + * `close` instant event recorded when the cell is closed. + * `open` instant event recorded when the cell is opened. + * histogram-set-table-name-cell + * `closeHistograms` instant event recorded when the user clicks the button to + close all histogram-set-table-cells in the row. + * `hideOverview` instant event recorded when the user clicks the button to + hide the overview line charts for the row. + * `openHistograms` instant event recorded when the user clicks the button to + open all histogram-set-table-cells in the row. + * `showOverview` instant event recorded when the user clicks the button to + show the overview line charts for the row. + * histogram-set-view + * `build` measures latency to find source Histograms, collect parameters, + configure the controls and build the table. Does not include parsing + Histograms from json. + * `sourceHistograms` measures latency to find source Histograms in the + relationship graphical model. + * `collectParameters` measures latency to collect display labels, statistic + names, and possible groupings. + * `export{Raw,Merged}{CSV,JSON}` measures latency to download a CSV/JSON file + of raw/merged Histograms. + * histogram-span + * `brushBins` instant event recorded when the user finishes brushing bins. + * `clearBrushedBins` instant event recorded when the user clears brushed + bins. + * `mergeSampleDiagnostics` measures latency of displaying the table of merged + sample diagnostics. + * `splitSampleDiagnostics` measures latency of displaying the table of + unmerged sample diagnostics. + * HistogramParameterCollector + * `maxSampleCount` instant event records maximum Histogram.numValues diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html new file mode 100644 index 00000000000..de68e0cfa57 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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. +--> + +<!-- + This file only depends on diagnostic_span.html, but it must be imported from + diagnostic_span.html. + Fortunately, this file is only imported from diagnostic_span.html, so it can + just not import anything. +--> + +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-unmergeable-diagnostic-set-span"> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-unmergeable-diagnostic-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + Polymer.dom(this).textContent = ''; + for (const diagnostic of this.diagnostic) { + if (diagnostic instanceof tr.v.d.RelatedNameMap) continue; + const div = document.createElement('div'); + div.appendChild(tr.v.ui.createDiagnosticSpan( + diagnostic, this.name_, this.histogram_)); + Polymer.dom(this).appendChild(div); + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html new file mode 100644 index 00000000000..29e48966fc0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html @@ -0,0 +1,40 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const event = tr.c.TestUtils.newSliceEx({ + title: 'event', + start: 0, + duration: 1, + }); + event.parentContainer = { + sliceGroup: { + stableId: 'fake_thread', + slices: [event], + }, + }; + const diagnostics = new tr.v.d.UnmergeableDiagnosticSet([ + new tr.v.d.GenericSet(['generic diagnostic']), + new tr.v.d.RelatedNameMap(), + new tr.v.d.RelatedEventSet([ + event, + ]), + ]); + const span = tr.v.ui.createDiagnosticSpan(diagnostics); + assert.strictEqual('TR-V-UI-UNMERGEABLE-DIAGNOSTIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html new file mode 100644 index 00000000000..0adeaf55e3c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html @@ -0,0 +1,410 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 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/value/ui/metrics_visualization.html"> +<link rel="import" href="/tracing/value/ui/raster_visualization.html"> +<meta charset="utf-8"> +<dom-module id='tr-v-ui-visualizations-data-container'> + <template> + <style> + .error { + color: red; + display: none; + } + + .sample{ + display: none; + } + + .subtitle{ + font-size: 20px; + font-weight: bold; + padding-bottom: 5px; + } + + .description{ + font-size: 15px; + padding-bottom: 5px; + } + + #title { + font-size: 30px; + font-weight: bold; + padding-bottom: 5px; + } + </style> + <div id="title">Visualizations</div> + <div id="data_error" class="error">Invalid data provided.</div> + <div id="pipeline_per_frame_container"> + <div class="subtitle">Graphics Pipeline and Raster Tasks</div> + <div class="description"> + When raster tasks are completed in comparison to the rest of the graphics pipeline.<br> + Only pages where raster tasks are completed after beginFrame is issued are included. + </div> + <tr-v-ui-raster-visualization id="rasterVisualization"> + </tr-v-ui-raster-visualization> + </div> + <div id=metrics_container> + <div class="subtitle">Metrics</div> + <div class="description">Total amount of time taken for the indicated metrics.</div> + <tr-v-ui-metrics-visualization id="metricsVisualization" class="sample"> + </tr-v-ui-metrics-visualization> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + const STATISTICS_KEY = 'statistics'; + const SUBMETRICS_KEY = 'submetrics'; + const AGGREGATE_KEY = 'aggregate'; + const RASTER_START_METRIC_KEY = 'pipeline:begin_frame_to_raster_start'; + + const COLORS = [ + ['#FFD740', '#FFC400', '#FFAB00', '#E29800'], + ['#FF6E40', '#FF3D00', '#DD2C00', '#A32000'], + ['#40C4FF', '#00B0FF', '#0091EA', '#006DAF'], + ['#89C641', '#54B503', '#4AA510', '#377A0D'], + ['#B388FF', '#7C4DFF', '#651FFF', '#6200EA'], + ['#FF80AB', '#FF4081', '#F50057', '#C51162'], + ['#FFAB40', '#FF9100', '#FF6D00', '#D65C02'], + ['#8C9EFF', '#536DFE', '#3D5AFE', '#304FFE']]; + + const FRAME = [new Map([ + ['pipeline:begin_frame_to_raster_start', false], + ['pipeline:begin_frame_to_raster_end', true]]), new Map([ + ['pipeline:begin_frame_transport', true], + ['pipeline:begin_frame_to_frame_submission', true], + ['pipeline:frame_submission_to_display', true], + ['pipeline:draw', true]])]; + + const METRICS = new Map([ + ['Pipeline', [ + 'pipeline:begin_frame_transport', + 'pipeline:begin_frame_to_frame_submission', + 'pipeline:frame_submission_to_display', + 'pipeline:draw']], + ['Thread', [ + 'thread_browser_cpu_time_per_frame', + 'thread_display_compositor_cpu_time_per_frame', + 'thread_GPU_cpu_time_per_frame', + 'thread_IO_cpu_time_per_frame', + 'thread_other_cpu_time_per_frame', + 'thread_raster_cpu_time_per_frame', + 'thread_renderer_compositor_cpu_time_per_frame', + 'thread_renderer_main_cpu_time_per_frame']]]); + + function getValueFromMap(key, map) { + let retrievedValue = map.get(key); + if (!retrievedValue) { + retrievedValue = new Map(); + map.set(key, retrievedValue); + } + return retrievedValue; + } + + Polymer({ + is: 'tr-v-ui-visualizations-data-container', + + created() { + // from earliest to latest + this.orderedBenchmarks_ = []; + // aggregate/page -> benchmark -> metric -> statistics/submetrics + this.groupedData_ = new Map(); + }, + + build(leafHistograms, histograms) { + if (!leafHistograms || leafHistograms.length < 1 || + !histograms || histograms.length < 1) { + this.$.data_error.style.display = 'block'; + return; + } + + this.processHistograms_(this.groupHistograms_(histograms), + this.groupHistograms_(leafHistograms)); + this.buildCharts_(); + }, + + processHistograms_(histograms, leafHistograms) { + const benchmarkStartGrouping = tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BENCHMARK_START); + + const benchmarkToStartTime = new Map(); + for (const [metric, benchmarks] of histograms.entries()) { + // process aggregate data + for (const [benchmark, pages] of leafHistograms.get(metric).entries()) { + for (const [page, histograms] of pages.entries()) { + for (const histogram of histograms) { + const aggregateToBenchmarkMap = getValueFromMap(AGGREGATE_KEY, + this.groupedData_); + const benchmarkToMetricMap = getValueFromMap(benchmark, + aggregateToBenchmarkMap); + + benchmarkToMetricMap.set(metric, new Map([ + [STATISTICS_KEY, histogram.running]])); + } + } + } + + // process data per page + for (const [benchmark, pages] of benchmarks.entries()) { + for (const [page, histograms] of pages.entries()) { + for (const histogram of histograms) { + if (!benchmarkToStartTime.get(benchmark)) { + benchmarkToStartTime.set(benchmark, + benchmarkStartGrouping.callback(histogram)); + } + + const pageToBenchmarkMap = getValueFromMap(page, + this.groupedData_); + const benchmarkToMetricMap = getValueFromMap(benchmark, + pageToBenchmarkMap); + + // retrieving submetric _ta + const mergedSubmetrics = new tr.v.d.DiagnosticMap(); + for (const bin of histogram.allBins) { + for (const map of bin.diagnosticMaps) { + mergedSubmetrics.addDiagnostics(map); + } + } + + if (benchmarkToMetricMap.get(metric)) continue; + benchmarkToMetricMap.set(metric, new Map([ + [STATISTICS_KEY, histogram.running], + [SUBMETRICS_KEY, mergedSubmetrics.get('breakdown')]])); + } + } + } + } + this.orderedBenchmarks_ = this.sortBenchmarks_(benchmarkToStartTime); + }, + + groupHistograms_(histograms) { + const groupings = [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.DISPLAY_LABEL, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES)]; + + return histograms.groupHistogramsRecursively(groupings); + }, + + sortBenchmarks_(benchmarks) { + return Array.from(benchmarks.keys()).sort((a, b) => { + Date.parse(benchmarks.get(a)) - Date.parse(benchmarks.get(b)); + }); + }, + + getSeriesKey_(metric, benchmark) { + return metric + '-' + benchmark; + }, + + buildCharts_() { + const rasterDataToBePassed = this.buildRasterChart_(); + this.$.rasterVisualization.build(rasterDataToBePassed); + + for (const chartName of METRICS.keys()) { + const metricsDataToBePassed = this.buildMetricsData_(chartName); + const newChart = this.$.metricsVisualization.cloneNode(true); + newChart.style.display = 'block'; + Polymer.dom(this.$.metrics_container).appendChild(newChart); + newChart.build(metricsDataToBePassed); + } + }, + + buildRasterChart_() { + const orderedPages = [...this.groupedData_.keys()] + .filter((page) => this.filterPagesWithoutRasterMetric_(page)) + .sort((a, b) => this.sortByRasterStart_(a, b)); + const allChartData = new Map(); + for (const page of orderedPages) { + const pageMap = this.groupedData_.get(page); + let chartData = []; + for (const benchmark of this.orderedBenchmarks_) { + if (!pageMap.has(benchmark)) continue; + const benchmarkMap = pageMap.get(benchmark); + const benchmarkData = []; + if (benchmarkMap.get(RASTER_START_METRIC_KEY) === undefined) { + continue; + } + for (const [threadName, thread] of FRAME.entries()) { + const data = {x: benchmark, hide: 0}; + if (page !== AGGREGATE_KEY) data.group = page; + let rasterBegin = 0; + for (const metric of thread.keys()) { + const metricMap = benchmarkMap.get(metric); + const key = this.getSeriesKey_(metric, + data.x + '-' + threadName); + const stats = metricMap.get(STATISTICS_KEY); + const mean = stats ? stats.mean : 0; + let roundedMean = Math.round(mean * 100) / 100; + if (metric === RASTER_START_METRIC_KEY) { + rasterBegin = roundedMean; + } else if (metric === 'pipeline:begin_frame_to_raster_end') { + roundedMean -= rasterBegin; + } + data[key] = roundedMean; + } + benchmarkData.push(data); + } + chartData = chartData.concat(benchmarkData); + } + allChartData.set(page, chartData); + } + return allChartData; + }, + + buildMetricsData_(chartName) { + // pages are ordered from smallest to largest by their total + // values for the first benchmark + const orderedPages = [...this.groupedData_.keys()].sort((a, b) => + this.sortByTotal_(a, b, chartName)); + const chartData = []; + const aggregateChart = []; + for (const page of orderedPages) { + const pageMap = this.groupedData_.get(page); + for (const benchmark of this.orderedBenchmarks_) { + if (!pageMap.has(benchmark)) continue; + const data = {x: benchmark, group: page}; + const benchmarkMap = pageMap.get(benchmark); + for (const metric of METRICS.get(chartName)) { + const metricMap = benchmarkMap.get(metric); + const key = this.getSeriesKey_(metric, benchmark); + const stats = metricMap.get(STATISTICS_KEY); + const mean = stats ? stats.mean : 0; + data[key] = Math.round(mean * 100) / 100; + } + if (page === AGGREGATE_KEY) { + aggregateChart.push(data); + } else { + chartData.push(data); + } + } + chartData.push({}); + } + chartData.shift(); + return { + title: chartName, + aggregate: aggregateChart, + page: chartData, + submetrics: this.processSubmetricsData_(chartName) + }; + }, + + submetricsHelper_(submetric, value, benchmark, metricToSubmetricMap) { + let submetricToBenchmarkMap = metricToSubmetricMap.get(submetric); + if (!submetricToBenchmarkMap) { + submetricToBenchmarkMap = []; + metricToSubmetricMap.set(submetric, submetricToBenchmarkMap); + } + const data = {x: submetric, hide: 0, group: benchmark}; + const mean = value; + const roundedMean = Math.round(mean * 100) / 100; + if (!roundedMean) return; + data[this.getSeriesKey_(submetric, benchmark)] = roundedMean; + submetricToBenchmarkMap.push(data); + }, + + // Get data for breakdown of a main step + processSubmetricsData_(chartName) { + // page -> metric -> submetric -> + // array of submetrics across all benchmarks + const submetrics = new Map(); + for (const [page, pageMap] of this.groupedData_.entries()) { + if (page === AGGREGATE_KEY) continue; + const pageToMetricMap = getValueFromMap(page, submetrics); + for (const benchmark of this.orderedBenchmarks_) { + const benchmarkMap = pageMap.get(benchmark); + if (!benchmarkMap) continue; + for (const metric of METRICS.get(chartName)) { + const metricMap = benchmarkMap.get(metric); + const metricToSubmetricMap = getValueFromMap(metric, + pageToMetricMap); + const submetrics = metricMap.get(SUBMETRICS_KEY); + if (!submetrics) { + this.submetricsHelper_(metric, metricMap.get(STATISTICS_KEY), + benchmark, metricToSubmetricMap); + continue; + } + for (const [submetric, value] of [...submetrics]) { + this.submetricsHelper_(submetric, value, benchmark, + metricToSubmetricMap); + } + } + } + } + return submetrics; + }, + + sortByTotal_(a, b, chartName) { + if (a === AGGREGATE_KEY) return -1; + if (b === AGGREGATE_KEY) return 1; + let aValue = 0; + const aMap = this.groupedData_.get(a); + if (aMap.get(this.orderedBenchmarks_[0]) !== undefined) { + for (const metric of METRICS.get(chartName)) { + const aMetricMap = aMap.get(this.orderedBenchmarks_[0]).get(metric); + const aStats = aMetricMap.get(STATISTICS_KEY); + aValue += aStats ? aStats.mean : 0; + } + } + let bValue = 0; + const bMap = this.groupedData_.get(b); + if (bMap.get(this.orderedBenchmarks_[0]) !== undefined) { + for (const metric of METRICS.get(chartName)) { + const bMetricMap = bMap.get(this.orderedBenchmarks_[0]).get(metric); + const bStats = bMetricMap.get(STATISTICS_KEY); + bValue += bStats ? bStats.mean : 0; + } + } + return aValue - bValue; + }, + + filterPagesWithoutRasterMetric_(page) { + const pageMap = this.groupedData_.get(page); + for (const benchmark of this.orderedBenchmarks_) { + const pageMetricMap = pageMap.get(benchmark); + if (!pageMetricMap) continue; + const wantedMetric = pageMetricMap.get(RASTER_START_METRIC_KEY); + if (wantedMetric !== undefined) return true; + } + return false; + }, + + sortByRasterStart_(a, b) { + if (a === AGGREGATE_KEY) return 1; + if (b === AGGREGATE_KEY) return -1; + let aValue = 0; + const aMap = this.groupedData_.get(a); + if (aMap.get(this.orderedBenchmarks_[0]) !== undefined) { + const aMetricMap = aMap.get(this.orderedBenchmarks_[0]) + .get(RASTER_START_METRIC_KEY); + const aStats = aMetricMap.get(STATISTICS_KEY); + aValue = aStats ? aStats.mean : 0; + } + let bValue = 0; + const bMap = this.groupedData_.get(b); + if (bMap.get(this.orderedBenchmarks_[0]) !== undefined) { + const bMetricMap = bMap.get(this.orderedBenchmarks_[0]) + .get(RASTER_START_METRIC_KEY); + const bStats = bMetricMap.get(STATISTICS_KEY); + bValue = bStats ? bStats.mean : 0; + } + return bValue - aValue; + }, + }); + + return { + STATISTICS_KEY, + SUBMETRICS_KEY, + AGGREGATE_KEY, + COLORS, + FRAME, + METRICS, + getValueFromMap, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html new file mode 100644 index 00000000000..1199d8ed731 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 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/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function getHistogram(name) { + const samples = []; + for (let i = 0; i < 5; ++i) { + const total = Math.random(); + const values = {}; + values[name + 'a'] = total / 2.0; + values[name + 'b'] = total / 4.0; + values[name + 'c'] = total / 4.0; + samples.push({ + value: total, + diagnostics: new Map([ + [ + tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(Date.now()), + ], [ + 'breakdown', tr.v.d.Breakdown.fromDict({values}), + ], + ]), + }); + } + return tr.v.Histogram.create(name, tr.b.Unit.byName.count, samples); + } + + function getHistogramSet(displayLabel, story, containsRasterStart = true) { + const histograms = new tr.v.HistogramSet(); + let metrics = []; + for (const category of tr.v.ui.METRICS.values()) { + metrics = metrics.concat(category); + } + for (const metric of metrics) { + histograms.addHistogram(getHistogram(metric)); + } + + if (containsRasterStart) { + histograms.addHistogram( + getHistogram('pipeline:begin_frame_to_raster_start')); + histograms.addHistogram( + getHistogram('pipeline:begin_frame_to_raster_end')); + } + histograms.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet([displayLabel])); + histograms.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet([story])); + return histograms; + } + + test('instantiate', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'test.com'); + + const histograms2 = getHistogramSet('Run 2', 'test.com'); + histograms.importDicts(histograms2.asDicts()); + + const histograms3 = getHistogramSet('Run 1', 'abc.com'); + histograms.importDicts(histograms3.asDicts()); + + const histograms4 = getHistogramSet('Run 2', 'abc.com'); + histograms.importDicts(histograms4.asDicts()); + + cp.build(histograms, histograms); + }); + + test('instantiateWithRepeat', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'repeat.com'); + const histograms2 = getHistogramSet('Run 1', 'repeat.com'); + histograms.importDicts(histograms2.asDicts()); + + cp.build(histograms, histograms); + }); + + test('instantiateWithoutRasterTasks', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'test.com', false); + + const histograms2 = getHistogramSet('Run 2', 'test.com', false); + histograms.importDicts(histograms2.asDicts()); + + const histograms3 = getHistogramSet('Run 1', 'abc.com'); + histograms.importDicts(histograms3.asDicts()); + + const histograms4 = getHistogramSet('Run 2', 'abc.com'); + histograms.importDicts(histograms4.asDicts()); + + cp.build(histograms, histograms); + }); + + test('instantiateWithDifferentStorySets', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'test.com'); + + const histograms2 = getHistogramSet('Run 1', 'abc.com'); + histograms.importDicts(histograms2.asDicts()); + + const histograms3 = getHistogramSet('Run 2', 'abc.com'); + histograms.importDicts(histograms3.asDicts()); + + cp.build(histograms, histograms); + }); +}); +</script> |