summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing/tracing/value
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/value')
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/__init__.py0
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html154
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html401
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py24
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/csv_builder.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html75
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py3
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py180
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py237
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html16
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html116
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html133
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py95
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html130
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html145
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py29
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py201
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram.html1478
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram.py1111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html204
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py161
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html163
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py127
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html131
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html141
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set.html374
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set.py155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html157
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py236
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_test.html809
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py674
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html23
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py116
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html271
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html350
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html125
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html557
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html300
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html251
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html290
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html459
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html396
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html361
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html299
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html1679
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html210
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html144
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html599
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html300
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html353
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html274
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html204
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html312
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html23
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html626
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html1027
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/timings.md78
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html410
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html124
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_">&#945;=[[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>