summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing/tracing/ui/tracks
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/ui/tracks')
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html328
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html566
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html331
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html213
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html313
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html281
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html454
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html106
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html138
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html205
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html140
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html215
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html145
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html236
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html137
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html107
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html251
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html253
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html270
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html534
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html240
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html294
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html130
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html313
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html249
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html412
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html167
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html299
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html29
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css7
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html131
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css10
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html141
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css33
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html167
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html309
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html133
79 files changed, 11707 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html
new file mode 100644
index 00000000000..571b0543bb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html
@@ -0,0 +1,51 @@
+<!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/ui/tracks/letter_dot_track.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of alert objects.
+ * @constructor
+ * @extends {LetterDotTrack}
+ */
+ const AlertTrack = tr.ui.b.define(
+ 'alert-track', tr.ui.tracks.LetterDotTrack);
+
+ AlertTrack.prototype = {
+ __proto__: tr.ui.tracks.LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Alerts';
+ this.alerts_ = undefined;
+ },
+
+ get alerts() {
+ return this.alerts_;
+ },
+
+ set alerts(alerts) {
+ this.alerts_ = alerts;
+ if (alerts === undefined) {
+ this.items = undefined;
+ return;
+ }
+ this.items = this.alerts_.map(function(alert) {
+ return new tr.ui.tracks.LetterDot(
+ alert, String.fromCharCode(9888), alert.colorId, alert.start);
+ });
+ }
+ };
+
+ return {
+ AlertTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html
new file mode 100644
index 00000000000..4e60180b00e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html
@@ -0,0 +1,76 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AlertTrack = tr.ui.tracks.AlertTrack;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'One alert');
+ const ALERT_INFO_2 = new tr.model.EventInfo(
+ 'Alert 2', 'Another alert');
+
+ const createAlerts = function() {
+ const alerts = [
+ new tr.model.Alert(ALERT_INFO_1, 5),
+ new tr.model.Alert(ALERT_INFO_1, 20),
+ new tr.model.Alert(ALERT_INFO_2, 35),
+ new tr.model.Alert(ALERT_INFO_2, 50)
+ ];
+ return alerts;
+ };
+
+ test('instantiate', function() {
+ const alerts = createAlerts();
+ alerts[1].selectionState = SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = AlertTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.alerts = alerts;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+
+ assert.strictEqual(5, track.items[0].start);
+ });
+
+ test('modelMapping', function() {
+ const alerts = createAlerts();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = AlertTrack(viewport);
+ track.alerts = alerts;
+
+ const a0 = track.items[0].modelItem;
+ assert.strictEqual(a0, alerts[0]);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html
new file mode 100644
index 00000000000..d922030ce70
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a AsyncSliceGroup.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const AsyncSliceGroupTrack = tr.ui.b.define(
+ 'async-slice-group-track',
+ tr.ui.tracks.MultiRowTrack);
+
+ AsyncSliceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('async-slice-group-track');
+ this.group_ = undefined;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ track.asyncStyle = true;
+ return track;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(group) {
+ this.group_ = group;
+ this.buildAndSetSubRows_();
+ },
+
+ get eventContainer() {
+ return this.group;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.group, this);
+ },
+
+ buildAndSetSubRows_() {
+ if (this.group_.viewSubGroups.length <= 1) {
+ // No nested groups or just only one, the most common case.
+ const rows = groupAsyncSlicesIntoSubRows(this.group_.slices);
+ const rowsWithHeadings = rows.map(row => {
+ return {row, heading: undefined};
+ });
+ this.setPrebuiltSubRows(this.group_, rowsWithHeadings);
+ return;
+ }
+
+ // We have nested grouping level (no further levels supported),
+ // so process sub-groups separately and preserve their titles.
+ const rowsWithHeadings = [];
+ for (const subGroup of this.group_.viewSubGroups) {
+ const subGroupRows = groupAsyncSlicesIntoSubRows(subGroup.slices);
+ if (subGroupRows.length === 0) {
+ continue;
+ }
+ for (let i = 0; i < subGroupRows.length; i++) {
+ rowsWithHeadings.push({
+ row: subGroupRows[i],
+ heading: (i === 0 ? subGroup.title : '')
+ });
+ }
+ }
+ this.setPrebuiltSubRows(this.group_, rowsWithHeadings);
+ }
+ };
+
+ /**
+ * Strip away wrapper slice which are used to group slices into
+ * a single track but provide no information themselves.
+ */
+ function stripSlice_(slice) {
+ if (slice.subSlices !== undefined && slice.subSlices.length === 1) {
+ const subSlice = slice.subSlices[0];
+ if (tr.b.math.approximately(subSlice.start, slice.start, 1) &&
+ tr.b.math.approximately(subSlice.duration, slice.duration, 1)) {
+ return subSlice;
+ }
+ }
+ return slice;
+ }
+
+ /**
+ * Unwrap the list of non-overlapping slices into a number of rows where
+ * the top row holds original slices and additional rows hold nested slices
+ * of ones from the row above them.
+ */
+ function makeLevelSubRows_(slices) {
+ const rows = [];
+ const putSlice = (slice, level) => {
+ while (rows.length <= level) {
+ rows.push([]);
+ }
+ rows[level].push(slice);
+ };
+ const putSliceRecursively = (slice, level) => {
+ putSlice(slice, level);
+ if (slice.subSlices !== undefined) {
+ for (const subSlice of slice.subSlices) {
+ putSliceRecursively(subSlice, level + 1);
+ }
+ }
+ };
+
+ for (const slice of slices) {
+ putSliceRecursively(stripSlice_(slice), 0);
+ }
+ return rows;
+ }
+
+ /**
+ * Breaks up the list of slices into a number of rows:
+ * - Which contain non-overlapping slices.
+ * - If slice has nested slices, they're placed onto the row below.
+ * Sorting may be skipped if slices are already sorted by start timestamp.
+ */
+ function groupAsyncSlicesIntoSubRows(slices, opt_skipSort) {
+ if (!opt_skipSort) {
+ slices.sort((x, y) => x.start - y.start);
+ }
+
+ // The algorithm is fairly simple:
+ // - Level is a group of rows, where the top row holds original slices and
+ // additional rows hold nested slices of ones from the row above them.
+ // - Make a level by putting sorted slices, skipping if one's overlapping.
+ // - Repeat and make more levels while we're having residual slices left.
+ const rows = [];
+ let slicesLeft = slices;
+ while (slicesLeft.length !== 0) {
+ // Make a level.
+ const fit = [];
+ const unfit = [];
+ let levelEndTime = -1;
+
+ for (const slice of slicesLeft) {
+ if (slice.start >= levelEndTime) {
+ // Assuming nested slices lie within parent's boundaries.
+ levelEndTime = slice.end;
+ fit.push(slice);
+ } else {
+ unfit.push(slice);
+ }
+ }
+ rows.push(...makeLevelSubRows_(fit));
+ slicesLeft = unfit;
+ }
+ return rows;
+ }
+
+ return {
+ AsyncSliceGroupTrack,
+ groupAsyncSlicesIntoSubRows,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html
new file mode 100644
index 00000000000..96003e1b5f2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html
@@ -0,0 +1,328 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSliceGroup = tr.model.AsyncSliceGroup;
+ const AsyncSliceGroupTrack = tr.ui.tracks.AsyncSliceGroupTrack;
+ const Process = tr.model.Process;
+ const ProcessTrack = tr.ui.tracks.ProcessTrack;
+ const Thread = tr.model.Thread;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed;
+ const groupAsyncSlicesIntoSubRows = tr.ui.tracks.groupAsyncSlicesIntoSubRows;
+
+ test('filterSubRows', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ assert.strictEqual(track.children.length, 1);
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_empty', function() {
+ const rows = groupAsyncSlicesIntoSubRows([]);
+ assert.strictEqual(rows.length, 0);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_trivial', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+
+ const s1 = newAsyncSlice(10, 200, t1, t1);
+ const s2 = newAsyncSlice(300, 30, t1, t1);
+
+ const slices = [s2, s1];
+ const rows = groupAsyncSlicesIntoSubRows(slices);
+
+ assert.strictEqual(rows.length, 1);
+ assert.sameMembers(rows[0], [s1, s2]);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_nonTrivial', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+
+ const s1 = newAsyncSlice(10, 200, t1, t1); // Should be stripped.
+ const s1s1 = newAsyncSlice(10, 200, t1, t1);
+ s1.subSlices = [s1s1];
+
+ const s2 = newAsyncSlice(300, 30, t1, t1);
+ const s2s1 = newAsyncSlice(300, 10, t1, t1);
+ const s2s2 = newAsyncSlice(310, 20, t1, t1); // Should not be stripped.
+ const s2s2s1 = newAsyncSlice(310, 20, t1, t1);
+ s2s2.subSlices = [s2s2s1];
+ s2.subSlices = [s2s2, s2s1];
+
+ const s3 = newAsyncSlice(200, 50, t1, t1); // Overlaps with s1.
+ const s3s1 = newAsyncSlice(220, 5, t1, t1);
+ s3.subSlices = [s3s1];
+
+ const slices = [s2, s3, s1];
+ const rows = groupAsyncSlicesIntoSubRows(slices);
+
+ assert.strictEqual(rows.length, 5);
+ assert.sameMembers(rows[0], [s1s1, s2]);
+ assert.sameMembers(rows[1], [s2s1, s2s2]);
+ assert.sameMembers(rows[2], [s2s2s1]);
+ assert.sameMembers(rows[3], [s3]);
+ assert.sameMembers(rows[4], [s3s1]);
+ });
+
+ test('rebuildSubRows_twoNonOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ const s1 = newAsyncSlice(0, 1, t1, t1);
+ const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s1.subSlices = [subs1];
+ g.push(s1);
+ g.push(newAsyncSlice(1, 1, t1, t1));
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ const subRows = track.subRows;
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.sameMembers(g.slices[1].subSlices, []);
+ });
+
+ test('rebuildSubRows_twoOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const s1 = newAsyncSlice(0, 1, t1, t1);
+ const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s1.subSlices = [subs1];
+ const s2 = newAsyncSlice(0, 1.5, t1, t1);
+ const subs2 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s2.subSlices = [subs2];
+ g.push(s1);
+ g.push(s2);
+
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.strictEqual(subRows[1][0], g.slices[1].subSlices[0]);
+ });
+
+ test('rebuildSubRows_threePartlyOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ g.push(newAsyncSlice(0, 1.5, t1, t1));
+ g.push(newAsyncSlice(1, 1.5, t1, t1));
+ g.updateBounds();
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ const subRows = track.subRows;
+
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.strictEqual(subRows[0][0], g.slices[0]);
+ assert.strictEqual(subRows[0][1], g.slices[2]);
+ assert.strictEqual(subRows[1][0], g.slices[1]);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.sameMembers(g.slices[0].subSlices, []);
+ assert.sameMembers(g.slices[1].subSlices, []);
+ assert.sameMembers(g.slices[2].subSlices, []);
+ });
+
+ test('rebuildSubRows_threeOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ g.push(newAsyncSlice(0, 1.5, t1, t1));
+ g.push(newAsyncSlice(2, 1, t1, t1));
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.strictEqual(subRows[0][0], g.slices[0]);
+ assert.strictEqual(subRows[1][0], g.slices[1]);
+ assert.strictEqual(subRows[0][1], g.slices[2]);
+ });
+
+ test('rebuildSubRows_twoViewSubGroups', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSliceNamed('foo', 0, 1, t1, t1));
+ g.push(newAsyncSliceNamed('foo', 2, 1, t1, t1));
+ g.push(newAsyncSliceNamed('bar', 1, 2, t1, t1));
+ g.push(newAsyncSliceNamed('bar', 3, 2, t1, t1));
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ track.heading = 'sup';
+
+ assert.strictEqual(track.subRows.length, 2);
+ const subTracks = Polymer.dom(track).children;
+ assert.strictEqual(subTracks.length, 3);
+ assert.strictEqual(subTracks[0].slices.length, 0);
+ assert.strictEqual(subTracks[1].slices.length, 2);
+ assert.strictEqual(subTracks[2].slices.length, 2);
+ const headings =
+ [subTracks[0].heading, subTracks[1].heading, subTracks[2].heading];
+ assert.sameMembers(headings, ['foo', 'bar', 'sup']);
+ });
+
+ // Tests that no slices and their sub slices overlap.
+ test('rebuildSubRows_NonOverlappingSubSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const slice1 = newAsyncSlice(0, 5, t1, t1);
+ const slice1Child = newAsyncSlice(1, 2, t1, t1);
+ slice1.subSlices = [slice1Child];
+ const slice2 = newAsyncSlice(3, 5, t1, t1);
+ const slice3 = newAsyncSlice(5, 4, t1, t1);
+ const slice3Child = newAsyncSlice(6, 2, t1, t1);
+ slice3.subSlices = [slice3Child];
+ g.push(slice1);
+ g.push(slice2);
+ g.push(slice3);
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ // Checks each sub row to see that we don't have any overlapping slices.
+ for (let i = 0; i < subRows.length; i++) {
+ const row = subRows[i];
+ for (let j = 0; j < row.length; j++) {
+ for (let k = j + 1; k < row.length; k++) {
+ assert.isTrue(row[j].end <= row[k].start);
+ }
+ }
+ }
+ });
+
+ test('rebuildSubRows_NonOverlappingSubSlicesThreeNestedLevels', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const slice1 = newAsyncSlice(0, 4, t1, t1);
+ const slice1Child = newAsyncSlice(1, 2, t1, t1);
+ slice1.subSlices = [slice1Child];
+ const slice2 = newAsyncSlice(2, 7, t1, t1);
+ const slice3 = newAsyncSlice(5, 5, t1, t1);
+ const slice3Child = newAsyncSlice(6, 3, t1, t1);
+ const slice3Child2 = newAsyncSlice(7, 1, t1, t1);
+ slice3.subSlices = [slice3Child];
+ slice3Child.subSlices = [slice3Child2];
+ g.push(slice1);
+ g.push(slice2);
+ g.push(slice3);
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ // Checks each sub row to see that we don't have any overlapping slices.
+ for (let i = 0; i < subRows.length; i++) {
+ const row = subRows[i];
+ for (let j = 0; j < row.length; j++) {
+ for (let k = j + 1; k < row.length; k++) {
+ assert.isTrue(row[j].end <= row[k].start);
+ }
+ }
+ }
+ });
+
+ test('asyncSliceGroupContainerMap', function() {
+ const vp = new tr.ui.TimelineViewport();
+ const containerToTrack = vp.containerToTrackMap;
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new AsyncSliceGroup(thread);
+
+ const processTrack = new ProcessTrack(vp);
+ const threadTrack = new ThreadTrack(vp);
+ const groupTrack = new AsyncSliceGroupTrack(vp);
+ processTrack.process = process;
+ threadTrack.thread = thread;
+ groupTrack.group = group;
+ Polymer.dom(processTrack).appendChild(threadTrack);
+ Polymer.dom(threadTrack).appendChild(groupTrack);
+
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+
+ assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+ assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+ assert.isUndefined(
+ containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'));
+
+ vp.modelTrackContainer = {
+ addContainersToTrackMap(containerToTrackMap) {
+ processTrack.addContainersToTrackMap(containerToTrackMap);
+ },
+ addEventListener() {}
+ };
+ vp.rebuildContainerToTrackMap();
+
+ // Check that all tracks call childs' addContainersToTrackMap()
+ // by checking the resulting map.
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123'), processTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456'), threadTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'),
+ groupTrack);
+
+ // Check the track's eventContainer getter.
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html
new file mode 100644
index 00000000000..1b73f367636
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html
@@ -0,0 +1,43 @@
+<!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/model/proxy_selectable_item.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A point in a chart series with x (timestamp) and y (value) coordinates
+ * and an associated model item. The point can optionally also have a base
+ * y coordinate (which for example corresponds to the bottom edge of the
+ * associated bar in a bar chart).
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function ChartPoint(modelItem, x, y, opt_yBase) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.x = x;
+ this.y = y;
+ this.dotLetter = undefined;
+
+ // If the base y-coordinate is undefined, the bottom edge of the associated
+ // bar in a bar chart will start at the outer bottom edge (which is most
+ // likely slightly below zero).
+ this.yBase = opt_yBase;
+ }
+
+ ChartPoint.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype,
+ };
+
+ return {
+ ChartPoint,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html
new file mode 100644
index 00000000000..e2d8bc3e11c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html
@@ -0,0 +1,37 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+
+ test('checkFields_withoutYBase', function() {
+ const event = {};
+ const point = new ChartPoint(event, 42, -7);
+
+ assert.strictEqual(point.modelItem, event);
+ assert.strictEqual(point.x, 42);
+ assert.strictEqual(point.y, -7);
+ assert.isUndefined(point.yBase);
+ });
+
+ test('checkFields_withYBase', function() {
+ const event = {};
+ const point = new ChartPoint(event, 111, 222, 333);
+
+ assert.strictEqual(point.modelItem, event);
+ assert.strictEqual(point.x, 111);
+ assert.strictEqual(point.y, 222);
+ assert.strictEqual(point.yBase, 333);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html
new file mode 100644
index 00000000000..45025d13e0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html
@@ -0,0 +1,566 @@
+<!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/color_scheme.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * The type of a chart series.
+ * @enum
+ */
+ const ChartSeriesType = {
+ LINE: 0,
+ AREA: 1
+ };
+
+ // The default rendering configuration for ChartSeries.
+ const DEFAULT_RENDERING_CONFIG = {
+ // The type of the chart series.
+ chartType: ChartSeriesType.LINE,
+
+ // The size of a selected point dot in device-independent pixels (circle
+ // diameter).
+ selectedPointSize: 4,
+
+ // The size of an unselected point dot in device-independent pixels (square
+ // width/height).
+ unselectedPointSize: 3,
+
+ // Whether the selected dots should be solid circles of the line color, or
+ // filled with the background's selection color.
+ solidSelectedDots: false,
+
+ // The color of the chart.
+ colorId: 0,
+
+ // The width of the top line in device-independent pixels.
+ lineWidth: 1,
+
+ // Minimum distance between points in physical pixels. Points which are
+ // closer than this distance will be skipped.
+ skipDistance: 1,
+
+ // Density in points per physical pixel at which unselected point dots
+ // become transparent.
+ unselectedPointDensityTransparent: 0.10,
+
+ // Density in points per physical pixel at which unselected point dots
+ // become fully opaque.
+ unselectedPointDensityOpaque: 0.05,
+
+ // Opacity of area chart background.
+ backgroundOpacity: 0.5,
+
+ // Whether to graph steps between points. Set to false for lines instead.
+ stepGraph: true
+ };
+
+ // The virtual width of the last point in a series (whose rectangle has zero
+ // width) in world timestamps difference for the purposes of selection.
+ const LAST_POINT_WIDTH = 16;
+
+ // Constants for sizing and font of points with dot letters.
+ const DOT_LETTER_RADIUS_PX = 7;
+ const DOT_LETTER_RADIUS_PADDING_PX = 0.5;
+ const DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX = 3;
+ const DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX = 1.5;
+ const DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX = 1;
+ const DOT_LETTER_FONT_WEIGHT = 400;
+ const DOT_LETTER_FONT_SIZE_PX = 9;
+ const DOT_LETTER_FONT = 'Arial';
+
+ /**
+ * Visual components of a ChartSeries.
+ * @enum
+ */
+ const ChartSeriesComponent = {
+ BACKGROUND: 0,
+ LINE: 1,
+ DOTS: 2
+ };
+
+ /**
+ * A series of points corresponding to a single chart on a chart track.
+ * This class is responsible for drawing the actual chart onto canvas.
+ *
+ * @constructor
+ */
+ function ChartSeries(points, seriesYAxis, opt_renderingConfig) {
+ this.points = points;
+ this.seriesYAxis = seriesYAxis;
+
+ this.useRenderingConfig_(opt_renderingConfig);
+ }
+
+ ChartSeries.prototype = {
+ useRenderingConfig_(opt_renderingConfig) {
+ const config = opt_renderingConfig || {};
+
+ // Store all configuration flags as private properties.
+ for (const [key, defaultValue] of
+ Object.entries(DEFAULT_RENDERING_CONFIG)) {
+ let value = config[key];
+ if (value === undefined) {
+ value = defaultValue;
+ }
+ this[key + '_'] = value;
+ }
+
+ // Avoid unnecessary recomputation in getters.
+ this.topPadding = this.bottomPadding = Math.max(
+ this.selectedPointSize_, this.unselectedPointSize_) / 2;
+ },
+
+ get range() {
+ const range = new tr.b.math.Range();
+ this.points.forEach(function(point) {
+ range.addValue(point.y);
+ }, this);
+ return range;
+ },
+
+ draw(ctx, transform, highDetails) {
+ if (this.points === undefined || this.points.length === 0) {
+ return;
+ }
+
+ // Draw the background.
+ if (this.chartType_ === ChartSeriesType.AREA) {
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.BACKGROUND,
+ highDetails);
+ }
+
+ // Draw the line at the top.
+ if (this.chartType_ === ChartSeriesType.LINE || highDetails) {
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.LINE,
+ highDetails);
+ }
+
+ // Draw the points.
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.DOTS,
+ highDetails);
+ },
+
+ drawComponent_(ctx, transform, component, highDetails) {
+ // We need to consider extra pixels outside the visible area to avoid
+ // visual glitches due to non-zero width of dots.
+ let extraPixels = 0;
+ if (component === ChartSeriesComponent.DOTS) {
+ extraPixels = Math.max(
+ this.selectedPointSize_, this.unselectedPointSize_);
+ }
+ const pixelRatio = transform.pixelRatio;
+ const leftViewX = transform.leftViewX - extraPixels * pixelRatio;
+ const rightViewX = transform.rightViewX + extraPixels * pixelRatio;
+ const leftTimestamp = transform.leftTimestamp - extraPixels;
+ const rightTimestamp = transform.rightTimestamp + extraPixels;
+
+ // Find the index of the first and last (partially) visible points.
+ const firstVisibleIndex = tr.b.findLowIndexInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ leftTimestamp);
+ let lastVisibleIndex = tr.b.findLowIndexInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ rightTimestamp);
+ if (lastVisibleIndex >= this.points.length ||
+ this.points[lastVisibleIndex].x > rightTimestamp) {
+ lastVisibleIndex--;
+ }
+
+ // Pre-calculate component style which does not depend on individual
+ // points:
+ // * Skip distance between points,
+ // * Selected (circle) and unselected (square) dot size,
+ // * Unselected dot opacity,
+ // * Selected dot edge color and width, and
+ // * Line component color and width.
+ const viewSkipDistance = this.skipDistance_ * pixelRatio;
+ let selectedCircleRadius;
+ let letterDotRadius;
+ let squareSize;
+ let squareHalfSize;
+ let squareOpacity;
+ let unselectedSeriesColor;
+ let currentStateSeriesColor;
+
+ ctx.save();
+ ctx.font =
+ DOT_LETTER_FONT_WEIGHT + ' ' +
+ Math.floor(DOT_LETTER_FONT_SIZE_PX * pixelRatio) + 'px ' +
+ DOT_LETTER_FONT;
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+
+ switch (component) {
+ case ChartSeriesComponent.DOTS: {
+ // Selected (circle) and unselected (square) dot size.
+ selectedCircleRadius =
+ (this.selectedPointSize_ / 2) * pixelRatio;
+ letterDotRadius =
+ Math.max(selectedCircleRadius, DOT_LETTER_RADIUS_PX * pixelRatio);
+ squareSize = this.unselectedPointSize_ * pixelRatio;
+ squareHalfSize = squareSize / 2;
+ unselectedSeriesColor = EventPresenter.getCounterSeriesColor(
+ this.colorId_, SelectionState.NONE);
+
+ // Unselected dot opacity.
+ if (!highDetails) {
+ // Unselected dots are not displayed in 'low details' mode.
+ squareOpacity = 0;
+ break;
+ }
+ const visibleIndexRange = lastVisibleIndex - firstVisibleIndex;
+ if (visibleIndexRange <= 0) {
+ // There is at most one visible point.
+ squareOpacity = 1;
+ break;
+ }
+ const visibleViewXRange =
+ transform.worldXToViewX(this.points[lastVisibleIndex].x) -
+ transform.worldXToViewX(this.points[firstVisibleIndex].x);
+ if (visibleViewXRange === 0) {
+ // Multiple visible points which all have the same timestamp.
+ squareOpacity = 1;
+ break;
+ }
+ const density = visibleIndexRange / visibleViewXRange;
+ const clampedDensity = tr.b.math.clamp(density,
+ this.unselectedPointDensityOpaque_,
+ this.unselectedPointDensityTransparent_);
+ const densityRange = this.unselectedPointDensityTransparent_ -
+ this.unselectedPointDensityOpaque_;
+ squareOpacity =
+ (this.unselectedPointDensityTransparent_ - clampedDensity) /
+ densityRange;
+ break;
+ }
+
+ case ChartSeriesComponent.LINE:
+ // Line component color and width.
+ ctx.strokeStyle = EventPresenter.getCounterSeriesColor(
+ this.colorId_, SelectionState.NONE);
+ ctx.lineWidth = this.lineWidth_ * pixelRatio;
+ break;
+
+ case ChartSeriesComponent.BACKGROUND:
+ // Style depends on the selection state of individual points.
+ break;
+
+ default:
+ throw new Error('Invalid component: ' + component);
+ }
+
+ // The main loop which draws the given component of visible points from
+ // left to right. Given the potentially large number of points to draw,
+ // it should be considered performance-critical and function calls should
+ // be avoided when possible.
+ //
+ // Note that the background and line components are drawn in a delayed
+ // fashion: the rectangle/line that we draw in an iteration corresponds
+ // to the *previous* point. This does not apply to the dots, whose
+ // position is independent of the surrounding dots.
+ let previousViewX = undefined;
+ let previousViewY = undefined;
+ let previousViewYBase = undefined;
+ let lastSelectionState = undefined;
+ let baseSteps = undefined;
+ const startIndex = Math.max(firstVisibleIndex - 1, 0);
+ let currentViewX;
+
+ for (let i = startIndex; i < this.points.length; i++) {
+ const currentPoint = this.points[i];
+ currentViewX = transform.worldXToViewX(currentPoint.x);
+
+ // Stop drawing the points once we are to the right of the visible area.
+ if (currentViewX > rightViewX) {
+ if (previousViewX !== undefined) {
+ previousViewX = currentViewX = rightViewX;
+ if (component === ChartSeriesComponent.BACKGROUND ||
+ component === ChartSeriesComponent.LINE) {
+ ctx.lineTo(currentViewX, previousViewY);
+ }
+ }
+ break;
+ }
+
+ if (i + 1 < this.points.length) {
+ const nextPoint = this.points[i + 1];
+ const nextViewX = transform.worldXToViewX(nextPoint.x);
+
+ // Skip points that are too close to each other.
+ if (previousViewX !== undefined &&
+ nextViewX - previousViewX <= viewSkipDistance &&
+ nextViewX < rightViewX) {
+ continue;
+ }
+
+ // Start drawing right at the left side of the visible are (instead
+ // of potentially very far to the left).
+ if (currentViewX < leftViewX) {
+ currentViewX = leftViewX;
+ }
+ }
+
+ if (previousViewX !== undefined &&
+ currentViewX - previousViewX < viewSkipDistance) {
+ // We know that nextViewX > previousViewX + viewSkipDistance, so we
+ // can safely move this points's x over that much without passing
+ // nextViewX. This ensures that the previous point is visible when
+ // zoomed out very far.
+ currentViewX = previousViewX + viewSkipDistance;
+ }
+
+ const currentViewY = Math.round(transform.worldYToViewY(
+ currentPoint.y));
+ let currentViewYBase;
+ if (currentPoint.yBase === undefined) {
+ currentViewYBase = transform.outerBottomViewY;
+ } else {
+ currentViewYBase = Math.round(
+ transform.worldYToViewY(currentPoint.yBase));
+ }
+ const currentSelectionState = currentPoint.selectionState;
+ if (currentSelectionState !== lastSelectionState) {
+ const opacity = currentSelectionState === SelectionState.SELECTED ?
+ 1 : squareOpacity;
+ currentStateSeriesColor = EventPresenter.getCounterSeriesColor(
+ this.colorId_, currentSelectionState, opacity);
+ }
+
+ // Actually draw the given component of the point.
+ switch (component) {
+ case ChartSeriesComponent.DOTS:
+ // Draw the dot for the current point.
+ if (currentPoint.dotLetter) {
+ ctx.fillStyle = unselectedSeriesColor;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('black');
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY,
+ letterDotRadius + DOT_LETTER_RADIUS_PADDING_PX, 0,
+ 2 * Math.PI);
+ ctx.fill();
+ if (currentSelectionState === SelectionState.SELECTED) {
+ ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('olive');
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY, letterDotRadius, 0,
+ 2 * Math.PI);
+ ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('yellow');
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('black');
+ ctx.stroke();
+ }
+ ctx.fillStyle =
+ ColorScheme.getColorForReservedNameAsString('white');
+ ctx.fillText(currentPoint.dotLetter, currentViewX, currentViewY);
+ } else {
+ ctx.strokeStyle = unselectedSeriesColor;
+ ctx.lineWidth = pixelRatio;
+ if (currentSelectionState === SelectionState.SELECTED) {
+ if (this.solidSelectedDots_) {
+ ctx.fillStyle = ctx.strokeStyle;
+ } else {
+ ctx.fillStyle = currentStateSeriesColor;
+ }
+
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY, selectedCircleRadius, 0,
+ 2 * Math.PI);
+ ctx.fill();
+ ctx.stroke();
+ } else if (squareOpacity > 0) {
+ ctx.fillStyle = currentStateSeriesColor;
+ ctx.fillRect(currentViewX - squareHalfSize,
+ currentViewY - squareHalfSize, squareSize, squareSize);
+ }
+ }
+ break;
+
+ case ChartSeriesComponent.LINE:
+ // Draw the top line for the previous point (if applicable), or
+ // prepare for drawing the top line of the current point in the next
+ // iteration.
+ if (previousViewX === undefined) {
+ ctx.beginPath();
+ ctx.moveTo(currentViewX, currentViewY);
+ } else if (this.stepGraph_) {
+ ctx.lineTo(currentViewX, previousViewY);
+ }
+
+ // Move to the current point coordinate.
+ ctx.lineTo(currentViewX, currentViewY);
+ break;
+
+ case ChartSeriesComponent.BACKGROUND:
+ // Draw the background for the previous point (if applicable).
+ if (previousViewX !== undefined && this.stepGraph_) {
+ ctx.lineTo(currentViewX, previousViewY);
+ } else {
+ ctx.lineTo(currentViewX, currentViewY);
+ }
+
+ // Finish the bottom part of the backgound polygon, change
+ // background color and start a new polygon when the selection state
+ // changes (and at the beginning).
+ if (currentSelectionState !== lastSelectionState) {
+ if (previousViewX !== undefined) {
+ let previousBaseStepViewX = currentViewX;
+ for (let j = baseSteps.length - 1; j >= 0; j--) {
+ const baseStep = baseSteps[j];
+ const baseStepViewX = baseStep.viewX;
+ const baseStepViewY = baseStep.viewY;
+ ctx.lineTo(previousBaseStepViewX, baseStepViewY);
+ ctx.lineTo(baseStepViewX, baseStepViewY);
+ previousBaseStepViewX = baseStepViewX;
+ }
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.beginPath();
+ ctx.fillStyle = EventPresenter.getCounterSeriesColor(
+ this.colorId_, currentSelectionState,
+ this.backgroundOpacity_);
+ ctx.moveTo(currentViewX, currentViewYBase);
+ baseSteps = [];
+ }
+
+ if (currentViewYBase !== previousViewYBase ||
+ currentSelectionState !== lastSelectionState) {
+ baseSteps.push({viewX: currentViewX, viewY: currentViewYBase});
+ }
+
+ // Move to the current point coordinate.
+ ctx.lineTo(currentViewX, currentViewY);
+ break;
+
+ default:
+ throw new Error('Not reachable');
+ }
+
+ previousViewX = currentViewX;
+ previousViewY = currentViewY;
+ previousViewYBase = currentViewYBase;
+ lastSelectionState = currentSelectionState;
+ }
+
+ // If we still have an open background or top line polygon (which is
+ // always the case once we have started drawing due to the delayed fashion
+ // of drawing), we must close it.
+ if (previousViewX !== undefined) {
+ switch (component) {
+ case ChartSeriesComponent.DOTS:
+ // All dots were drawn in the main loop.
+ break;
+
+ case ChartSeriesComponent.LINE:
+ ctx.stroke();
+ break;
+
+ case ChartSeriesComponent.BACKGROUND: {
+ let previousBaseStepViewX = currentViewX;
+ for (let j = baseSteps.length - 1; j >= 0; j--) {
+ const baseStep = baseSteps[j];
+ const baseStepViewX = baseStep.viewX;
+ const baseStepViewY = baseStep.viewY;
+ ctx.lineTo(previousBaseStepViewX, baseStepViewY);
+ ctx.lineTo(baseStepViewX, baseStepViewY);
+ previousBaseStepViewX = baseStepViewX;
+ }
+ ctx.closePath();
+ ctx.fill();
+ break;
+ }
+
+ default:
+ throw new Error('Not reachable');
+ }
+ }
+ ctx.restore();
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ const points = this.points;
+
+ function getPointWidth(point, i) {
+ if (i === points.length - 1) {
+ return LAST_POINT_WIDTH * viewPixWidthWorld;
+ }
+ const nextPoint = points[i + 1];
+ return nextPoint.x - point.x;
+ }
+
+ function selectPoint(point) {
+ point.addToSelection(selection);
+ }
+
+ tr.b.iterateOverIntersectingIntervals(
+ this.points,
+ function(point) { return point.x; },
+ getPointWidth,
+ loWX,
+ hiWX,
+ selectPoint);
+ },
+
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (this.points === undefined) return false;
+
+ const index = this.points.findIndex(point => point.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex < 0 || newIndex >= this.points.length) return false;
+
+ this.points[newIndex].addToSelection(selection);
+ return true;
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ if (this.points === undefined) return;
+
+ const item = tr.b.findClosestElementInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ worldX,
+ worldMaxDist);
+
+ if (!item) return;
+
+ item.addToSelection(selection);
+ }
+ };
+
+ return {
+ ChartSeries,
+ ChartSeriesType,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html
new file mode 100644
index 00000000000..b07e4276e26
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html
@@ -0,0 +1,331 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const Event = tr.model.Event;
+ const SelectionState = tr.model.SelectionState;
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const ChartTransform = tr.ui.tracks.ChartTransform;
+ const ChartSeriesType = tr.ui.tracks.ChartSeriesType;
+
+ const CANVAS_WIDTH = 800;
+ const CANVAS_HEIGHT = 80;
+
+ function getSelectionStateForTesting(index) {
+ index = index % 7;
+ if (index < 5) {
+ return SelectionState.getFromBrighteningLevel(index % 4);
+ }
+ return SelectionState.getFromDimmingLevel(index % 3);
+ }
+
+ function buildSeries(renderingConfig) {
+ const points = [];
+ for (let i = 0; i < 60; i++) {
+ const event = new Event();
+ event.index = i;
+ const phase = i * Math.PI / 15;
+ const value = Math.sin(phase);
+ const peakIndex = Math.floor((phase + Math.PI / 2) / (2 * Math.PI));
+ const base = peakIndex % 2 === 0 ? undefined : -1 + value / 1.5;
+ const point = new ChartPoint(event, i - 30, value, base);
+ points.push(point);
+ }
+ const seriesYAxis = new ChartSeriesYAxis(-1, 1);
+ return new ChartSeries(points, seriesYAxis, renderingConfig);
+ }
+
+ function drawSeriesWithDetails(test, series, highDetails) {
+ const div = document.createElement('div');
+ const canvas = document.createElement('canvas');
+ Polymer.dom(div).appendChild(canvas);
+
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ canvas.width = CANVAS_WIDTH * pixelRatio;
+ canvas.style.width = CANVAS_WIDTH + 'px';
+ canvas.height = CANVAS_HEIGHT * pixelRatio;
+ canvas.style.height = CANVAS_HEIGHT + 'px';
+
+ const displayTransform = new TimelineDisplayTransform();
+ displayTransform.scaleX = CANVAS_WIDTH * pixelRatio / 60;
+ displayTransform.panX = 30;
+
+ const transform = new ChartTransform(
+ displayTransform,
+ series.seriesYAxis,
+ CANVAS_WIDTH * pixelRatio,
+ CANVAS_HEIGHT * pixelRatio,
+ 10 * pixelRatio,
+ 10 * pixelRatio,
+ pixelRatio);
+
+ series.draw(canvas.getContext('2d'), transform, highDetails);
+
+ test.addHTMLOutput(div);
+ }
+
+ function drawSeries(test, series) {
+ drawSeriesWithDetails(test, series, false);
+ drawSeriesWithDetails(test, series, true);
+ series.stepGraph_ = !series.stepGraph_;
+ drawSeriesWithDetails(test, series, false);
+ drawSeriesWithDetails(test, series, true);
+ }
+
+ test('instantiate_defaultConfig', function() {
+ const series = buildSeries(undefined);
+ drawSeries(this, series);
+ });
+
+ test('instantiate_lineChart', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.LINE,
+ colorId: 4,
+ unselectedPointSize: 6,
+ lineWidth: 2,
+ unselectedPointDensityOpaque: 0.08
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_areaChart', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 2,
+ backgroundOpacity: 0.2
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_largeSkipDistance', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 1,
+ skipDistance: 40,
+ unselectedPointDensityTransparent: 0.07
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selection', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithSolidDots', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ solidSelectedDots: true,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithAllConfigFlags', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ colorId: 15,
+ lineWidth: 2,
+ skipDistance: 25,
+ unselectedPointDensityOpaque: 0.07,
+ unselectedPointDensityTransparent: 0.09,
+ backgroundOpacity: 0.8
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithDotLetters', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ solidSelectedDots: true,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ if (index % 10 === 3) {
+ point.dotLetter = 'P';
+ } else if (index % 10 === 7) {
+ point.dotLetter = '\u26A0';
+ }
+ });
+ drawSeries(this, series);
+ });
+
+ test('checkRange', function() {
+ const series = buildSeries();
+ const range = series.range;
+ assert.isFalse(range.isEmpty);
+ assert.closeTo(range.min, -1, 0.05);
+ assert.closeTo(range.max, 1, 0.05);
+ });
+
+ test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() {
+ const series = buildSeries();
+
+ // Too far left.
+ let sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -1000, -30.5, 40, sel);
+ assert.lengthOf(sel, 0);
+
+ // Select first point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -30.5, -29.5, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 0);
+
+ // Select second point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -28.8, -28.2, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 1);
+
+ // Select points in the middle.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -0.99, 1.01, 40, sel);
+ assert.lengthOf(sel, 3);
+ const iterator = sel[Symbol.iterator]();
+ assert.strictEqual(iterator.next().value.index, 29);
+ assert.strictEqual(iterator.next().value.index, 30);
+ assert.strictEqual(iterator.next().value.index, 31);
+
+ // Select the last point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 668.99, 668.99, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 59);
+
+ // Too far right.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 669.01, 2000, 40, sel);
+ assert.lengthOf(sel, 0);
+
+ // Select everything.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -29.01, 669.01, 40, sel);
+ assert.lengthOf(sel, 60);
+ });
+
+ test('checkaddEventNearToProvidedEventToSelection', function() {
+ const series = buildSeries();
+
+ // Invalid event.
+ let sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ new Event(), 1, sel));
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ new Event(), -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // First point.
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[0].modelItem, 1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 1);
+
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ series.points[0].modelItem, -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // Middle point.
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[30].modelItem, 1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 31);
+
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[30].modelItem, -1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 29);
+
+ // Last point.
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ series.points[59].modelItem, 1, sel));
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[59].modelItem, -1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 58);
+ });
+
+ test('checkAddClosestEventToSelection', function() {
+ const series = buildSeries();
+
+ // Left of first point.
+ let sel = new EventSet();
+ series.addClosestEventToSelection(-40, 9, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(-40, 11, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 0);
+
+ // Between two points.
+ sel = new EventSet();
+ series.addClosestEventToSelection(0.4, 0.3, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(0.4, 0.4, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 30);
+
+ // Right of last point.
+ sel = new EventSet();
+ series.addClosestEventToSelection(40, 10, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(40, 12, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 59);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html
new file mode 100644
index 00000000000..f34b4c68579
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html
@@ -0,0 +1,213 @@
+<!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/color_scheme.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const IDEAL_MAJOR_MARK_HEIGHT_PX = 30;
+ const AXIS_LABLE_MARGIN_PX = 10;
+ const AXIS_LABLE_FONT_SIZE_PX = 9;
+ const AXIS_LABLE_FONT = 'Arial';
+
+ /**
+ * A vertical axis for a (set of) chart series which maps an arbitrary range
+ * of values [min, max] to the unit range [0, 1].
+ *
+ * @constructor
+ */
+ function ChartSeriesYAxis(opt_min, opt_max) {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.bounds = new tr.b.math.Range();
+ if (opt_min !== undefined) this.bounds.addValue(opt_min);
+ if (opt_max !== undefined) this.bounds.addValue(opt_max);
+ }
+
+ ChartSeriesYAxis.prototype = {
+ get guid() {
+ return this.guid_;
+ },
+
+ valueToUnitRange(value) {
+ if (this.bounds.isEmpty) {
+ throw new Error('Chart series y-axis bounds are empty');
+ }
+ const bounds = this.bounds;
+ if (bounds.range === 0) return 0;
+ return (value - bounds.min) / bounds.range;
+ },
+
+ unitRangeToValue(unitRange) {
+ if (this.bounds.isEmpty) {
+ throw new Error('Chart series y-axis bounds are empty');
+ }
+ return unitRange * this.bounds.range + this.bounds.min;
+ },
+
+ /**
+ * Automatically set the y-axis bounds from the range of values of all
+ * series in a list.
+ *
+ * See the description of autoSetFromRange for the optional configuration
+ * argument flags.
+ */
+ autoSetFromSeries(series, opt_config) {
+ const range = new tr.b.math.Range();
+ series.forEach(function(s) {
+ range.addRange(s.range);
+ }, this);
+ this.autoSetFromRange(range, opt_config);
+ },
+
+ /**
+ * Automatically set the y-axis bound from a range of values.
+ *
+ * The following four flags, which affect the behavior of this method with
+ * respect to already defined bounds, can be present in the optional
+ * configuration (a flag is assumed to be false if it is not provided or if
+ * the configuration is not provided):
+ *
+ * - expandMin: allow decreasing the min bound (if range.min < this.min)
+ * - shrinkMin: allow increasing the min bound (if range.min > this.min)
+ * - expandMax: allow increasing the max bound (if range.max > this.max)
+ * - shrinkMax: allow decreasing the max bound (if range.max < this.max)
+ *
+ * This method will ensure that the resulting bounds are defined and valid
+ * (i.e. min <= max) provided that they were valid or empty before and the
+ * value range is non-empty and valid.
+ *
+ * Note that unless expanding/shrinking a bound is explicitly enabled in
+ * the configuration, non-empty bounds will not be changed under any
+ * circumstances.
+ *
+ * Observe that if no configuration is provided (or all flags are set to
+ * false), this method will only modify the y-axis bounds if they are empty.
+ */
+ autoSetFromRange(range, opt_config) {
+ if (range.isEmpty) return;
+
+ const bounds = this.bounds;
+ if (bounds.isEmpty) {
+ bounds.addRange(range);
+ return;
+ }
+
+ if (!opt_config) return;
+
+ const useRangeMin = (opt_config.expandMin && range.min < bounds.min ||
+ opt_config.shrinkMin && range.min > bounds.min);
+ const useRangeMax = (opt_config.expandMax && range.max > bounds.max ||
+ opt_config.shrinkMax && range.max < bounds.max);
+
+ // Neither bound is modified.
+ if (!useRangeMin && !useRangeMax) return;
+
+ // Both bounds are modified. Assuming the range argument is a valid
+ // range, no extra checks are necessary.
+ if (useRangeMin && useRangeMax) {
+ bounds.min = range.min;
+ bounds.max = range.max;
+ return;
+ }
+
+ // Only one bound is modified. We must ensure that it doesn't go
+ // over/under the other (unmodified) bound.
+ if (useRangeMin) {
+ bounds.min = Math.min(range.min, bounds.max);
+ } else {
+ bounds.max = Math.max(range.max, bounds.min);
+ }
+ },
+
+
+ majorMarkHeightWorld_(transform, pixelRatio) {
+ const idealMajorMarkHeightPx = IDEAL_MAJOR_MARK_HEIGHT_PX * pixelRatio;
+ const idealMajorMarkHeightWorld =
+ transform.vectorToWorldDistance(idealMajorMarkHeightPx);
+
+ return tr.b.math.preferredNumberLargerThanMin(idealMajorMarkHeightWorld);
+ },
+
+ draw(ctx, transform, showYAxisLabels, showYGridLines) {
+ if (!showYAxisLabels && !showYGridLines) return;
+
+ const pixelRatio = transform.pixelRatio;
+ const viewTop = transform.outerTopViewY;
+ const worldTop = transform.viewYToWorldY(viewTop);
+ const viewBottom = transform.outerBottomViewY;
+ const viewHeight = viewBottom - viewTop;
+ const viewLeft = transform.leftViewX;
+ const viewRight = transform.rightViewX;
+ const labelLeft = transform.leftYLabel;
+
+ ctx.save();
+ ctx.lineWidth = pixelRatio;
+ ctx.fillStyle = ColorScheme.getColorForReservedNameAsString('black');
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'center';
+
+ ctx.font =
+ (AXIS_LABLE_FONT_SIZE_PX * pixelRatio) + 'px ' + AXIS_LABLE_FONT;
+
+ // Draw left edge of chart series.
+ ctx.beginPath();
+ ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('black');
+ tr.ui.b.drawLine(
+ ctx, viewLeft, viewTop, viewLeft, viewBottom, viewLeft);
+ ctx.stroke();
+ ctx.closePath();
+
+ // Draw y-axis ticks and gridlines.
+ ctx.beginPath();
+ ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('grey');
+
+ const majorMarkHeight = this.majorMarkHeightWorld_(transform, pixelRatio);
+ const maxMajorMark = Math.max(transform.viewYToWorldY(viewTop),
+ Math.abs(transform.viewYToWorldY(viewBottom)));
+ for (let curWorldY = 0;
+ curWorldY <= maxMajorMark;
+ curWorldY += majorMarkHeight) {
+ const roundedUnitValue = Math.floor(curWorldY * 1000000) / 1000000;
+ const curViewYPositive = transform.worldYToViewY(curWorldY);
+ if (curViewYPositive >= viewTop) {
+ if (showYAxisLabels) {
+ ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
+ curViewYPositive - AXIS_LABLE_MARGIN_PX);
+ }
+ if (showYGridLines) {
+ tr.ui.b.drawLine(
+ ctx, viewLeft, curViewYPositive, viewRight, curViewYPositive);
+ }
+ }
+
+ const curViewYNegative = transform.worldYToViewY(-1 * curWorldY);
+ if (curViewYNegative <= viewBottom) {
+ if (showYAxisLabels) {
+ ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
+ curViewYNegative - AXIS_LABLE_MARGIN_PX);
+ }
+ if (showYGridLines) {
+ tr.ui.b.drawLine(
+ ctx, viewLeft, curViewYNegative, viewRight, curViewYNegative);
+ }
+ }
+ }
+ ctx.stroke();
+ ctx.restore();
+ }
+ };
+
+ return {
+ ChartSeriesYAxis,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html
new file mode 100644
index 00000000000..4a759e040d4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html
@@ -0,0 +1,313 @@
+<!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/range.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const Range = tr.b.math.Range;
+
+ function buildRange() {
+ const range = new Range();
+ for (let i = 0; i < arguments.length; i++) {
+ range.addValue(arguments[i]);
+ }
+ return range;
+ }
+
+ function buildSeries() {
+ const points = [];
+ for (let i = 0; i < arguments.length; i++) {
+ points.push(new ChartPoint(undefined, i, arguments[i]));
+ }
+ return new ChartSeries(points, new ChartSeriesYAxis());
+ }
+
+ test('instantiate_emptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis();
+ assert.isTrue(seriesYAxis.bounds.isEmpty);
+ });
+
+ test('instantiate_nonEmptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-2, 12);
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -2);
+ assert.strictEqual(seriesYAxis.bounds.max, 12);
+ });
+
+ test('instantiate_equalBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(2.72);
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 2.72);
+ assert.strictEqual(seriesYAxis.bounds.max, 2.72);
+ });
+
+ test('checkValueToUnitRange_emptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis();
+ assert.throws(function() { seriesYAxis.valueToUnitRange(42); });
+ });
+
+ test('checkValueToUnitRange_nonEmptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(10, 20);
+
+ assert.strictEqual(seriesYAxis.valueToUnitRange(0), -1);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(10), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(15), 0.5);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(20), 1);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(30), 2);
+ });
+
+ test('checkValueToUnitRange_equalBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(3.14);
+
+ assert.strictEqual(seriesYAxis.valueToUnitRange(0), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(3.14), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(6.28), 0);
+ });
+
+ test('checkAutoSetFromRange_emptyBounds', function() {
+ // Empty range.
+ let seriesYAxis = new ChartSeriesYAxis();
+ seriesYAxis.autoSetFromRange(buildRange());
+ assert.isTrue(seriesYAxis.bounds.isEmpty);
+
+ // Non-empty range.
+ seriesYAxis = new ChartSeriesYAxis();
+ seriesYAxis.autoSetFromRange(buildRange(-1, 3));
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -1);
+ assert.strictEqual(seriesYAxis.bounds.max, 3);
+ });
+
+ test('checkAutoSetFromRange_nonEmptyBounds', function() {
+ // Empty range.
+ let seriesYAxis = new ChartSeriesYAxis(0, 1);
+ seriesYAxis.autoSetFromRange(buildRange());
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 1);
+
+ // No configuration.
+ seriesYAxis = new ChartSeriesYAxis(2, 3);
+ seriesYAxis.autoSetFromRange(buildRange(1, 4));
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 2);
+ assert.strictEqual(seriesYAxis.bounds.max, 3);
+
+ // Allow expanding min.
+ seriesYAxis = new ChartSeriesYAxis(-2, -1);
+ seriesYAxis.autoSetFromRange(buildRange(-3, 0), {expandMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -3);
+ assert.strictEqual(seriesYAxis.bounds.max, -1);
+
+ // Allow shrinking min.
+ seriesYAxis = new ChartSeriesYAxis(-2, -1);
+ seriesYAxis.autoSetFromRange(buildRange(-1.5, 0.5), {shrinkMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -1.5);
+ assert.strictEqual(seriesYAxis.bounds.max, -1);
+
+ seriesYAxis = new ChartSeriesYAxis(7, 8);
+ seriesYAxis.autoSetFromRange(buildRange(9, 10), {shrinkMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 8);
+ assert.strictEqual(seriesYAxis.bounds.max, 8);
+
+ // Allow expanding max.
+ seriesYAxis = new ChartSeriesYAxis(19, 20);
+ seriesYAxis.autoSetFromRange(buildRange(18, 21), {expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 19);
+ assert.strictEqual(seriesYAxis.bounds.max, 21);
+
+ // Allow shrinking max.
+ seriesYAxis = new ChartSeriesYAxis(30, 32);
+ seriesYAxis.autoSetFromRange(buildRange(29, 31), {shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 30);
+ assert.strictEqual(seriesYAxis.bounds.max, 31);
+
+ seriesYAxis = new ChartSeriesYAxis(41, 42);
+ seriesYAxis.autoSetFromRange(buildRange(39, 40), {shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 41);
+ assert.strictEqual(seriesYAxis.bounds.max, 41);
+
+ // Allow shrinking both bounds.
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(51, 52),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 51);
+ assert.strictEqual(seriesYAxis.bounds.max, 52);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(49, 52),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 50);
+ assert.strictEqual(seriesYAxis.bounds.max, 52);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(51, 54),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 51);
+ assert.strictEqual(seriesYAxis.bounds.max, 53);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(49, 54),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 50);
+ assert.strictEqual(seriesYAxis.bounds.max, 53);
+
+ // Allow expanding both bounds.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(0, 100),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.5, 100),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(0, 60.5),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ // Allow shrinking min and expanding max.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(62, 63),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 62);
+ assert.strictEqual(seriesYAxis.bounds.max, 63);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 63),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 63);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60.2);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 60.5),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ // Allow expanding min and shrinking max.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(62, 63),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 63),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 59);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 60.8);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 60.5),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 59);
+ assert.strictEqual(seriesYAxis.bounds.max, 60.5);
+
+ // Allow everything.
+ seriesYAxis = new ChartSeriesYAxis(200, 250);
+ seriesYAxis.autoSetFromRange(buildRange(150, 175),
+ {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 150);
+ assert.strictEqual(seriesYAxis.bounds.max, 175);
+
+ seriesYAxis = new ChartSeriesYAxis(0, 0.1);
+ seriesYAxis.autoSetFromRange(buildRange(0.2, 0.3),
+ {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0.2);
+ assert.strictEqual(seriesYAxis.bounds.max, 0.3);
+ });
+
+ test('checkAutoSetFromSeries_noSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [];
+
+ seriesYAxis.autoSetFromSeries(series);
+ assert.strictEqual(seriesYAxis.bounds.min, -100);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+ });
+
+ test('checkAutoSetFromSeries_oneSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [buildSeries(-80, 100, -40, 200)];
+
+ seriesYAxis.autoSetFromSeries(series, {shrinkMin: true, expandMax: true});
+ assert.strictEqual(seriesYAxis.bounds.min, -80);
+ assert.strictEqual(seriesYAxis.bounds.max, 200);
+ });
+
+ test('checkAutoSetFromSeries_multipleSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [
+ buildSeries(0, 20, 10, 30),
+ buildSeries(),
+ buildSeries(-500)
+ ];
+
+ seriesYAxis.autoSetFromSeries(series, {expandMin: true, shrinkMax: true});
+ assert.strictEqual(seriesYAxis.bounds.min, -500);
+ assert.strictEqual(seriesYAxis.bounds.max, 30);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html
new file mode 100644
index 00000000000..58ef08d651c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html
@@ -0,0 +1,281 @@
+<!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/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.chart-track {
+ height: 30px;
+ position: relative;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a chart.
+ *
+ * @constructor
+ * @extends {Track}
+ */
+ const ChartTrack =
+ tr.ui.b.define('chart-track', tr.ui.tracks.Track);
+
+ ChartTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('chart-track');
+ this.series_ = undefined;
+ this.axes_ = undefined;
+
+ // GUID -> {axis: ChartSeriesYAxis, series: [ChartSeries]}.
+ this.axisGuidToAxisData_ = undefined;
+
+ // The maximum top and bottom padding of all series.
+ this.topPadding_ = undefined;
+ this.bottomPadding_ = undefined;
+
+ this.showYAxisLabels_ = undefined;
+ this.showGridLines_ = undefined;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get series() {
+ return this.series_;
+ },
+
+ /**
+ * Set the list of chart series to be displayed on this track. The list
+ * is assumed to be sorted in increasing z-order (i.e. the last series in
+ * the list will be drawn at the top).
+ */
+ set series(series) {
+ this.series_ = series;
+ this.calculateAxisDataAndPadding_();
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ this.invalidateDrawingContainer();
+ },
+
+ get showYAxisLabels() {
+ return this.showYAxisLabels_;
+ },
+
+ set showYAxisLabels(showYAxisLabels) {
+ this.showYAxisLabels_ = showYAxisLabels;
+ this.invalidateDrawingContainer();
+ },
+
+ get showGridLines() {
+ return this.showGridLines_;
+ },
+
+ set showGridLines(showGridLines) {
+ this.showGridLines_ = showGridLines;
+ this.invalidateDrawingContainer();
+ },
+
+ get hasVisibleContent() {
+ return !!this.series && this.series.length > 0;
+ },
+
+ calculateAxisDataAndPadding_() {
+ if (!this.series_) {
+ this.axes_ = undefined;
+ this.axisGuidToAxisData_ = undefined;
+ this.topPadding_ = undefined;
+ this.bottomPadding_ = undefined;
+ return;
+ }
+
+ const axisGuidToAxisData = {};
+ let topPadding = 0;
+ let bottomPadding = 0;
+
+ this.series_.forEach(function(series) {
+ const seriesYAxis = series.seriesYAxis;
+ const axisGuid = seriesYAxis.guid;
+ if (!(axisGuid in axisGuidToAxisData)) {
+ axisGuidToAxisData[axisGuid] = {
+ axis: seriesYAxis,
+ series: []
+ };
+ if (!this.axes_) this.axes_ = [];
+ this.axes_.push(seriesYAxis);
+ }
+ axisGuidToAxisData[axisGuid].series.push(series);
+ topPadding = Math.max(topPadding, series.topPadding);
+ bottomPadding = Math.max(bottomPadding, series.bottomPadding);
+ }, this);
+
+ this.axisGuidToAxisData_ = axisGuidToAxisData;
+ this.topPadding_ = topPadding;
+ this.bottomPadding_ = bottomPadding;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawChart_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawChart_(viewLWorld, viewRWorld) {
+ if (!this.series_) return;
+
+ const ctx = this.context();
+
+ // Get track drawing parameters.
+ const displayTransform = this.viewport.currentDisplayTransform;
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const highDetails = this.viewport.highDetails;
+
+ // Pre-multiply all device-independent pixel parameters with the pixel
+ // ratio to avoid unnecessary recomputation in the performance-critical
+ // drawing code.
+ const width = bounds.width * pixelRatio;
+ const height = bounds.height * pixelRatio;
+ const topPadding = this.topPadding_ * pixelRatio;
+ const bottomPadding = this.bottomPadding_ * pixelRatio;
+
+ // Set up clipping.
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(0, 0, width, height);
+ ctx.clip();
+
+ // TODO(aiolos): Add support for secondary y-axis on right side of chart.
+ // https://github.com/catapult-project/catapult/issues/3008
+ // Draw y-axis grid lines.
+ if (this.axes_) {
+ if ((this.showGridLines_ || this.showYAxisLabels_) &&
+ this.axes_.length > 1) {
+ throw new Error('Only one axis allowed when showing grid lines.');
+ }
+ for (const yAxis of this.axes_) {
+ const chartTransform = new tr.ui.tracks.ChartTransform(
+ displayTransform, yAxis, width, height,
+ topPadding, bottomPadding, pixelRatio);
+ yAxis.draw(
+ ctx, chartTransform, this.showYAxisLabels_, this.showGridLines_);
+ }
+ }
+
+ // Draw all series in the increasing z-order.
+ for (const series of this.series) {
+ const chartTransform = new tr.ui.tracks.ChartTransform(
+ displayTransform, series.seriesYAxis, width, height, topPadding,
+ bottomPadding, pixelRatio);
+ series.draw(ctx, chartTransform, highDetails);
+ }
+
+ // Stop clipping.
+ ctx.restore();
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ // TODO(petrcermak): Consider adding the series to the track map instead
+ // of the track (a potential performance optimization).
+ this.series_.forEach(function(series) {
+ series.points.forEach(function(point) {
+ point.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ this.series_.forEach(function(series) {
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection);
+ }, this);
+ },
+
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ let foundItem = false;
+ this.series_.forEach(function(series) {
+ foundItem = foundItem || series.addEventNearToProvidedEventToSelection(
+ event, offset, selection);
+ }, this);
+ return foundItem;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ // Do nothing.
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.series_.forEach(function(series) {
+ series.addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }, this);
+ },
+
+ /**
+ * Automatically set the bounds of all axes on this track from the range of
+ * values of all series (in this track) associated with each of them.
+ *
+ * See the description of ChartSeriesYAxis.autoSetFromRange for the optional
+ * configuration argument flags.
+ */
+ autoSetAllAxes(opt_config) {
+ for (const axisData of Object.values(this.axisGuidToAxisData_)) {
+ const seriesYAxis = axisData.axis;
+ const series = axisData.series;
+ seriesYAxis.autoSetFromSeries(series, opt_config);
+ }
+ },
+
+ /**
+ * Automatically set the bounds of the provided axis from the range of
+ * values of all series (in this track) associated with it.
+ *
+ * See the description of ChartSeriesYAxis.autoSetFromRange for the optional
+ * configuration argument flags.
+ */
+ autoSetAxis(seriesYAxis, opt_config) {
+ const series = this.axisGuidToAxisData_[seriesYAxis.guid].series;
+ seriesYAxis.autoSetFromSeries(series, opt_config);
+ }
+ };
+
+ return {
+ ChartTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html
new file mode 100644
index 00000000000..405640a9b2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html
@@ -0,0 +1,454 @@
+<!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/xhr.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const ChartSeriesType = tr.ui.tracks.ChartSeriesType;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+ const Event = tr.model.Event;
+ const EventSet = tr.model.EventSet;
+ const EventToTrackMap = tr.ui.tracks.EventToTrackMap;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ function buildPoint(x, y) {
+ const event = new Event();
+ return new ChartPoint(event, x, y);
+ }
+
+ function buildTrack(opt_args) {
+ const viewport = (opt_args && opt_args.viewport) ?
+ opt_args.viewport : new Viewport(document.createElement('div'));
+
+ const seriesYAxis1 = new ChartSeriesYAxis(0, 2.5);
+
+ const points1 = [
+ buildPoint(-2.5, 2),
+ buildPoint(-1.5, 1),
+ buildPoint(-0.5, 0),
+ buildPoint(0.5, 1),
+ buildPoint(1.5, 2),
+ buildPoint(2.5, 0)
+ ];
+ const renderingConfig1 = {
+ chartType: ChartSeriesType.AREA,
+ colorId: 6,
+ selectedPointSize: 7
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig1.stepGraph = opt_args.stepGraph;
+ }
+ const series1 = new ChartSeries(points1, seriesYAxis1, renderingConfig1);
+
+ const points2 = [
+ buildPoint(-2.3, 0.2),
+ buildPoint(-1.3, 1.2),
+ buildPoint(-0.3, 2.2),
+ buildPoint(0.3, 1.2),
+ buildPoint(1.3, 0.2),
+ buildPoint(2.3, 0)
+ ];
+ const renderingConfig2 = {
+ chartType: ChartSeriesType.AREA,
+ colorId: 4,
+ selectedPointSize: 10
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig2.stepGraph = opt_args.stepGraph;
+ }
+ const series2 = new ChartSeries(points2, seriesYAxis1, renderingConfig2);
+
+ const seriesList = [series1, series2];
+
+ if (!opt_args || !opt_args.singleAxis) {
+ const seriesYAxis2 = new ChartSeriesYAxis(-100, 100);
+ const points3 = [
+ buildPoint(-3, -50),
+ buildPoint(-2.4, -40),
+ buildPoint(-1.8, -30),
+ buildPoint(-1.2, -20),
+ buildPoint(-0.6, -10),
+ buildPoint(0, 0),
+ buildPoint(0.6, 10),
+ buildPoint(1.2, 20),
+ buildPoint(1.8, 30),
+ buildPoint(2.4, 40),
+ buildPoint(3, 50)
+ ];
+ const renderingConfig3 = {
+ chartType: ChartSeriesType.LINE,
+ lineWidth: 2
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig3.stepGraph = opt_args.stepGraph;
+ }
+ const series3 = new ChartSeries(points3, seriesYAxis2, renderingConfig3);
+ seriesList.push(series3);
+ }
+
+ const track = new ChartTrack(viewport);
+ track.series = seriesList;
+
+ return track;
+ }
+
+ function buildDashboardTrack(opt_viewport) {
+ const viewport = opt_viewport || new Viewport(
+ document.createElement('div'));
+
+ const seriesYAxis = new ChartSeriesYAxis(0, 1.1);
+ const fileUrl = '/test_data/dashboard_test_points.json';
+ const pointsArray = JSON.parse(tr.b.getSync(fileUrl));
+ const points = [];
+ for (let i = 0; i < pointsArray.length; i++) {
+ points.push(buildPoint(pointsArray[i][0], pointsArray[i][1]));
+ }
+ const renderingConfig = {
+ chartType: ChartSeriesType.LINE,
+ lineWidth: 1,
+ stepGraph: false,
+ selectedPointSize: 10,
+ solidSelectedDots: true,
+ highDetail: false,
+ skipDistance: 0.4
+ };
+ const series = new ChartSeries(points, seriesYAxis, renderingConfig);
+
+ const track = new ChartTrack(viewport);
+ track.series = [series];
+
+ return track;
+ }
+
+ test('instantiate_lowDetailsWithoutSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[2].points[3].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_lowDetailsNoStepGraphWithoutSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport, stepGraph: false});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport, stepGraph: false});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[2].points[3].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionAndYAxisLabels',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showYAxisLabels = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionAndGridLines',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionYAxisLabelsAndGridLines',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showYAxisLabels = true;
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_dashboardChartStyleWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildDashboardTrack(viewport);
+ track.showYAxisLabels = true;
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[40].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(
+ 26610390797802200, 28950000891700000, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('checkPadding', function() {
+ const track = buildTrack();
+
+ // Padding should be equal to half maximum point size.
+ assert.strictEqual(track.topPadding_, 5);
+ assert.strictEqual(track.bottomPadding_, 5);
+ });
+
+ test('checkAddEventsToTrackMap', function() {
+ const track = buildTrack();
+ const eventToTrackMap = new EventToTrackMap();
+ track.addEventsToTrackMap(eventToTrackMap);
+ assert.lengthOf(Object.keys(eventToTrackMap), 23);
+ });
+
+ test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() {
+ const track = buildTrack();
+
+ const sel = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -1.1, -0.7, 0.01, sel);
+ assert.lengthOf(sel, 3);
+ const iter = sel[Symbol.iterator]();
+ assert.strictEqual(iter.next().value, track.series[0].points[1].modelItem);
+ assert.strictEqual(iter.next().value, track.series[1].points[1].modelItem);
+ assert.strictEqual(iter.next().value, track.series[2].points[3].modelItem);
+ });
+
+ test('checkaddEventNearToProvidedEventToSelection', function() {
+ const track = buildTrack();
+
+ // Fail to find a near item to the left in any series.
+ let sel = new EventSet();
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ track.series[0].points[0].modelItem, -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // Succeed at finding a near item to the right of one series.
+ sel = new EventSet();
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ track.series[1].points[1].modelItem, 1, sel));
+ assert.strictEqual(
+ tr.b.getOnlyElement(sel), track.series[1].points[2].modelItem);
+ });
+
+ test('checkAddClosestEventToSelection', function() {
+ const track = buildTrack();
+
+ const sel = new EventSet();
+ track.addClosestEventToSelection(-0.8, 0.4, 0.5, 1.5, sel);
+ assert.lengthOf(sel, 2);
+ const iter = sel[Symbol.iterator]();
+ assert.strictEqual(iter.next().value, track.series[0].points[2].modelItem);
+ assert.strictEqual(iter.next().value, track.series[2].points[4].modelItem);
+ });
+
+ test('checkAutoSetAllAxes', function() {
+ const track = buildTrack();
+ const seriesYAxis1 = track.series[0].seriesYAxis;
+ const seriesYAxis2 = track.series[2].seriesYAxis;
+
+ track.autoSetAllAxes({expandMax: true, shrinkMax: true});
+
+ // Min bounds of both axes should not have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.min, 0);
+ assert.strictEqual(seriesYAxis2.bounds.min, -100);
+
+ // Max bounds of both axes should have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.max, 2.2);
+ assert.strictEqual(seriesYAxis2.bounds.max, 50);
+ });
+
+ test('checkAutoSetAxis', function() {
+ const track = buildTrack();
+ const seriesYAxis1 = track.series[0].seriesYAxis;
+ const seriesYAxis2 = track.series[2].seriesYAxis;
+
+ track.autoSetAxis(seriesYAxis2,
+ {expandMin: true, shrinkMin: true, expandMax: true, shrinkMax: true});
+
+ // First axis should not have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.min, 0);
+ assert.strictEqual(seriesYAxis1.bounds.max, 2.5);
+
+ // Second axis should have been modified.
+ assert.strictEqual(seriesYAxis2.bounds.min, -50);
+ assert.strictEqual(seriesYAxis2.bounds.max, 50);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html
new file mode 100644
index 00000000000..f6bf6310116
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html
@@ -0,0 +1,92 @@
+<!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/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A helper object encapsulating all parameters necessary to draw a chart
+ * series and provides conversion between world coordinates and physical
+ * pixels.
+ *
+ * All parameters (except for pixelRatio) are assumed to be in physical pixels
+ * (i.e. already pre-multiplied with pixelRatio).
+ *
+ * The diagram below explains the meaning of the resulting fields with
+ * respect to a chart track:
+ *
+ * outerTopViewY -> +--------------------/-\------+ <- Top padding
+ * innerTopViewY -> + - - - - - - - - - -| |- - - + <- Axis max
+ * | .. ==\-/== |
+ * | === Series === |
+ * | ==/-\== .. |
+ * innerBottomViewY -> + - - -Point- - - - - - - - - + <- Axis min
+ * outerBottomViewY -> +-------\-/-------------------+ <- Bottom padding
+ * ^ ^
+ * leftViewX rightViewX
+ * leftTimeStamp rightTimestamp
+ *
+ * Labels starting with a lower case letter are the resulting fields of the
+ * transform object. Labels starting with an upper case letter correspond
+ * to the relevant chart track concepts.
+ *
+ * @constructor
+ */
+ function ChartTransform(displayTransform, axis, trackWidth,
+ trackHeight, topPadding, bottomPadding, pixelRatio) {
+ this.pixelRatio = pixelRatio;
+
+ // X axis.
+ this.leftViewX = 0;
+ this.rightViewX = trackWidth;
+ this.leftTimestamp = displayTransform.xViewToWorld(this.leftViewX);
+ this.rightTimestamp = displayTransform.xViewToWorld(this.rightViewX);
+
+ this.displayTransform_ = displayTransform;
+
+ // Y axis.
+ this.outerTopViewY = 0;
+ this.innerTopViewY = topPadding;
+ this.innerBottomViewY = trackHeight - bottomPadding;
+ this.outerBottomViewY = trackHeight;
+
+ this.axis_ = axis;
+ this.innerHeight_ = this.innerBottomViewY - this.innerTopViewY;
+ }
+
+ ChartTransform.prototype = {
+ worldXToViewX(worldX) {
+ return this.displayTransform_.xWorldToView(worldX);
+ },
+
+ viewXToWorldX(viewX) {
+ return this.displayTransform_.xViewToWorld(viewX);
+ },
+
+ vectorToWorldDistance(viewY) {
+ return this.axis_.bounds.range * Math.abs(viewY / this.innerHeight_);
+ },
+
+ viewYToWorldY(viewY) {
+ return this.axis_.unitRangeToValue(
+ 1 - (viewY - this.innerTopViewY) / this.innerHeight_);
+ },
+
+ worldYToViewY(worldY) {
+ const innerHeightCoefficient = 1 - this.axis_.valueToUnitRange(worldY);
+ return innerHeightCoefficient * this.innerHeight_ + this.innerTopViewY;
+ }
+ };
+
+ return {
+ ChartTransform,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html
new file mode 100644
index 00000000000..8d46e08aace
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html
@@ -0,0 +1,106 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const ChartTransform = tr.ui.tracks.ChartTransform;
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+
+ function buildChartTransform() {
+ const displayTransform = new TimelineDisplayTransform();
+ displayTransform.panX = -20;
+ displayTransform.scaleX = 0.5;
+
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+
+ const chartTransform = new ChartTransform(
+ displayTransform,
+ seriesYAxis,
+ 500, /* trackWidth */
+ 80, /* trackHeight */
+ 15, /* topPadding */
+ 5, /* bottomPadding */
+ 3 /* pixelRatio */);
+
+ return chartTransform;
+ }
+
+ test('checkFields', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.pixelRatio, 3);
+
+ assert.strictEqual(t.leftViewX, 0);
+ assert.strictEqual(t.rightViewX, 500);
+ assert.strictEqual(t.leftTimestamp, 20);
+ assert.strictEqual(t.rightTimestamp, 1020);
+
+ assert.strictEqual(t.outerTopViewY, 0);
+ assert.strictEqual(t.innerTopViewY, 15);
+ assert.strictEqual(t.innerBottomViewY, 75);
+ assert.strictEqual(t.outerBottomViewY, 80);
+ });
+
+ test('checkWorldXToViewX', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.worldXToViewX(-100), -60);
+ assert.strictEqual(t.worldXToViewX(0), -10);
+ assert.strictEqual(t.worldXToViewX(520), 250);
+ assert.strictEqual(t.worldXToViewX(1020), 500);
+ assert.strictEqual(t.worldXToViewX(1200), 590);
+ });
+
+ test('checkViewXToWorldX', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.viewXToWorldX(-60), -100);
+ assert.strictEqual(t.viewXToWorldX(-10), 0);
+ assert.strictEqual(t.viewXToWorldX(250), 520);
+ assert.strictEqual(t.viewXToWorldX(500), 1020);
+ assert.strictEqual(t.viewXToWorldX(590), 1200);
+ });
+
+ test('checkWorldYToViewY', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.worldYToViewY(-200), 105);
+ assert.strictEqual(t.worldYToViewY(-100), 75);
+ assert.strictEqual(t.worldYToViewY(0), 45);
+ assert.strictEqual(t.worldYToViewY(100), 15);
+ assert.strictEqual(t.worldYToViewY(200), -15);
+ });
+
+ test('checkViewYToWorldY', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.viewYToWorldY(105), -200);
+ assert.strictEqual(t.viewYToWorldY(75), -100);
+ assert.strictEqual(t.viewYToWorldY(45), 0);
+ assert.strictEqual(t.viewYToWorldY(15), 100);
+ assert.strictEqual(t.viewYToWorldY(-15), 200);
+ });
+
+ test('checkVectorToWorldDistance', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.vectorToWorldDistance(105), 350);
+ assert.strictEqual(t.vectorToWorldDistance(75), 250);
+ assert.strictEqual(t.vectorToWorldDistance(45), 150);
+ assert.strictEqual(t.vectorToWorldDistance(15), 50);
+ assert.strictEqual(t.vectorToWorldDistance(-15), 50);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html
new file mode 100644
index 00000000000..ecaac0dd3b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html
@@ -0,0 +1,45 @@
+<!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/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * ContainerToTrackMap is a class to handle building and accessing a map
+ * between an EventContainer's stableId and its handling track.
+ *
+ * @constructor
+ */
+ function ContainerToTrackMap() {
+ this.stableIdToTrackMap_ = {};
+ }
+
+ ContainerToTrackMap.prototype = {
+ addContainer(container, track) {
+ if (!track) {
+ throw new Error('Must provide a track.');
+ }
+ this.stableIdToTrackMap_[container.stableId] = track;
+ },
+
+ clear() {
+ this.stableIdToTrackMap_ = {};
+ },
+
+ getTrackByStableId(stableId) {
+ return this.stableIdToTrackMap_[stableId];
+ }
+ };
+
+ return {
+ ContainerToTrackMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html
new file mode 100644
index 00000000000..454c1df585c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/task.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const Task = tr.b.Task;
+
+ /**
+ * A generic track that contains other tracks as its children.
+ * @constructor
+ */
+ const ContainerTrack = tr.ui.b.define('container-track', tr.ui.tracks.Track);
+ ContainerTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ },
+
+ detach() {
+ Polymer.dom(this).textContent = '';
+ },
+
+ get tracks_() {
+ const tracks = [];
+ for (let i = 0; i < this.children.length; i++) {
+ if (this.children[i] instanceof tr.ui.tracks.Track) {
+ tracks.push(this.children[i]);
+ }
+ }
+ return tracks;
+ },
+
+ drawTrack(type) {
+ this.tracks_.forEach(function(track) {
+ track.drawTrack(type);
+ });
+ },
+
+ /**
+ * Adds items intersecting the given range to a selection.
+ * @param {number} loVX Lower X bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVX Upper X bound of the interval to search, in
+ * viewspace.
+ * @param {number} loY Lower Y bound of the interval to search, in
+ * viewspace space.
+ * @param {number} hiY Upper Y bound of the interval to search, in
+ * viewspace space.
+ * @param {Selection} selection Selection to which to add results.
+ */
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ const trackClientRect = this.tracks_[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.tracks_[i].addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addIntersectingEventsInRangeToSelection.
+ apply(this, arguments);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ for (const track of this.tracks_) {
+ track.addEventsToTrackMap(eventToTrackMap);
+ }
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ this.tracks_[i].addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ }
+ },
+
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ const task = new Task();
+ for (let i = 0; i < this.tracks_.length; i++) {
+ task.subTask(function(i) {
+ return function() {
+ this.tracks_[i].addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ };
+ }(i), this);
+ }
+ return task;
+ },
+
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ const trackClientRect = this.tracks_[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.tracks_[i].addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ this.tracks_.forEach(function(track) {
+ track.addContainersToTrackMap(containerToTrackMap);
+ });
+ },
+
+ clearTracks_() {
+ this.tracks_.forEach(function(track) {
+ Polymer.dom(this).removeChild(track);
+ }, this);
+ }
+ };
+
+ return {
+ ContainerTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html
new file mode 100644
index 00000000000..7f25e41bf6a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a Counter object.
+ * @constructor
+ * @extends {ChartTrack}
+ */
+ const CounterTrack = tr.ui.b.define('counter-track', tr.ui.tracks.ChartTrack);
+
+ CounterTrack.prototype = {
+ __proto__: tr.ui.tracks.ChartTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ChartTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('counter-track');
+ },
+
+ get counter() {
+ return this.chart;
+ },
+
+ set counter(counter) {
+ this.heading = counter.name + ': ';
+ this.series = CounterTrack.buildChartSeriesFromCounter(counter);
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ getModelEventFromItem(chartValue) {
+ return chartValue;
+ }
+ };
+
+ CounterTrack.buildChartSeriesFromCounter = function(counter) {
+ const numSeries = counter.series.length;
+ const totals = counter.totals;
+
+ // Create one common axis for all series.
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+
+ // Build one chart series for each counter series.
+ const chartSeries = counter.series.map(function(series, seriesIndex) {
+ const chartPoints = series.samples.map(function(sample, sampleIndex) {
+ const total = totals[sampleIndex * numSeries + seriesIndex];
+ return new tr.ui.tracks.ChartPoint(sample, sample.timestamp, total);
+ });
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: series.color
+ };
+ return new tr.ui.tracks.ChartSeries(
+ chartPoints, seriesYAxis, renderingConfig);
+ });
+
+ // Show the first series (with the smallest cumulative value) at the top.
+ chartSeries.reverse();
+
+ return chartSeries;
+ };
+
+ return {
+ CounterTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html
new file mode 100644
index 00000000000..3a4f84a14b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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/model/model.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function getSynchronous(url) {
+ const req = new XMLHttpRequest();
+ req.open('GET', url, false);
+ // Without the mime type specified like this, the file's bytes are not
+ // retrieved correctly.
+ req.overrideMimeType('text/plain; charset=x-user-defined');
+ req.send(null);
+ return req.responseText;
+ }
+
+ const ZOOM_STEPS = 10;
+ const ZOOM_COEFFICIENT = 1.2;
+
+ let model = undefined;
+
+ let drawingContainer;
+ let viewportDiv;
+
+ let viewportWidth;
+ let worldMid;
+
+ let startScale = undefined;
+
+ function timedCounterTrackPerfTest(name, testFn, iterations) {
+ function setUpOnce() {
+ if (model !== undefined) return;
+
+ const fileUrl = '/test_data/counter_tracks.html';
+ const events = getSynchronous(fileUrl);
+ model = tr.c.TestUtils.newModelWithEvents([events]);
+ }
+
+ function setUp() {
+ setUpOnce();
+ viewportDiv = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(viewportDiv);
+
+ drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ viewport.modelTrackContainer = drawingContainer;
+
+ const modelTrack = new tr.ui.tracks.ModelTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(modelTrack);
+
+ modelTrack.model = model;
+
+ Polymer.dom(viewportDiv).appendChild(drawingContainer);
+
+ this.addHTMLOutput(viewportDiv);
+
+ // Size the canvas.
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ // Size the viewport.
+ viewportWidth = drawingContainer.canvas.width;
+ const min = model.bounds.min;
+ const range = model.bounds.range;
+ worldMid = min + range / 2;
+
+ const boost = range * 0.15;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(min - boost, min + range + boost, viewportWidth);
+ modelTrack.viewport.setDisplayTransformImmediately(dt);
+ startScale = dt.scaleX;
+
+ // Select half of the counter samples.
+ for (const pid in model.processes) {
+ const counters = model.processes[pid].counters;
+ for (const cid in counters) {
+ const series = counters[cid].series;
+ for (let i = 0; i < series.length; i++) {
+ const samples = series[i].samples;
+ for (let j = Math.floor(samples.length / 2); j < samples.length;
+ j++) {
+ samples[j].selectionState =
+ tr.model.SelectionState.SELECTED;
+ }
+ }
+ }
+ }
+ }
+
+ function tearDown() {
+ viewportDiv.innerText = '';
+ drawingContainer = undefined;
+ }
+
+ timedPerfTest(name, testFn, {
+ setUp,
+ tearDown,
+ iterations
+ });
+ }
+
+ const n110100 = [1, 10, 100];
+ n110100.forEach(function(val) {
+ timedCounterTrackPerfTest(
+ 'draw_softwareCanvas_' + val,
+ function() {
+ let scale = startScale;
+ for (let i = 0; i < ZOOM_STEPS; i++) {
+ const dt =
+ drawingContainer.viewport.currentDisplayTransform.clone();
+ scale *= ZOOM_COEFFICIENT;
+ dt.scaleX = scale;
+ dt.xPanWorldPosToViewPos(worldMid, 'center', viewportWidth);
+ drawingContainer.viewport.setDisplayTransformImmediately(dt);
+ drawingContainer.draw_();
+ }
+ }, val);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html
new file mode 100644
index 00000000000..dd0286b6b67
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/event_set.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Counter = tr.model.Counter;
+ const Viewport = tr.ui.TimelineViewport;
+ const CounterTrack = tr.ui.tracks.CounterTrack;
+
+ const runTest = function(timestamps, samples, testFn) {
+ const testEl = document.createElement('div');
+
+ const ctr = new Counter(undefined, 'foo', '', 'foo');
+ const n = samples.length;
+
+ for (let i = 0; i < n; ++i) {
+ ctr.addSeries(new tr.model.CounterSeries('value' + i,
+ ColorScheme.getColorIdForGeneralPurposeString('value' + i)));
+ }
+
+ for (let i = 0; i < samples.length; ++i) {
+ for (let k = 0; k < timestamps.length; ++k) {
+ ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+ }
+ }
+
+ ctr.updateBounds();
+
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new CounterTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ // Force the container to update sizes so the test can use coordinates that
+ // make sense. This has to be after the adding of the track as we need to
+ // use the track header to figure out our positioning.
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ track.heading = ctr.name;
+ track.counter = ctr;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 10, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ testFn(ctr, drawingContainer, track);
+ };
+
+ test('instantiate', function() {
+ const ctr = new Counter(undefined, 'testBasicCounter', '',
+ 'testBasicCounter');
+ ctr.addSeries(new tr.model.CounterSeries('value1',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ 'testBasicCounter.value1')));
+ ctr.addSeries(new tr.model.CounterSeries('value2',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ 'testBasicCounter.value2')));
+
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+ for (let i = 0; i < samples.length; ++i) {
+ for (let k = 0; k < timestamps.length; ++k) {
+ ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+ }
+ }
+
+ ctr.updateBounds();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new CounterTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = ctr.name;
+ track.counter = ctr;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 7.7, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('basicCounterXPointPicking', function() {
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+ runTest.call(this, timestamps, samples, function(ctr, container, track) {
+ const clientRect = track.getBoundingClientRect();
+ const y75 = clientRect.top + (0.75 * clientRect.height);
+
+ // In bounds.
+ let sel = new tr.model.EventSet();
+ let x = 0.15 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+
+ let nextSeriesIndex = 1;
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.series.counter, ctr);
+ assert.strictEqual(event.getSampleIndex(), 1);
+ assert.strictEqual(event.series.seriesIndex, nextSeriesIndex--);
+ }
+
+ // Outside bounds.
+ sel = new tr.model.EventSet();
+ x = -0.5 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+ assert.strictEqual(sel.length, 0);
+
+ sel = new tr.model.EventSet();
+ x = 0.8 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+ assert.strictEqual(sel.length, 0);
+ });
+ });
+
+ test('counterTrackAddClosestEventToSelection', function() {
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 4, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+ runTest.call(this, timestamps, samples, function(ctr, container, track) {
+ // Before with not range.
+ let sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, 0, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Before with negative range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, -10, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Before first sample.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 0);
+ }
+
+ // Between and closer to sample before.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 1);
+ }
+
+ // Between samples with bad range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(1.45, 0.25, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Between and closer to next sample.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(4.7, 6, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 5);
+ }
+
+ // After last sample with good range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(8.5, 2, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 7);
+ }
+
+ // After last sample with bad range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(10, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html
new file mode 100644
index 00000000000..3a6c627fb38
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * Visualizes a Cpu using a series of SliceTracks.
+ * @constructor
+ */
+ const CpuTrack =
+ tr.ui.b.define('cpu-track', tr.ui.tracks.ContainerTrack);
+ CpuTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('cpu-track');
+ this.detailedMode_ = true;
+ },
+
+ get cpu() {
+ return this.cpu_;
+ },
+
+ set cpu(cpu) {
+ this.cpu_ = cpu;
+ this.updateContents_();
+ },
+
+ get detailedMode() {
+ return this.detailedMode_;
+ },
+
+ set detailedMode(detailedMode) {
+ this.detailedMode_ = detailedMode;
+ this.updateContents_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(value) {
+ this.tooltip_ = value;
+ this.updateContents_();
+ },
+
+ get hasVisibleContent() {
+ if (this.cpu_ === undefined) return false;
+
+ const cpu = this.cpu_;
+ if (cpu.slices.length) return true;
+
+ if (cpu.samples && cpu.samples.length) return true;
+
+ if (Object.keys(cpu.counters).length > 0) return true;
+
+ return false;
+ },
+
+ updateContents_() {
+ this.detach();
+ if (!this.cpu_) return;
+
+ const slices = this.cpu_.slices;
+ if (slices.length) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ track.heading = this.cpu_.userFriendlyName + ':';
+ Polymer.dom(this).appendChild(track);
+ }
+
+ if (this.detailedMode_) {
+ this.appendSamplesTracks_();
+
+ for (const counterName in this.cpu_.counters) {
+ const counter = this.cpu_.counters[counterName];
+ const track = new tr.ui.tracks.CounterTrack(this.viewport);
+ track.heading = this.cpu_.userFriendlyName + ' ' +
+ counter.name + ':';
+ track.counter = counter;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+ },
+
+ appendSamplesTracks_() {
+ const samples = this.cpu_.samples;
+ if (samples === undefined || samples.length === 0) {
+ return;
+ }
+ const samplesByTitle = {};
+ samples.forEach(function(sample) {
+ if (samplesByTitle[sample.title] === undefined) {
+ samplesByTitle[sample.title] = [];
+ }
+ samplesByTitle[sample.title].push(sample);
+ });
+
+ const sampleTitles = Object.keys(samplesByTitle);
+ sampleTitles.sort();
+
+ sampleTitles.forEach(function(sampleTitle) {
+ const samples = samplesByTitle[sampleTitle];
+ const samplesTrack = new tr.ui.tracks.SliceTrack(this.viewport);
+ samplesTrack.group = this.cpu_;
+ samplesTrack.slices = samples;
+ samplesTrack.heading = this.cpu_.userFriendlyName + ': ' +
+ sampleTitle;
+ samplesTrack.tooltip = this.cpu_.userFriendlyDetails;
+ samplesTrack.selectionGenerator = function() {
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < samplesTrack.slices.length; i++) {
+ selection.push(samplesTrack.slices[i]);
+ }
+ return selection;
+ };
+ Polymer.dom(this).appendChild(samplesTrack);
+ }, this);
+ }
+ };
+
+ return {
+ CpuTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html
new file mode 100644
index 00000000000..442992522f5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Cpu = tr.model.Cpu;
+ const CpuTrack = tr.ui.tracks.CpuTrack;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const StackFrame = tr.model.StackFrame;
+ const Sample = tr.model.Sample;
+ const Thread = tr.model.Thread;
+ const Viewport = tr.ui.TimelineViewport;
+
+ test('basicCpu', function() {
+ const cpu = new Cpu({}, 7);
+ cpu.slices = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8)
+ ];
+ cpu.updateBounds();
+
+ const testEl = document.createElement('div');
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+
+ const track = new CpuTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.heading = 'CPU ' + cpu.cpuNumber;
+ track.cpu = cpu;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+
+ test('withSamples', function() {
+ let thread;
+ let cpu;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ cpu = model.kernel.getOrCreateCpu(1);
+ thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+ const nodeA = tr.c.TestUtils.newProfileNode(model, 'a');
+ const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA);
+ const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB);
+ const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA);
+
+ model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread,
+ undefined, 10));
+ model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread,
+ undefined, 10));
+
+ model.samples.push(new Sample(25, 'page_fault', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(35, 'page_fault', nodeD, thread,
+ undefined, 10));
+ }
+ });
+
+ const testEl = document.createElement('div');
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+
+ const track = new CpuTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.heading = 'CPU ' + cpu.cpuNumber;
+ track.cpu = cpu;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html
new file mode 100644
index 00000000000..912220b8236
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html
@@ -0,0 +1,91 @@
+<!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/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<style>
+.cpu-usage-track {
+ height: 90px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+
+ /**
+ * A track that displays the cpu usage of a process.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.ChartTrack}
+ */
+ const CpuUsageTrack = tr.ui.b.define('cpu-usage-track', ChartTrack);
+
+ CpuUsageTrack.prototype = {
+ __proto__: ChartTrack.prototype,
+
+ decorate(viewport) {
+ ChartTrack.prototype.decorate.call(this, viewport);
+ this.classList.add('cpu-usage-track');
+ this.heading = 'CPU usage';
+ this.cpuUsageSeries_ = undefined;
+ },
+
+ // Given a tr.Model, it creates a cpu usage series and a graph.
+ initialize(model) {
+ if (model !== undefined) {
+ this.cpuUsageSeries_ = model.device.cpuUsageSeries;
+ } else {
+ this.cpuUsageSeries_ = undefined;
+ }
+ this.series = this.buildChartSeries_();
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ get hasVisibleContent() {
+ return !!this.cpuUsageSeries_ &&
+ this.cpuUsageSeries_.samples.length > 0;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ containerToTrackMap.addContainer(this.series_, this);
+ },
+
+ buildChartSeries_(yAxis, color) {
+ if (!this.hasVisibleContent) return [];
+
+ yAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+ const usageSamples = this.cpuUsageSeries_.samples;
+ const pts = new Array(usageSamples.length + 1);
+ for (let i = 0; i < usageSamples.length; i++) {
+ pts[i] = new tr.ui.tracks.ChartPoint(undefined,
+ usageSamples[i].start, usageSamples[i].usage);
+ }
+ pts[usageSamples.length] = new tr.ui.tracks.ChartPoint(undefined,
+ usageSamples[usageSamples.length - 1].start, 0);
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: color
+ };
+
+ return [new tr.ui.tracks.ChartSeries(pts, yAxis, renderingConfig)];
+ },
+ };
+
+ return {
+ CpuUsageTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html
new file mode 100644
index 00000000000..2970e81eaf8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html
@@ -0,0 +1,215 @@
+<!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/extras/cpu/cpu_usage_auditor.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel='import' href='/tracing/ui/base/constants.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html">
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const DIFF_EPSILON = 0.0001;
+
+ // Input : slices is an array-of-array-of slices. Each top level array
+ // represents a process. So, each slice in one of the top level array
+ // will be placed in the same process.
+ function buildModel(slices) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < slices.length; i++) {
+ const thread = process.getOrCreateThread(i);
+ slices[i].forEach(s => thread.sliceGroup.pushSlice(s));
+ }
+ });
+ const auditor = new tr.e.audits.CpuUsageAuditor(model);
+ auditor.runAnnotate();
+ return model;
+ }
+
+ // Compare float arrays based on an epsilon since floating point arithmetic
+ // is not always 100% accurate.
+ function assertArrayValuesCloseTo(actualValue, expectedValue) {
+ assert.lengthOf(actualValue, expectedValue.length);
+ for (let i = 0; i < expectedValue.length; i++) {
+ assert.closeTo(actualValue[i], expectedValue[i], DIFF_EPSILON);
+ }
+ }
+
+ function createCpuUsageTrack(model, interval) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ div.appendChild(drawingContainer);
+ const track = new tr.ui.tracks.CpuUsageTrack(drawingContainer.viewport);
+ if (model !== undefined) {
+ setDisplayTransformFromBounds(viewport, model.bounds);
+ }
+ track.initialize(model, interval);
+ drawingContainer.appendChild(track);
+ this.addHTMLOutput(drawingContainer);
+ return track;
+ }
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ function setDisplayTransformFromBounds(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ }
+
+ test('computeCpuUsage_simple', function() {
+ // Set the boundaries, from 0-15 ms. This slice will not
+ // contain any CPU usage data, it's just to make the boundaries
+ // of the bins go as 0-1, 1-2, 2-3, etc. This also tests whether
+ // this function works properly in the presence of slices that
+ // don't include CPU usage data.
+ const bigSlice = new tr.model.ThreadSlice('', title, 0, 0, {}, 15);
+ // First thread.
+ // 0 5 10 15
+ // [ sliceA ]
+ // [ sliceB ] [C ]
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 0.5, {}, 5);
+ sliceA.cpuDuration = 5;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 2.5, {}, 8);
+ sliceB.cpuDuration = 6;
+ // The slice completely fits into an interval and is the last.
+ const sliceC = new tr.model.ThreadSlice('', title, 0, 12.5, {}, 2);
+ sliceC.cpuDuration = 1;
+
+ // Second thread.
+ // 0 5 10 15
+ // [ sliceD ][ sliceE ]
+ const sliceD = new tr.model.ThreadSlice('', title, 0, 3.5, {}, 3);
+ sliceD.cpuDuration = 3;
+ const sliceE = new tr.model.ThreadSlice('', title, 0, 6.5, {}, 6);
+ sliceE.cpuDuration = 3;
+
+ const model = buildModel([
+ [bigSlice, sliceA, sliceB, sliceC],
+ [sliceD, sliceE]
+ ]);
+
+ // Compute average CPU usage over A (but not over B and C).
+ const avgCpuUsageA = sliceA.cpuSelfTime / sliceA.selfTime;
+ // Compute average CPU usage over B, C, D, E. They don't have subslices.
+ const avgCpuUsageB = sliceB.cpuDuration / sliceB.duration;
+ const avgCpuUsageC = sliceC.cpuDuration / sliceC.duration;
+ const avgCpuUsageD = sliceD.cpuDuration / sliceD.duration;
+ const avgCpuUsageE = sliceE.cpuDuration / sliceE.duration;
+
+ const expectedValue = [
+ 0,
+ avgCpuUsageA,
+ avgCpuUsageA,
+ avgCpuUsageA + avgCpuUsageB,
+ avgCpuUsageA + avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageA + avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageE,
+ avgCpuUsageE,
+ avgCpuUsageC,
+ avgCpuUsageC,
+ 0
+ ];
+ const track = createCpuUsageTrack.call(this, model);
+ const actualValue = track.series[0].points.map(point => point.y);
+ assertArrayValuesCloseTo(actualValue, expectedValue);
+ });
+
+ test('computeCpuUsage_longDurationThreadSlice', function() {
+ // Create a slice covering 24 hours.
+ const sliceA = new tr.model.ThreadSlice(
+ '', title, 0, 0, {}, 24 * 60 * 60 * 1000);
+ sliceA.cpuDuration = sliceA.duration * 0.25;
+
+ const model = buildModel([[sliceA]]);
+
+ const track = createCpuUsageTrack.call(this, model);
+ const cpuSamples = track.series[0].points.map(point => point.y);
+
+ // All except the last sample is 0.25, since sliceA.cpuDuration was set to
+ // 0.25 of the total.
+ for (const cpuSample of cpuSamples.slice(0, cpuSamples.length - 1)) {
+ assert.closeTo(cpuSample, 0.25, DIFF_EPSILON);
+ }
+ // The last sample is 0.
+ assert.closeTo(cpuSamples[cpuSamples.length - 1], 0, DIFF_EPSILON);
+ });
+
+ test('instantiate', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023);
+ sliceA.cpuDuration = 25;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769);
+ sliceB.cpuDuration = 1.5;
+ const sliceC = new tr.model.ThreadSlice('', title, 0, 11.239, {}, 5.8769);
+ sliceC.cpuDuration = 5;
+ const sliceD = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01);
+ sliceD.cpuDuration = 4;
+
+ const model = buildModel([[sliceA, sliceB, sliceC, sliceD]]);
+ createCpuUsageTrack.call(this, model);
+ });
+
+ test('hasVisibleContent_trueWithThreadSlicePresent', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01);
+ sliceA.cpuDuration = 4;
+ const model = buildModel([[sliceA]]);
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedProcessModel', function() {
+ const track = createCpuUsageTrack.call(this, undefined);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithNoThreadSlice', function() {
+ // model with a CPU and a thread but no ThreadSlice.
+ const model = buildModel([]);
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_trueWithSubSlices', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023);
+ sliceA.cpuDuration = 25;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769);
+ sliceB.cpuDuration = 1.5;
+
+ const model = buildModel([[sliceA, sliceB]]);
+ const process = model.getProcess(1);
+ // B will become lowest level slices of A.
+ process.getThread(0).sliceGroup.createSubSlices();
+ assert.strictEqual(
+ sliceA.cpuSelfTime, (sliceA.cpuDuration - sliceB.cpuDuration));
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html
new file mode 100644
index 00000000000..a068a7ebebb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html
@@ -0,0 +1,90 @@
+<!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/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/power_series_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ContainerTrack = tr.ui.tracks.ContainerTrack;
+
+ // TODO(charliea): Make this track collapsible.
+ /**
+ * Track to visualize the device model.
+ *
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const DeviceTrack = tr.ui.b.define('device-track', ContainerTrack);
+
+ DeviceTrack.prototype = {
+
+ __proto__: ContainerTrack.prototype,
+
+ decorate(viewport) {
+ ContainerTrack.prototype.decorate.call(this, viewport);
+
+ Polymer.dom(this).classList.add('device-track');
+ this.device_ = undefined;
+ this.powerSeriesTrack_ = undefined;
+ },
+
+ get device() {
+ return this.device_;
+ },
+
+ set device(device) {
+ this.device_ = device;
+ this.updateContents_();
+ },
+
+ get powerSeriesTrack() {
+ return this.powerSeriesTrack_;
+ },
+
+ get hasVisibleContent() {
+ return (this.powerSeriesTrack_ &&
+ this.powerSeriesTrack_.hasVisibleContent);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.call(
+ this, containerToTrackMap);
+ containerToTrackMap.addContainer(this.device, this);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ this.tracks_.forEach(function(track) {
+ track.addEventsToTrackMap(eventToTrackMap);
+ });
+ },
+
+ appendPowerSeriesTrack_() {
+ this.powerSeriesTrack_ = new tr.ui.tracks.PowerSeriesTrack(this.viewport);
+ this.powerSeriesTrack_.powerSeries = this.device.powerSeries;
+
+ if (this.powerSeriesTrack_.hasVisibleContent) {
+ Polymer.dom(this).appendChild(this.powerSeriesTrack_);
+ Polymer.dom(this).appendChild(
+ new tr.ui.tracks.SpacingTrack(this.viewport));
+ }
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+ this.appendPowerSeriesTrack_();
+ }
+ };
+
+ return {
+ DeviceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html
new file mode 100644
index 00000000000..fdd3b392993
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html
@@ -0,0 +1,145 @@
+<!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/model/device.html'>
+<link rel='import' href='/tracing/model/model.html'>
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel='import' href='/tracing/ui/timeline_display_transform.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel='import' href='/tracing/ui/tracks/device_track.html'>
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+<link rel='import' href='/tracing/ui/tracks/event_to_track_map.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Device = tr.model.Device;
+ const DeviceTrack = tr.ui.tracks.DeviceTrack;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+
+ const createDrawingContainer = function(series) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ if (series) {
+ series.updateBounds();
+ setDisplayTransformFromBounds(viewport, series.bounds);
+ }
+
+ return drawingContainer;
+ };
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ const setDisplayTransformFromBounds = function(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ };
+
+ test('instantiate', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+ device.powerSeries.addPowerSample(1, 3);
+ device.powerSeries.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+ });
+
+ test('instantiate_noPowerSeries', function() {
+ const device = new Device(new Model());
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ // Adding output should throw due to no visible content.
+ assert.throw(function() { this.addHTMLOutput(drawingContainer); });
+ });
+
+ test('setDevice_clearsTrackBeforeUpdating', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+ device.powerSeries.addPowerSample(1, 3);
+ device.powerSeries.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+
+ // Set the device twice and make sure that this doesn't result in
+ // the track appearing twice.
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+
+ // The device track should still have two subtracks: one counter track and
+ // one spacing track.
+ assert.strictEqual(track.tracks_.length, 2);
+ });
+
+ test('addContainersToTrackMap', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+
+ const drawingContainer = createDrawingContainer(device.series);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+
+ const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+ track.addContainersToTrackMap(containerToTrackMap);
+
+ assert.strictEqual(containerToTrackMap.getTrackByStableId('Device'), track);
+ assert.strictEqual(
+ containerToTrackMap.getTrackByStableId('Device.PowerSeries'),
+ track.powerSeriesTrack);
+ });
+
+ test('addEventsToTrackMap', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new DeviceTrack(viewport);
+ track.device = device;
+
+ const eventToTrackMap = new tr.ui.tracks.EventToTrackMap();
+ track.addEventsToTrackMap(eventToTrackMap);
+
+ const expected = new tr.ui.tracks.EventToTrackMap();
+ expected[device.powerSeries.samples[0].guid] = track.powerSeriesTrack;
+ expected[device.powerSeries.samples[1].guid] = track.powerSeriesTrack;
+
+ assert.deepEqual(eventToTrackMap, expected);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css
new file mode 100644
index 00000000000..a8f4d17c91c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css
@@ -0,0 +1,18 @@
+/* Copyright (c) 2012 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.
+ */
+
+.drawing-container {
+ display: inline;
+ overflow: auto;
+ overflow-x: hidden;
+ position: relative;
+}
+
+.drawing-container-canvas {
+ display: block;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html
new file mode 100644
index 00000000000..d13a3a5383c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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="stylesheet" href="/tracing/ui/tracks/drawing_container.css">
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const DrawType = {
+ GENERAL_EVENT: 1,
+ INSTANT_EVENT: 2,
+ BACKGROUND: 3,
+ GRID: 4,
+ FLOW_ARROWS: 5,
+ MARKERS: 6,
+ HIGHLIGHTS: 7,
+ ANNOTATIONS: 8
+ };
+
+ // Must be > 1.0. This is the maximum multiple by which the size
+ // of the canvas can exceed the window dimensions. For example
+ // if window.innerHeight is 1000 and this is 1.4, then the
+ // largest the canvas height can be set to is 1400px assuming a
+ // window.devicePixelRatio of 1.
+ // Currently this value is set rather large to mostly match
+ // previous behavior & performance. This should be reduced to
+ // be as small as possible once raw drawing performance is improved
+ // such that a repaint doesn't incur a large jank
+ const MAX_OVERSIZE_MULTIPLE = 3.0;
+ const REDRAW_SLOP = (MAX_OVERSIZE_MULTIPLE - 1) / 2;
+
+ const DrawingContainer = tr.ui.b.define('drawing-container',
+ tr.ui.tracks.Track);
+
+ DrawingContainer.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('drawing-container');
+
+ this.canvas_ = document.createElement('canvas');
+ this.canvas_.className = 'drawing-container-canvas';
+ this.canvas_.style.left = tr.ui.b.constants.HEADING_WIDTH + 'px';
+ Polymer.dom(this).appendChild(this.canvas_);
+
+ this.ctx_ = this.canvas_.getContext('2d');
+ this.offsetY_ = 0;
+
+ this.viewportChange_ = this.viewportChange_.bind(this);
+ this.viewport.addEventListener('change', this.viewportChange_);
+
+ window.addEventListener('resize', this.windowResized_.bind(this));
+ this.addEventListener('scroll', this.scrollChanged_.bind(this));
+ },
+
+ // Needed to support the calls in TimelineTrackView.
+ get canvas() {
+ return this.canvas_;
+ },
+
+ context() {
+ return this.ctx_;
+ },
+
+ viewportChange_() {
+ this.invalidate();
+ },
+
+ windowResized_() {
+ this.invalidate();
+ },
+
+ scrollChanged_() {
+ if (this.updateOffsetY_()) {
+ this.invalidate();
+ }
+ },
+
+ invalidate() {
+ if (this.rafPending_) return;
+
+ this.rafPending_ = true;
+
+ tr.b.requestPreAnimationFrame(this.preDraw_, this);
+ },
+
+ preDraw_() {
+ this.rafPending_ = false;
+ this.updateCanvasSizeIfNeeded_();
+
+ tr.b.requestAnimationFrameInThisFrameIfPossible(this.draw_, this);
+ },
+
+ draw_() {
+ this.ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+
+ const typesToDraw = [
+ DrawType.BACKGROUND,
+ DrawType.HIGHLIGHTS,
+ DrawType.GRID,
+ DrawType.INSTANT_EVENT,
+ DrawType.GENERAL_EVENT,
+ DrawType.MARKERS,
+ DrawType.ANNOTATIONS,
+ DrawType.FLOW_ARROWS
+ ];
+
+ for (const idx in typesToDraw) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ this.children[i].drawTrack(typesToDraw[idx]);
+ }
+ }
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.canvas_.getBoundingClientRect();
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(
+ bounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ this.viewport.drawGridLines(
+ this.ctx_, viewLWorld, viewRWorld, viewHeight);
+ },
+
+ // Update's this.offsetY_, returning true if the value has changed
+ // and thus a redraw is needed, or false if it did not change.
+ updateOffsetY_() {
+ const maxYDelta = window.innerHeight * REDRAW_SLOP;
+ let newOffset = this.scrollTop - maxYDelta;
+ if (Math.abs(newOffset - this.offsetY_) <= maxYDelta) return false;
+ // Now clamp to the valid range.
+ const maxOffset = this.scrollHeight -
+ this.canvas_.getBoundingClientRect().height;
+ newOffset = Math.max(0, Math.min(newOffset, maxOffset));
+ if (newOffset !== this.offsetY_) {
+ this.offsetY_ = newOffset;
+ return true;
+ }
+ return false;
+ },
+
+ updateCanvasSizeIfNeeded_() {
+ const visibleChildTracks =
+ Array.from(this.children).filter(this.visibleFilter_);
+
+ if (visibleChildTracks.length === 0) {
+ return;
+ }
+
+ const thisBounds = this.getBoundingClientRect();
+
+ const firstChildTrackBounds =
+ visibleChildTracks[0].getBoundingClientRect();
+ const lastChildTrackBounds =
+ visibleChildTracks[visibleChildTracks.length - 1].
+ getBoundingClientRect();
+
+ const innerWidth = firstChildTrackBounds.width -
+ tr.ui.b.constants.HEADING_WIDTH;
+ const innerHeight = Math.min(
+ lastChildTrackBounds.bottom - firstChildTrackBounds.top,
+ Math.floor(window.innerHeight * MAX_OVERSIZE_MULTIPLE));
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ if (this.canvas_.width !== innerWidth * pixelRatio) {
+ this.canvas_.width = innerWidth * pixelRatio;
+ this.canvas_.style.width = innerWidth + 'px';
+ }
+
+ if (this.canvas_.height !== innerHeight * pixelRatio) {
+ this.canvas_.height = innerHeight * pixelRatio;
+ this.canvas_.style.height = innerHeight + 'px';
+ }
+
+ if (this.canvas_.top !== this.offsetY_) {
+ this.canvas_.top = this.offsetY_;
+ this.canvas_.style.top = this.offsetY_ + 'px';
+ }
+ },
+
+ visibleFilter_(element) {
+ if (!(element instanceof tr.ui.tracks.Track)) return false;
+
+ return window.getComputedStyle(element).display !== 'none';
+ },
+
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ const trackClientRect = this.children[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.children[i].addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ this.children[i].addEventsToTrackMap(eventToTrackMap);
+ }
+ }
+ };
+
+ return {
+ DrawingContainer,
+ DrawType,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html
new file mode 100644
index 00000000000..7b778b1c332
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ let generalModel;
+ function getOrCreateGeneralModel() {
+ if (generalModel !== undefined) {
+ generalModel;
+ }
+ const fileUrl = '/test_data/thread_time_visualisation.json.gz';
+ const events = tr.b.getSync(fileUrl);
+ generalModel = tr.c.TestUtils.newModelWithEvents([events]);
+ return generalModel;
+ }
+
+ function DCPerfTestCase(testName, opt_options) {
+ tr.b.unittest.PerfTestCase.call(this, testName, undefined, opt_options);
+ this.viewportDiv = undefined;
+ this.drawingContainer = undefined;
+ this.viewport = undefined;
+ }
+ DCPerfTestCase.prototype = {
+ __proto__: tr.b.unittest.PerfTestCase.prototype,
+
+ setUp(model) {
+ this.viewportDiv = document.createElement('div');
+
+ this.viewport = new tr.ui.TimelineViewport(this.viewportDiv);
+
+ this.drawingContainer = new tr.ui.tracks.DrawingContainer(this.viewport);
+ this.viewport.modelTrackContainer = this.drawingContainer;
+
+ const modelTrack = new tr.ui.tracks.ModelTrack(this.viewport);
+ Polymer.dom(this.drawingContainer).appendChild(modelTrack);
+
+ modelTrack.model = model;
+
+ Polymer.dom(this.viewportDiv).appendChild(this.drawingContainer);
+
+ this.addHTMLOutput(this.viewportDiv);
+
+ // Size the canvas.
+ this.drawingContainer.updateCanvasSizeIfNeeded_();
+
+ // Size the viewport.
+ const w = this.drawingContainer.canvas.width;
+ const min = model.bounds.min;
+ const range = model.bounds.range;
+
+ const boost = range * 0.15;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(min - boost, min + range + boost, w);
+ this.viewport.setDisplayTransformImmediately(dt);
+ },
+
+ runOneIteration() {
+ this.drawingContainer.draw_();
+ }
+ };
+
+
+ function GeneralDCPerfTestCase(testName, opt_options) {
+ DCPerfTestCase.call(this, testName, opt_options);
+ }
+
+ GeneralDCPerfTestCase.prototype = {
+ __proto__: DCPerfTestCase.prototype,
+
+ setUp() {
+ const model = getOrCreateGeneralModel();
+ DCPerfTestCase.prototype.setUp.call(this, model);
+ }
+ };
+
+ // Failing on Chrome canary, see
+ // https://github.com/catapult-project/catapult/issues/1826
+ flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_One',
+ {iterations: 1}));
+ // Failing on Chrome stable on Windows, see
+ // https://github.com/catapult-project/catapult/issues/1908
+ flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_Ten',
+ {iterations: 10}));
+ test(new GeneralDCPerfTestCase('draw_softwareCanvas_AHundred',
+ {iterations: 100}));
+
+ function AsyncDCPerfTestCase(testName, opt_options) {
+ DCPerfTestCase.call(this, testName, opt_options);
+ }
+
+ AsyncDCPerfTestCase.prototype = {
+ __proto__: DCPerfTestCase.prototype,
+
+ setUp() {
+ const model = tr.c.TestUtils.newModel(function(m) {
+ const proc = m.getOrCreateProcess(1);
+ for (let tid = 1; tid <= 5; tid++) {
+ const thread = proc.getOrCreateThread(tid);
+ for (let i = 0; i < 5000; i++) {
+ const mod = Math.floor(i / 100) % 4;
+ const slice = tr.c.TestUtils.newAsyncSliceEx({
+ name: 'Test' + i,
+ colorId: tid + mod,
+ id: tr.b.GUID.allocateSimple(),
+ start: i * 10,
+ duration: 9,
+ isTopLevel: true
+ });
+ thread.asyncSliceGroup.push(slice);
+ }
+ }
+ });
+ DCPerfTestCase.prototype.setUp.call(this, model);
+
+ const w = this.drawingContainer.canvas.width;
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(-2000, 54000, w);
+ this.viewport.setDisplayTransformImmediately(dt);
+ }
+ };
+ test(new AsyncDCPerfTestCase('draw_asyncSliceHeavy_Twenty',
+ {iterations: 20}));
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html
new file mode 100644
index 00000000000..f8ba209d01b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html
@@ -0,0 +1,34 @@
+<!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/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * EventToTrackMap provides a mapping mechanism between events and the
+ * tracks those events belong on.
+ * @constructor
+ */
+ function EventToTrackMap() {}
+
+ EventToTrackMap.prototype = {
+ addEvent(event, track) {
+ if (!track) {
+ throw new Error('Must provide a track.');
+ }
+ this[event.guid] = track;
+ }
+ };
+
+ return {
+ EventToTrackMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html
new file mode 100644
index 00000000000..3e8de1d9831
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html
@@ -0,0 +1,71 @@
+<!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/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const startCompare = function(x, y) { return x.start - y.start; };
+
+ /**
+ * Track enabling quick selection of frame slices/events.
+ * @constructor
+ */
+ const FrameTrack = tr.ui.b.define(
+ 'frame-track', tr.ui.tracks.LetterDotTrack);
+
+ FrameTrack.prototype = {
+ __proto__: tr.ui.tracks.LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Frames';
+
+ this.frames_ = undefined;
+ this.items = undefined;
+ },
+
+ get frames() {
+ return this.frames_;
+ },
+
+ set frames(frames) {
+ this.frames_ = frames;
+ if (frames === undefined) return;
+
+ this.frames_ = this.frames_.slice();
+ this.frames_.sort(startCompare);
+
+ // letter dots
+ this.items = this.frames_.map(function(frame) {
+ return new FrameDot(frame);
+ });
+ }
+ };
+
+ /**
+ * @constructor
+ * @extends {LetterDot}
+ */
+ function FrameDot(frame) {
+ tr.ui.tracks.LetterDot.call(this, frame, 'F', frame.colorId, frame.start);
+ }
+
+ FrameDot.prototype = {
+ __proto__: tr.ui.tracks.LetterDot.prototype
+ };
+
+ return {
+ FrameTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html
new file mode 100644
index 00000000000..94189d0fecb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html
@@ -0,0 +1,107 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/frame.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/frame_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Frame = tr.model.Frame;
+ const FrameTrack = tr.ui.tracks.FrameTrack;
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createFrames = function() {
+ let frames = undefined;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(1);
+ for (let i = 1; i < 5; i++) {
+ const slice = tr.c.TestUtils.newSliceEx(
+ {title: 'work for frame', start: i * 20, duration: 10});
+ thread.sliceGroup.pushSlice(slice);
+ const events = [slice];
+ const threadTimeRanges =
+ [{thread, start: slice.start, end: slice.end}];
+ process.frames.push(new Frame(events, threadTimeRanges));
+ }
+ frames = process.frames;
+ });
+ return frames;
+ };
+
+ test('instantiate', function() {
+ const frames = createFrames();
+ frames[1].selectionState = SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = FrameTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.frames = frames;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.strictEqual(track.items[0].start, 20);
+ });
+
+ test('modelMapping', function() {
+ const frames = createFrames();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = FrameTrack(viewport);
+ track.frames = frames;
+
+ const a0 = track.items[0].modelItem;
+ assert.strictEqual(a0, frames[0]);
+ });
+
+ test('selectionMapping', function() {
+ const frames = createFrames();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = FrameTrack(viewport);
+ track.frames = frames;
+
+ const selection = new EventSet();
+ track.items[0].addToSelection(selection);
+
+ // select both frame, but not its component slice
+ assert.strictEqual(selection.length, 1);
+
+ let frameCount = 0;
+ let eventCount = 0;
+ selection.forEach(function(event) {
+ if (event instanceof Frame) {
+ assert.strictEqual(event, frames[0]);
+ frameCount++;
+ } else {
+ eventCount++;
+ }
+ });
+ assert.strictEqual(frameCount, 1);
+ assert.strictEqual(eventCount, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html
new file mode 100644
index 00000000000..aaf9bc0a80d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html
@@ -0,0 +1,105 @@
+<!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/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const USED_MEMORY_TRACK_HEIGHT = 50;
+ const ALLOCATED_MEMORY_TRACK_HEIGHT = 50;
+
+ /**
+ * A track that displays an array of GlobalMemoryDump objects.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const GlobalMemoryDumpTrack = tr.ui.b.define(
+ 'global-memory-dump-track', tr.ui.tracks.ContainerTrack);
+
+ GlobalMemoryDumpTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.memoryDumps_ = undefined;
+ },
+
+ get memoryDumps() {
+ return this.memoryDumps_;
+ },
+
+ set memoryDumps(memoryDumps) {
+ this.memoryDumps_ = memoryDumps;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ // Show no tracks if there are no dumps.
+ if (!this.memoryDumps_ || !this.memoryDumps_.length) return;
+
+ this.appendDumpDotsTrack_();
+ this.appendUsedMemoryTrack_();
+ this.appendAllocatedMemoryTrack_();
+ },
+
+ appendDumpDotsTrack_() {
+ const items = tr.ui.tracks.buildMemoryLetterDots(this.memoryDumps_);
+ if (!items) return;
+
+ const track = new tr.ui.tracks.LetterDotTrack(this.viewport);
+ track.heading = 'Memory Dumps';
+ track.items = items;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendUsedMemoryTrack_() {
+ const tracks = [];
+ const perProcessSeries =
+ tr.ui.tracks.buildGlobalUsedMemoryChartSeries(this.memoryDumps_);
+ if (perProcessSeries !== undefined) {
+ tracks.push({name: 'Memory per process', series: perProcessSeries});
+ } else {
+ tracks.push.apply(tracks, tr.ui.tracks.buildSystemMemoryChartSeries(
+ this.memoryDumps_[0].model));
+ }
+
+ for (const {name, series} of tracks) {
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = name;
+ track.height = USED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ },
+
+ appendAllocatedMemoryTrack_() {
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ this.memoryDumps_);
+ if (!series) return;
+
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = 'Memory per component';
+ track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ };
+
+ return {
+ GlobalMemoryDumpTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html
new file mode 100644
index 00000000000..20fa869bbf0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html
@@ -0,0 +1,62 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Viewport = tr.ui.TimelineViewport;
+ const GlobalMemoryDumpTrack = tr.ui.tracks.GlobalMemoryDumpTrack;
+ const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps;
+
+ function instantiateTrack(withVMRegions, withAllocatorDumps,
+ expectedTrackCount) {
+ const dumps = createTestGlobalMemoryDumps(
+ withVMRegions, withAllocatorDumps);
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new GlobalMemoryDumpTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.invalidate();
+
+ track.memoryDumps = dumps;
+ this.addHTMLOutput(div);
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.lengthOf(track.tracks_, expectedTrackCount);
+ }
+
+ test('instantiate_dotsOnly', function() {
+ instantiateTrack.call(this, false, false, 1);
+ });
+
+ test('instantiate_withVMRegions', function() {
+ instantiateTrack.call(this, true, false, 2);
+ });
+
+ test('instantiate_withMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, true, 2);
+ });
+
+ test('instantiate_withBoth', function() {
+ instantiateTrack.call(this, true, true, 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html
new file mode 100644
index 00000000000..7ae139672d2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html
@@ -0,0 +1,67 @@
+<!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/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/kernel_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of interaction records.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const InteractionTrack = tr.ui.b.define(
+ 'interaction-track', tr.ui.tracks.MultiRowTrack);
+
+ InteractionTrack.prototype = {
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Interactions';
+ this.subRows_ = [];
+ },
+
+ set model(model) {
+ this.setItemsToGroup(model.userModel.expectations, {
+ guid: tr.b.GUID.allocateSimple(),
+ model,
+ getSettingsKey() {
+ return undefined;
+ }
+ });
+ },
+
+ buildSubRows_(slices) {
+ if (this.subRows_.length) {
+ return this.subRows_;
+ }
+ this.subRows_.push(
+ ...tr.ui.tracks.groupAsyncSlicesIntoSubRows(slices, true));
+ return this.subRows_;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ }
+ };
+
+ return {
+ InteractionTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html
new file mode 100644
index 00000000000..ac0d5692c4d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html
@@ -0,0 +1,51 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/user_model/stub_expectation.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/interaction_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ // UserExpectations should be sorted by start time, not title, so that
+ // AsyncSliceGroupTrack.buildSubRows_ can lay them out in as few tracks as
+ // possible, so that they mesh instead of stacking unnecessarily.
+ test('instantiate', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+ const track = new tr.ui.tracks.InteractionTrack(viewport);
+ track.model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(1);
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 0, duration: 200}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 100, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 0, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 150, duration: 50}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 50, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 0, duration: 50}));
+ // Model.createImportTracesTask() automatically sorts UEs by start time.
+ });
+ assert.strictEqual(2, track.subRows_.length);
+ assert.strictEqual(2, track.subRows_[0].length);
+ assert.strictEqual(3, track.subRows_[1].length);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html
new file mode 100644
index 00000000000..b10547bc2e9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/tracks/cpu_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track_base.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const Cpu = tr.model.Cpu;
+ const CpuTrack = tr.ui.tracks.cpu_track;
+ const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase;
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ /**
+ * @constructor
+ */
+ const KernelTrack = tr.ui.b.define('kernel-track', ProcessTrackBase);
+
+ KernelTrack.prototype = {
+ __proto__: ProcessTrackBase.prototype,
+
+ decorate(viewport) {
+ ProcessTrackBase.prototype.decorate.call(this, viewport);
+ },
+
+
+ // Kernel maps to processBase because we derive from ProcessTrackBase.
+ set kernel(kernel) {
+ this.processBase = kernel;
+ },
+
+ get kernel() {
+ return this.processBase;
+ },
+
+ get eventContainer() {
+ return this.kernel;
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 1;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.call(
+ this, containerToTrackMap);
+ containerToTrackMap.addContainer(this.kernel, this);
+ },
+
+ willAppendTracks_() {
+ const cpus = Object.values(this.kernel.cpus);
+ cpus.sort(tr.model.Cpu.compare);
+
+ let didAppendAtLeastOneTrack = false;
+ for (let i = 0; i < cpus.length; ++i) {
+ const cpu = cpus[i];
+ const track = new tr.ui.tracks.CpuTrack(this.viewport);
+ track.detailedMode = this.expanded;
+ track.cpu = cpu;
+ if (!track.hasVisibleContent) continue;
+ Polymer.dom(this).appendChild(track);
+ didAppendAtLeastOneTrack = true;
+ }
+ if (didAppendAtLeastOneTrack) {
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ }
+ };
+
+
+ return {
+ KernelTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html
new file mode 100644
index 00000000000..6a642e52ff9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html
@@ -0,0 +1,251 @@
+<!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/color_scheme.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.letter-dot-track {
+ height: 18px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * A track that displays an array of dots with filled letters inside them.
+ * @constructor
+ * @extends {Track}
+ */
+ const LetterDotTrack = tr.ui.b.define(
+ 'letter-dot-track', tr.ui.tracks.Track);
+
+ LetterDotTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('letter-dot-track');
+ this.items_ = undefined;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get items() {
+ return this.items_;
+ },
+
+ set items(items) {
+ this.items_ = items;
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ get dumpRadiusView() {
+ return 7 * (window.devicePixelRatio || 1);
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ if (this.items_ === undefined) return;
+
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawLetterDots_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawLetterDots_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const height = bounds.height * pixelRatio;
+ const halfHeight = height * 0.5;
+ const twoPi = Math.PI * 2;
+
+ // Culling parameters.
+ const dt = this.viewport.currentDisplayTransform;
+ const dumpRadiusView = this.dumpRadiusView;
+ const itemRadiusWorld = dt.xViewVectorToWorld(height);
+
+ // Draw the memory dumps.
+ const items = this.items_;
+ const loI = tr.b.findLowIndexInSortedArray(
+ items,
+ function(item) { return item.start; },
+ viewLWorld);
+
+ const oldFont = ctx.font;
+ ctx.font = '400 ' + Math.floor(9 * pixelRatio) + 'px Arial';
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+
+ const drawItems = function(selected) {
+ for (let i = loI; i < items.length; ++i) {
+ const item = items[i];
+ const x = item.start;
+ if (x - itemRadiusWorld > viewRWorld) break;
+
+ if (item.selected !== selected) continue;
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.fillStyle = EventPresenter.getSelectableItemColorAsString(item);
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, dumpRadiusView + 0.5, 0, twoPi);
+ ctx.fill();
+ if (item.selected) {
+ ctx.lineWidth = 3;
+ ctx.strokeStyle = 'rgb(100,100,0)';
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, dumpRadiusView, 0, twoPi);
+ ctx.lineWidth = 1.5;
+ ctx.strokeStyle = 'rgb(255,255,0)';
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.stroke();
+ }
+
+ ctx.fillStyle = 'rgb(255, 255, 255)';
+ ctx.fillText(item.dotLetter, xView, halfHeight);
+ }
+ };
+
+ // Draw unselected items first to make sure they don't occlude selected
+ // items.
+ drawItems(false);
+ drawItems(true);
+
+ ctx.lineWidth = 1;
+ ctx.font = oldFont;
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.items_ === undefined) return;
+
+ this.items_.forEach(function(item) {
+ item.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ if (this.items_ === undefined) return;
+
+ const itemRadiusWorld = viewPixWidthWorld * this.dumpRadiusView;
+ tr.b.iterateOverIntersectingIntervals(
+ this.items_,
+ function(x) { return x.start - itemRadiusWorld; },
+ function(x) { return 2 * itemRadiusWorld; },
+ loWX, hiWX,
+ function(item) {
+ item.addToSelection(selection);
+ }.bind(this));
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {event} The current event item.
+ * @param {Number} offset Number of slices away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (this.items_ === undefined) return;
+
+ const index = this.items_.findIndex(item => item.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < this.items_.length) {
+ this.items_[newIndex].addToSelection(selection);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ if (this.items_ === undefined) return;
+
+ const item = tr.b.findClosestElementInSortedArray(
+ this.items_,
+ function(x) { return x.start; },
+ worldX,
+ worldMaxDist);
+
+ if (!item) return;
+
+ item.addToSelection(selection);
+ }
+ };
+
+ /**
+ * A filled dot with a letter inside it.
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function LetterDot(modelItem, dotLetter, colorId, start) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.dotLetter = dotLetter;
+ this.colorId = colorId;
+ this.start = start;
+ }
+
+ LetterDot.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype
+ };
+
+ return {
+ LetterDotTrack,
+ LetterDot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html
new file mode 100644
index 00000000000..b37034afab2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html
@@ -0,0 +1,121 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const LetterDotTrack = tr.ui.tracks.LetterDotTrack;
+ const LetterDot = tr.ui.tracks.LetterDot;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createItems = function() {
+ const items = [
+ new LetterDot({selectionState: SelectionState.SELECTED}, 'a', 7, 5),
+ new LetterDot({selectionState: SelectionState.SELECTED}, 'b', 2, 20),
+ new LetterDot({selectionState: SelectionState.NONE}, 'c', 4, 35),
+ new LetterDot({selectionState: SelectionState.NONE}, 'd', 4, 50)
+ ];
+ return items;
+ };
+
+ test('instantiate', function() {
+ const items = createItems();
+
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = LetterDotTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.items = items;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 60, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('selectionHitTesting', function() {
+ const items = createItems();
+
+ const track = new LetterDotTrack(new Viewport());
+ track.items = items;
+
+ // Fake a view pixel size.
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ const viewPixWidthWorld = 0.1 / devicePixelRatio;
+
+ // Hit outside range
+ let selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 3, 4, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 0);
+
+ // Hit the first item, via pixel-nearness.
+ selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 19.98, 19.99, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.strictEqual(selection[0], items[1].modelItem);
+
+ // Hit the instance, between the 1st and 2nd snapshots
+ selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 30, 50, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 2);
+ assert.strictEqual(selection[0], items[2].modelItem);
+ assert.strictEqual(selection[1], items[3].modelItem);
+ });
+
+ test('addEventNearToProvidedEventToSelection', function() {
+ const items = createItems();
+
+ const track = new LetterDotTrack(new Viewport());
+ track.items = items;
+
+ // Right from the middle of items.
+ const selection1 = [];
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ items[2].modelItem, 1, selection1));
+ assert.strictEqual(selection1.length, 1);
+ assert.strictEqual(selection1[0], items[3].modelItem);
+
+ // Left from the middle of items.
+ const selection2 = [];
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ items[2].modelItem, -1, selection2));
+ assert.strictEqual(selection2.length, 1);
+ assert.strictEqual(selection2[0], items[1].modelItem);
+
+ // Right from the right edge of items.
+ const selection3 = [];
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ items[3].modelItem, 1, selection3));
+ assert.strictEqual(selection3.length, 0);
+
+ // Left from the left edge of items.
+ const selection4 = [];
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ items[0].modelItem, -1, selection4));
+ assert.strictEqual(selection4.length, 0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html
new file mode 100644
index 00000000000..611bd8664f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html
@@ -0,0 +1,155 @@
+<!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/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for memory dump track tests.
+ */
+tr.exportTo('tr.ui.tracks', function() {
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const SelectionState = tr.model.SelectionState;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ function createVMRegions(pssValues) {
+ return VMRegionClassificationNode.fromRegions(
+ pssValues.map(function(pssValue, i) {
+ return VMRegion.fromDict({
+ startAddress: 1000 * i,
+ sizeInBytes: 1000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack' + i + ']',
+ byteStats: {
+ privateDirtyResident: pssValue / 3,
+ swapped: pssValue * 3,
+ proportionalResident: pssValue
+ }
+ });
+ }));
+ }
+
+ function createAllocatorDumps(memoryDump, dumpData) {
+ // Create the individual allocator dumps.
+ const allocatorDumps = {};
+ for (const [allocatorName, data] of Object.entries(dumpData)) {
+ const size = data.size;
+ assert.typeOf(size, 'number'); // Sanity check.
+ allocatorDumps[allocatorName] = newAllocatorDump(
+ memoryDump, allocatorName, {numerics: {size}});
+ }
+
+ // Add ownership links between them.
+ for (const [allocatorName, data] of Object.entries(dumpData)) {
+ const owns = data.owns;
+ if (owns === undefined) continue;
+
+ const ownerDump = allocatorDumps[allocatorName];
+ assert.isDefined(ownerDump); // Sanity check.
+ const ownedDump = allocatorDumps[owns];
+ assert.isDefined(ownedDump); // Sanity check.
+
+ addOwnershipLink(ownerDump, ownedDump);
+ }
+
+ return Object.values(allocatorDumps);
+ }
+
+ function addProcessMemoryDumpWithFields(globalMemoryDump, process, start,
+ opt_pssValues, opt_dumpData) {
+ const pmd = addProcessMemoryDump(globalMemoryDump, process, {ts: start});
+ if (opt_pssValues !== undefined) {
+ pmd.vmRegions = createVMRegions(opt_pssValues);
+ }
+ if (opt_dumpData !== undefined) {
+ pmd.memoryAllocatorDumps = createAllocatorDumps(pmd, opt_dumpData);
+ }
+ }
+
+ function createModelWithDumps(withVMRegions, withAllocatorDumps) {
+ const maybePssValues = function(pssValues) {
+ return withVMRegions ? pssValues : undefined;
+ };
+ const maybeDumpData = function(dumpData) {
+ return withAllocatorDumps ? dumpData : undefined;
+ };
+ return tr.c.TestUtils.newModel(function(model) {
+ // Construct a model with three processes.
+ const pa = model.getOrCreateProcess(3);
+ const pb = model.getOrCreateProcess(6);
+ const pc = model.getOrCreateProcess(9);
+
+ const gmd1 = addGlobalMemoryDump(model, {ts: 0, levelOfDetail: LIGHT});
+ addProcessMemoryDumpWithFields(gmd1, pa, 0, maybePssValues([111]));
+ addProcessMemoryDumpWithFields(gmd1, pb, 0.2, undefined,
+ maybeDumpData({oilpan: {size: 1024}}));
+
+ const gmd2 = addGlobalMemoryDump(model, {ts: 5, levelOfDetail: DETAILED});
+ addProcessMemoryDumpWithFields(gmd2, pa, 0);
+ addProcessMemoryDumpWithFields(gmd2, pb, 4.99, maybePssValues([100, 50]),
+ maybeDumpData({v8: {size: 512}}));
+ addProcessMemoryDumpWithFields(gmd2, pc, 5.12, undefined,
+ maybeDumpData({oilpan: {size: 128, owns: 'v8'},
+ v8: {size: 384, owns: 'tracing'}, tracing: {size: 65920}}));
+
+ const gmd3 = addGlobalMemoryDump(
+ model, {ts: 15, levelOfDetail: DETAILED});
+ addProcessMemoryDumpWithFields(gmd3, pa, 15.5, maybePssValues([]),
+ maybeDumpData({v8: {size: 768}}));
+ addProcessMemoryDumpWithFields(gmd3, pc, 14.5,
+ maybePssValues([70, 70, 70]), maybeDumpData({oilpan: {size: 512}}));
+
+ const gmd4 = addGlobalMemoryDump(model, {ts: 18, levelOfDetail: LIGHT});
+
+ const gmd5 = addGlobalMemoryDump(model,
+ {ts: 20, levelOfDetail: BACKGROUND});
+ addProcessMemoryDumpWithFields(gmd5, pa, 0, maybePssValues([105]));
+ addProcessMemoryDumpWithFields(gmd5, pb, 0.2, undefined,
+ maybeDumpData({oilpan: {size: 100}}));
+ });
+ }
+
+ function createTestGlobalMemoryDumps(withVMRegions, withAllocatorDumps) {
+ const model = createModelWithDumps(withVMRegions, withAllocatorDumps);
+ const dumps = model.globalMemoryDumps;
+ dumps[1].selectionState = SelectionState.HIGHLIGHTED;
+ dumps[2].selectionState = SelectionState.SELECTED;
+ return dumps;
+ }
+
+ function createTestProcessMemoryDumps(withVMRegions, withAllocatorDumps) {
+ const model = createModelWithDumps(withVMRegions, withAllocatorDumps);
+ const dumps = model.getProcess(9).memoryDumps;
+ dumps[0].selectionState = SelectionState.SELECTED;
+ dumps[1].selectionState = SelectionState.HIGHLIGHTED;
+ return dumps;
+ }
+
+ return {
+ createTestGlobalMemoryDumps,
+ createTestProcessMemoryDumps,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html
new file mode 100644
index 00000000000..a483d545880
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html
@@ -0,0 +1,253 @@
+<!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/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ const DISPLAYED_SIZE_NUMERIC_NAME =
+ tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ const SYSTEM_MEMORY_CHART_RENDERING_CONFIG = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: ColorScheme.getColorIdForGeneralPurposeString('systemMemory'),
+ backgroundOpacity: 0.8
+ };
+ const SYSTEM_MEMORY_SERIES_NAMES = ['Used (KB)', 'Swapped (KB)'];
+
+ /** Extract PSS values of processes in a global memory dump. */
+ function extractGlobalMemoryDumpUsedSizes(globalMemoryDump, addSize) {
+ for (const [pid, pmd] of
+ Object.entries(globalMemoryDump.processMemoryDumps)) {
+ const mostRecentVmRegions = pmd.mostRecentVmRegions;
+ if (mostRecentVmRegions === undefined) continue;
+ addSize(pid, mostRecentVmRegions.byteStats.proportionalResident || 0,
+ pmd.process.userFriendlyName);
+ }
+ }
+
+ /** Extract sizes of root allocators in a process memory dump. */
+ function extractProcessMemoryDumpAllocatorSizes(processMemoryDump, addSize) {
+ const allocatorDumps = processMemoryDump.memoryAllocatorDumps;
+ if (allocatorDumps === undefined) return;
+
+ allocatorDumps.forEach(function(allocatorDump) {
+ // Don't show tracing overhead in the charts.
+ // TODO(petrcermak): Find a less hacky way to do this.
+ if (allocatorDump.fullName === 'tracing') return;
+
+ const allocatorSize = allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME];
+ if (allocatorSize === undefined) return;
+
+ const allocatorSizeValue = allocatorSize.value;
+ if (allocatorSizeValue === undefined) return;
+
+ addSize(allocatorDump.fullName, allocatorSizeValue);
+ });
+ }
+
+ /** Extract sizes of root allocators in a global memory dump. */
+ function extractGlobalMemoryDumpAllocatorSizes(globalMemoryDump, addSize) {
+ for (const pmd of Object.values(globalMemoryDump.processMemoryDumps)) {
+ extractProcessMemoryDumpAllocatorSizes(pmd, addSize);
+ }
+ }
+
+ /**
+ * A generic function which converts a list of memory dumps to a list of
+ * chart series.
+ *
+ * @param {!Array<!tr.model.ContainerMemoryDump>} memoryDumps List of
+ * container memory dumps.
+ * @param {!function(
+ * !tr.model.ContainerMemoryDump,
+ * !function(string, number, string=))} dumpSizeExtractor Callback for
+ * extracting sizes from a container memory dump.
+ * @return {(!Array<!tr.ui.tracks.ChartSeries>|undefined)} List of chart
+ * series (or undefined if no size is extracted from any container memory
+ * dump).
+ */
+ function buildMemoryChartSeries(memoryDumps, dumpSizeExtractor) {
+ const dumpCount = memoryDumps.length;
+ const idToTimestampToPoint = {};
+ const idToName = {};
+
+ // Extract the sizes of all components from each memory dump.
+ memoryDumps.forEach(function(dump, index) {
+ dumpSizeExtractor(dump, function addSize(id, size, opt_name) {
+ let timestampToPoint = idToTimestampToPoint[id];
+ if (timestampToPoint === undefined) {
+ idToTimestampToPoint[id] = timestampToPoint = new Array(dumpCount);
+ for (let i = 0; i < dumpCount; i++) {
+ const modelItem = memoryDumps[i];
+ timestampToPoint[i] = new tr.ui.tracks.ChartPoint(
+ modelItem, modelItem.start, 0);
+ }
+ }
+ timestampToPoint[index].y += size;
+ if (opt_name !== undefined) idToName[id] = opt_name;
+ });
+ });
+
+ // Do not generate any chart series if no sizes were extracted.
+ const ids = Object.keys(idToTimestampToPoint);
+ if (ids.length === 0) return undefined;
+
+ ids.sort();
+ for (let i = 0; i < dumpCount; i++) {
+ let baseSize = 0;
+ // Traverse |ids| in reverse (alphabetical) order so that the first id is
+ // at the top of the chart.
+ for (let j = ids.length - 1; j >= 0; j--) {
+ const point = idToTimestampToPoint[ids[j]][i];
+ point.yBase = baseSize;
+ point.y += baseSize;
+ baseSize = point.y;
+ }
+ }
+
+ // Create one common axis for all memory chart series.
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0);
+
+ // Build a chart series for each id.
+ const series = ids.map(function(id) {
+ const colorId = ColorScheme.getColorIdForGeneralPurposeString(
+ idToName[id] || id);
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId,
+ backgroundOpacity: 0.8
+ };
+ return new tr.ui.tracks.ChartSeries(idToTimestampToPoint[id],
+ seriesYAxis, renderingConfig);
+ });
+
+ // Ensure that the series at the top of the chart are drawn last.
+ series.reverse();
+
+ return series;
+ }
+
+ /**
+ * Transform a list of memory dumps to a list of letter dots (with letter 'M'
+ * inside).
+ */
+ function buildMemoryLetterDots(memoryDumps) {
+ const backgroundMemoryColorId =
+ ColorScheme.getColorIdForReservedName('background_memory_dump');
+ const lightMemoryColorId =
+ ColorScheme.getColorIdForReservedName('light_memory_dump');
+ const detailedMemoryColorId =
+ ColorScheme.getColorIdForReservedName('detailed_memory_dump');
+ return memoryDumps.map(function(memoryDump) {
+ let memoryColorId;
+ switch (memoryDump.levelOfDetail) {
+ case BACKGROUND:
+ memoryColorId = backgroundMemoryColorId;
+ break;
+ case DETAILED:
+ memoryColorId = detailedMemoryColorId;
+ break;
+ case LIGHT:
+ default:
+ memoryColorId = lightMemoryColorId;
+ }
+ return new tr.ui.tracks.LetterDot(
+ memoryDump, 'M', memoryColorId, memoryDump.start);
+ });
+ }
+
+ /**
+ * Convert a list of global memory dumps to a list of chart series (one per
+ * process). Each series represents the evolution of the memory used by the
+ * process over time.
+ */
+ function buildGlobalUsedMemoryChartSeries(globalMemoryDumps) {
+ return buildMemoryChartSeries(globalMemoryDumps,
+ extractGlobalMemoryDumpUsedSizes);
+ }
+
+ /**
+ * Convert a list of process memory dumps to a list of chart series (one per
+ * root allocator). Each series represents the evolution of the size of a the
+ * corresponding root allocator (e.g. 'v8') over time.
+ */
+ function buildProcessAllocatedMemoryChartSeries(processMemoryDumps) {
+ return buildMemoryChartSeries(processMemoryDumps,
+ extractProcessMemoryDumpAllocatorSizes);
+ }
+
+ /**
+ * Convert a list of global memory dumps to a list of chart series (one per
+ * root allocator). Each series represents the evolution of the size of a the
+ * corresponding root allocator (e.g. 'v8') over time.
+ */
+ function buildGlobalAllocatedMemoryChartSeries(globalMemoryDumps) {
+ return buildMemoryChartSeries(globalMemoryDumps,
+ extractGlobalMemoryDumpAllocatorSizes);
+ }
+
+ /**
+ * Converts system memory counters in the model to a list of
+ * {'name': trackName, 'series': ChartSeries}.
+ */
+ function buildSystemMemoryChartSeries(model) {
+ if (model.kernel.counters === undefined) return;
+ const memoryCounter = model.kernel.counters['global.SystemMemory'];
+ if (memoryCounter === undefined) return;
+
+ const tracks = [];
+ for (const name of SYSTEM_MEMORY_SERIES_NAMES) {
+ const series = memoryCounter.series.find(series => series.name === name);
+ if (series === undefined || series.samples.length === 0) return;
+
+ const chartPoints = [];
+ const valueRange = new tr.b.math.Range();
+ for (const sample of series.samples) {
+ chartPoints.push(new tr.ui.tracks.ChartPoint(
+ sample, sample.timestamp, sample.value, 0));
+ valueRange.addValue(sample.value);
+ }
+ // Stretch min to max range over the top half of a chart for readability.
+ const baseLine = Math.max(0, valueRange.min - valueRange.range);
+ const axisY = new tr.ui.tracks.ChartSeriesYAxis(baseLine, valueRange.max);
+ const chartSeries =
+ [new tr.ui.tracks.ChartSeries(chartPoints, axisY,
+ SYSTEM_MEMORY_CHART_RENDERING_CONFIG)];
+ tracks.push({
+ name: 'System Memory ' + name,
+ series: chartSeries
+ });
+ }
+ return tracks;
+ }
+
+ return {
+ buildMemoryLetterDots,
+ buildGlobalUsedMemoryChartSeries,
+ buildProcessAllocatedMemoryChartSeries,
+ buildGlobalAllocatedMemoryChartSeries,
+ buildSystemMemoryChartSeries,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html
new file mode 100644
index 00000000000..f4f9451b4b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html
@@ -0,0 +1,270 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SelectionState = tr.model.SelectionState;
+ const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps;
+ const createTestProcessMemoryDumps =
+ tr.ui.tracks.createTestProcessMemoryDumps;
+
+ test('buildMemoryLetterDots_withoutVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const items = tr.ui.tracks.buildMemoryLetterDots(dumps);
+
+ assert.lengthOf(items, 5);
+ assert.strictEqual(items[0].start, 0);
+ assert.strictEqual(items[1].start, 5);
+ assert.strictEqual(items[2].start, 15);
+ assert.strictEqual(items[3].start, 18);
+ assert.strictEqual(items[4].start, 20);
+
+ // Check model mapping.
+ assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(items[2].selected);
+ assert.strictEqual(items[3].modelItem, dumps[3]);
+ });
+
+ test('buildMemoryLetterDots_withVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const items = tr.ui.tracks.buildMemoryLetterDots(dumps);
+
+ assert.lengthOf(items, 5);
+ assert.strictEqual(items[0].start, 0);
+ assert.strictEqual(items[1].start, 5);
+ assert.strictEqual(items[2].start, 15);
+ assert.strictEqual(items[3].start, 18);
+ assert.strictEqual(items[4].start, 20);
+
+ // Check model mapping.
+ assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(items[2].selected);
+ assert.strictEqual(items[3].modelItem, dumps[3]);
+ });
+
+ test('buildGlobalUsedMemoryChartSeries_withoutVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildGlobalUsedMemoryChartSeries_withVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(true, false);
+ const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps);
+
+ assert.lengthOf(series, 3);
+
+ const sa = series[2];
+ const sb = series[1];
+ const sc = series[0];
+
+ assert.lengthOf(sa.points, 5);
+ assert.lengthOf(sb.points, 5);
+ assert.lengthOf(sc.points, 5);
+
+ // Process A: VM regions defined -> sum their PSS values (111).
+ // Process B: VM regions undefined and no previous value -> assume zero.
+ // Process C: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[0].x, 0);
+ assert.strictEqual(sb.points[0].x, 0);
+ assert.strictEqual(sc.points[0].x, 0);
+ assert.strictEqual(sa.points[0].y, 111);
+ assert.strictEqual(sb.points[0].y, 0);
+ assert.strictEqual(sc.points[0].y, 0);
+ assert.strictEqual(sa.points[0].yBase, 0);
+ assert.strictEqual(sb.points[0].yBase, 0);
+ assert.strictEqual(sc.points[0].yBase, 0);
+
+ // Process A: VM regions undefined -> assume previous value (111).
+ // Process B: VM regions defined -> sum their PSS values (100 + 50).
+ // Process C: VM regions undefined -> assume previous value (0).
+ assert.strictEqual(sa.points[1].x, 5);
+ assert.strictEqual(sb.points[1].x, 5);
+ assert.strictEqual(sc.points[1].x, 5);
+ assert.strictEqual(sa.points[1].y, 150 + 111);
+ assert.strictEqual(sb.points[1].y, 150);
+ assert.strictEqual(sc.points[1].y, 0);
+ assert.strictEqual(sa.points[1].yBase, 150);
+ assert.strictEqual(sb.points[1].yBase, 0);
+ assert.strictEqual(sc.points[1].yBase, 0);
+
+ // Process A: VM regions defined -> sum their PSS values (0).
+ // Process B: Memory dump not present -> assume process not alive (0).
+ // Process C: VM regions defined -> sum their PSS values (70 + 70 + 70).
+ assert.strictEqual(sa.points[2].x, 15);
+ assert.strictEqual(sb.points[2].x, 15);
+ assert.strictEqual(sc.points[2].x, 15);
+ assert.strictEqual(sa.points[2].y, 210);
+ assert.strictEqual(sb.points[2].y, 210);
+ assert.strictEqual(sc.points[2].y, 210);
+ assert.strictEqual(sa.points[2].yBase, 210);
+ assert.strictEqual(sb.points[2].yBase, 210);
+ assert.strictEqual(sc.points[2].yBase, 0);
+
+ // All processes: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[3].x, 18);
+ assert.strictEqual(sb.points[3].x, 18);
+ assert.strictEqual(sc.points[3].x, 18);
+ assert.strictEqual(sa.points[3].y, 0);
+ assert.strictEqual(sb.points[3].y, 0);
+ assert.strictEqual(sc.points[3].y, 0);
+ assert.strictEqual(sa.points[3].yBase, 0);
+ assert.strictEqual(sb.points[3].yBase, 0);
+ assert.strictEqual(sc.points[3].yBase, 0);
+
+ // Process A: VM regions defined -> sum their PSS values (105).
+ // Process B: VM regions undefined and no previous value -> assume zero.
+ // Process C: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[4].x, 20);
+ assert.strictEqual(sb.points[4].x, 20);
+ assert.strictEqual(sc.points[4].x, 20);
+ assert.strictEqual(sa.points[4].y, 105);
+ assert.strictEqual(sb.points[4].y, 0);
+ assert.strictEqual(sc.points[4].y, 0);
+ assert.strictEqual(sa.points[4].yBase, 0);
+ assert.strictEqual(sb.points[4].yBase, 0);
+ assert.strictEqual(sc.points[4].yBase, 0);
+
+ // Check model mapping.
+ assert.strictEqual(sa.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sb.points[2].selected);
+ assert.strictEqual(sc.points[3].modelItem, dumps[3]);
+ });
+
+ test('buildGlobalAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildGlobalAllocatedMemoryChartSeries_withMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestGlobalMemoryDumps(false, true);
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.lengthOf(series, 2);
+
+ const so = series[1];
+ const sv = series[0];
+
+ assert.lengthOf(so.points, 5);
+ assert.lengthOf(sv.points, 5);
+
+ // Oilpan: Only process B dumps allocated objects size (1024).
+ // V8: No process dumps allocated objects size (0).
+ assert.strictEqual(so.points[0].x, 0);
+ assert.strictEqual(sv.points[0].x, 0);
+ assert.strictEqual(so.points[0].y, 1024);
+ assert.strictEqual(sv.points[0].y, 0);
+ assert.strictEqual(so.points[0].yBase, 0);
+ assert.strictEqual(sv.points[0].yBase, 0);
+
+ // Oilpan: Process B did not provide a value and process C dumps (128).
+ // V8: Processes B and C dump (512 + 256).
+ assert.strictEqual(so.points[1].x, 5);
+ assert.strictEqual(sv.points[1].x, 5);
+ assert.strictEqual(so.points[1].y, 768 + 128);
+ assert.strictEqual(sv.points[1].y, 768);
+ assert.strictEqual(so.points[1].yBase, 768);
+ assert.strictEqual(sv.points[1].yBase, 0);
+
+ // Oilpan: Process B assumed not alive and process C dumps (512)
+ // V8: Process A dumps now, process B assumed not alive, process C did
+ // not provide a value (768).
+ assert.strictEqual(so.points[2].x, 15);
+ assert.strictEqual(sv.points[2].x, 15);
+ assert.strictEqual(so.points[2].y, 768 + 512);
+ assert.strictEqual(sv.points[2].y, 768);
+ assert.strictEqual(so.points[2].yBase, 768);
+ assert.strictEqual(sv.points[2].yBase, 0);
+
+ // All processes: Memory dump not present -> assume process not alive
+ // (0).
+ assert.strictEqual(so.points[3].x, 18);
+ assert.strictEqual(sv.points[3].x, 18);
+ assert.strictEqual(so.points[3].y, 0);
+ assert.strictEqual(sv.points[3].y, 0);
+ assert.strictEqual(so.points[3].yBase, 0);
+ assert.strictEqual(sv.points[3].yBase, 0);
+
+ // Oilpan: Only process B dumps allocated objects size (100).
+ // V8: No process dumps allocated objects size (0).
+ assert.strictEqual(so.points[4].x, 20);
+ assert.strictEqual(sv.points[4].x, 20);
+ assert.strictEqual(so.points[4].y, 100);
+ assert.strictEqual(sv.points[4].y, 0);
+
+ // Check model mapping.
+ assert.strictEqual(
+ so.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sv.points[2].selected);
+ assert.strictEqual(so.points[3].modelItem, dumps[3]);
+ });
+
+ test('buildProcessAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestProcessMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildProcessAllocatedMemoryChartSeries_withMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestProcessMemoryDumps(false, true);
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ dumps);
+
+ // There should be only 2 series (because 'tracing' is not shown in the
+ // charts).
+ assert.lengthOf(series, 2);
+
+ const so = series[1];
+ const sv = series[0];
+
+ assert.lengthOf(so.points, 2);
+ assert.lengthOf(sv.points, 2);
+
+ // Oilpan: Process dumps (128).
+ // V8: Process dumps (256).
+ assert.strictEqual(so.points[0].x, 5.12);
+ assert.strictEqual(sv.points[0].x, 5.12);
+ assert.strictEqual(so.points[0].y, 256 + 128);
+ assert.strictEqual(sv.points[0].y, 256);
+ assert.strictEqual(so.points[0].yBase, 256);
+ assert.strictEqual(sv.points[0].yBase, 0);
+
+ // Oilpan: Process dumps (512).
+ // V8: Process did not provide a value (0).
+ assert.strictEqual(so.points[1].x, 14.5);
+ assert.strictEqual(sv.points[1].x, 14.5);
+ assert.strictEqual(so.points[1].y, 512);
+ assert.strictEqual(sv.points[1].y, 0);
+ assert.strictEqual(so.points[1].yBase, 0);
+ assert.strictEqual(sv.points[1].yBase, 0);
+
+ // Check model mapping.
+ assert.strictEqual(
+ so.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sv.points[0].selected);
+ assert.strictEqual(so.points[1].modelItem, dumps[1]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html
new file mode 100644
index 00000000000..bf59f11349e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html
@@ -0,0 +1,67 @@
+<!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/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const LetterDotTrack = tr.ui.tracks.LetterDotTrack;
+
+ /**
+ * A track that displays global memory events.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.LetterDotTrack}
+ */
+ const MemoryTrack = tr.ui.b.define('memory-track', LetterDotTrack);
+
+ MemoryTrack.prototype = {
+ __proto__: LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.classList.add('memory-track');
+ this.heading = 'Memory Events';
+ this.lowMemoryEvents_ = undefined;
+ },
+
+ initialize(model) {
+ if (model !== undefined) {
+ this.lowMemoryEvents_ = model.device.lowMemoryEvents;
+ } else {
+ this.lowMemoryEvents_ = undefined;
+ }
+
+ if (this.hasVisibleContent) {
+ this.items = this.buildMemoryLetterDots_(this.lowMemoryEvents_);
+ }
+ },
+
+ get hasVisibleContent() {
+ return !!this.lowMemoryEvents_ && this.lowMemoryEvents_.length !== 0;
+ },
+
+ buildMemoryLetterDots_(memoryEvents) {
+ return memoryEvents.map(
+ memoryEvent => new tr.ui.tracks.LetterDot(
+ memoryEvent,
+ 'K',
+ ColorScheme.getColorIdForReservedName('background_memory_dump'),
+ memoryEvent.start
+ )
+ );
+ },
+ };
+
+ return {
+ MemoryTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html
new file mode 100644
index 00000000000..60f1c8ccd2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.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/core/test_utils.html">
+<link rel="import" href="/tracing/extras/memory/lowmemory_auditor.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/memory_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ // Input : slices is an array-of-array-of slices. Each top level array
+ // represents a process. So, each slice in one of the top level array
+ // will be placed in the same process.
+ function buildModel(slices) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < slices.length; i++) {
+ const thread = process.getOrCreateThread(i);
+ slices[i].forEach(s => thread.sliceGroup.pushSlice(s));
+ }
+ });
+
+ const auditor = new tr.e.audits.LowMemoryAuditor(model);
+ auditor.runAnnotate();
+ return model;
+ }
+
+ function createMemoryTrack(model, interval) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ div.appendChild(drawingContainer);
+ const track = new tr.ui.tracks.MemoryTrack(drawingContainer.viewport);
+ if (model !== undefined) {
+ setDisplayTransformFromBounds(viewport, model.bounds);
+ }
+ track.initialize(model, interval);
+ drawingContainer.appendChild(track);
+ this.addHTMLOutput(drawingContainer);
+ return track;
+ }
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ function setDisplayTransformFromBounds(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ }
+
+ test('instantiate', function() {
+ const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {});
+ const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {});
+
+ const model = buildModel([[sliceA, sliceB]]);
+ createMemoryTrack.call(this, model);
+ });
+
+ test('hasVisibleContent_trueWithThreadSlicePresent', function() {
+ const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {});
+ const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {});
+ const model = buildModel([[sliceA, sliceB]]);
+ const track = createMemoryTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedProcessModel', function() {
+ const track = createMemoryTrack.call(this, undefined);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithNoThreadSlice', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 7.6211, {});
+ const model = buildModel([[sliceA]]);
+ const track = createMemoryTrack.call(this, model);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html
new file mode 100644
index 00000000000..45404899b7c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html
@@ -0,0 +1,534 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html">
+<link rel="import" href="/tracing/ui/tracks/device_track.html">
+<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/interaction_track.html">
+<link rel="import" href="/tracing/ui/tracks/kernel_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track.html">
+
+<style>
+.model-track {
+ flex-grow: 1;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const SelectionState = tr.model.SelectionState;
+ const ColorScheme = tr.b.ColorScheme;
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ /**
+ * Visualizes a Model by building ProcessTracks and CpuTracks.
+ * @constructor
+ */
+ const ModelTrack = tr.ui.b.define('model-track', tr.ui.tracks.ContainerTrack);
+
+ ModelTrack.VSYNC_HIGHLIGHT_ALPHA = 0.1;
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT = 0.20;
+ ModelTrack.VSYNC_DENSITY_OPAQUE = 0.10;
+ ModelTrack.VSYNC_DENSITY_RANGE =
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT - ModelTrack.VSYNC_DENSITY_OPAQUE;
+
+ /**
+ * Generate a zebra striping from a list of times.
+ *
+ * @param {!Array.<number>} times A sorted array of timestamps.
+ * @param {number} minTime the lower bound of time to start striping at.
+ * @param {number} maxTime the upper bound of time to stop striping at.
+ * of |times|.
+ *
+ * @returns {!Array.<tr.b.math.Range>} An array of ranges where each element
+ * represents the time range that a stripe covers. Each range is a subset
+ * of the interval [minTime, maxTime].
+ */
+ ModelTrack.generateStripes_ = function(times, minTime, maxTime) {
+ if (times.length === 0) return [];
+
+ // Find the lowest and highest index within the viewport.
+ const lowIndex = tr.b.findLowIndexInSortedArray(
+ times, (x => x), minTime);
+ let highIndex = lowIndex - 1;
+ while (times[highIndex + 1] <= maxTime) {
+ highIndex++;
+ }
+
+ const stripes = [];
+ // Must start at an even index and end at an odd index.
+ for (let i = lowIndex - (lowIndex % 2); i <= highIndex; i += 2) {
+ const left = i < lowIndex ? minTime : times[i];
+ const right = i + 1 > highIndex ? maxTime : times[i + 1];
+ stripes.push(tr.b.math.Range.fromExplicitRange(left, right));
+ }
+
+ return stripes;
+ };
+
+
+ ModelTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('model-track');
+
+ this.upperMode_ = false;
+ this.annotationViews_ = [];
+ this.vSyncTimes_ = [];
+ },
+
+ get processViews() {
+ return Polymer.dom(this).querySelectorAll('.process-track-base');
+ },
+
+ // upperMode is true if the track is being used on the ruler.
+ get upperMode() {
+ return this.upperMode_;
+ },
+
+ set upperMode(upperMode) {
+ this.upperMode_ = upperMode;
+ this.updateContents_();
+ },
+
+ detach() {
+ tr.ui.tracks.ContainerTrack.prototype.detach.call(this);
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+
+ this.model_.addEventListener('annotationChange',
+ this.updateAnnotations_.bind(this));
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 0;
+ },
+
+ updateContents_() {
+ Polymer.dom(this).textContent = '';
+ if (!this.model_) return;
+
+ if (this.upperMode_) {
+ this.updateContentsForUpperMode_();
+ } else {
+ this.updateContentsForLowerMode_();
+ }
+ },
+
+ updateContentsForUpperMode_() {
+ },
+
+ updateContentsForLowerMode_() {
+ if (this.model_.userModel.expectations.length > 1) {
+ const mrt = new tr.ui.tracks.InteractionTrack(this.viewport_);
+ mrt.model = this.model_;
+ Polymer.dom(this).appendChild(mrt);
+ }
+
+ if (this.model_.alerts.length) {
+ const at = new tr.ui.tracks.AlertTrack(this.viewport_);
+ at.alerts = this.model_.alerts;
+ Polymer.dom(this).appendChild(at);
+ }
+
+ if (this.model_.globalMemoryDumps.length) {
+ const gmdt = new tr.ui.tracks.GlobalMemoryDumpTrack(this.viewport_);
+ gmdt.memoryDumps = this.model_.globalMemoryDumps;
+ Polymer.dom(this).appendChild(gmdt);
+ }
+
+ this.appendDeviceTrack_();
+ this.appendCpuUsageTrack_();
+ this.appendMemoryTrack_();
+ this.appendKernelTrack_();
+
+ // Get a sorted list of processes.
+ const processes = this.model_.getAllProcesses();
+ processes.sort(tr.model.Process.compare);
+
+ for (let i = 0; i < processes.length; ++i) {
+ const process = processes[i];
+
+ const track = new tr.ui.tracks.ProcessTrack(this.viewport);
+ track.process = process;
+ if (!track.hasVisibleContent) continue;
+
+ Polymer.dom(this).appendChild(track);
+ }
+ this.viewport_.rebuildEventToTrackMap();
+ this.viewport_.rebuildContainerToTrackMap();
+ this.vSyncTimes_ = this.model_.device.vSyncTimestamps;
+
+ this.updateAnnotations_();
+ },
+
+ getContentBounds() { return this.model.bounds; },
+
+ addAnnotation(annotation) {
+ this.model.addAnnotation(annotation);
+ },
+
+ removeAnnotation(annotation) {
+ this.model.removeAnnotation(annotation);
+ },
+
+ updateAnnotations_() {
+ this.annotationViews_ = [];
+ const annotations = this.model_.getAllAnnotations();
+ for (let i = 0; i < annotations.length; i++) {
+ this.annotationViews_.push(
+ annotations[i].getOrCreateView(this.viewport_));
+ }
+ this.invalidateDrawingContainer();
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (!this.model_) return;
+
+ const tracks = this.children;
+ for (let i = 0; i < tracks.length; ++i) {
+ tracks[i].addEventsToTrackMap(eventToTrackMap);
+ }
+
+ if (this.instantEvents === undefined) return;
+
+ const vp = this.viewport_;
+ this.instantEvents.forEach(function(ev) {
+ eventToTrackMap.addEvent(ev, this);
+ }.bind(this));
+ },
+
+ appendDeviceTrack_() {
+ const device = this.model.device;
+ const track = new tr.ui.tracks.DeviceTrack(this.viewport);
+ track.device = this.model.device;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendKernelTrack_() {
+ const kernel = this.model.kernel;
+ const track = new tr.ui.tracks.KernelTrack(this.viewport);
+ track.kernel = this.model.kernel;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendCpuUsageTrack_() {
+ const track = new tr.ui.tracks.CpuUsageTrack(this.viewport);
+ track.initialize(this.model);
+ if (!track.hasVisibleContent) return;
+
+ this.appendChild(track);
+ },
+
+ appendMemoryTrack_() {
+ const track = new tr.ui.tracks.MemoryTrack(this.viewport);
+ track.initialize(this.model);
+ if (!track.hasVisibleContent) return;
+
+ Polymer.dom(this).appendChild(track);
+ },
+
+ drawTrack(type) {
+ const ctx = this.context();
+ if (!this.model_) return;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ switch (type) {
+ case tr.ui.tracks.DrawType.GRID:
+ this.viewport.drawMajorMarkLines(ctx, viewHeight);
+ // The model is the only thing that draws grid lines.
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.FLOW_ARROWS:
+ if (this.model_.flowIntervalTree.size === 0) {
+ ctx.restore();
+ return;
+ }
+
+ this.drawFlowArrows_(viewLWorld, viewRWorld);
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.INSTANT_EVENT:
+ if (!this.model_.instantEvents ||
+ this.model_.instantEvents.length === 0) {
+ break;
+ }
+
+ tr.ui.b.drawInstantSlicesAsLines(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.model_.instantEvents,
+ 4);
+
+ break;
+
+ case tr.ui.tracks.DrawType.MARKERS:
+ if (!this.viewport.interestRange.isEmpty) {
+ this.viewport.interestRange.draw(
+ ctx, viewLWorld, viewRWorld, viewHeight);
+ this.viewport.interestRange.drawIndicators(
+ ctx, viewLWorld, viewRWorld);
+ }
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.HIGHLIGHTS:
+ this.drawVSyncHighlight(
+ ctx, dt, viewLWorld, viewRWorld, viewHeight);
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.ANNOTATIONS:
+ for (let i = 0; i < this.annotationViews_.length; i++) {
+ this.annotationViews_[i].draw(ctx);
+ }
+ ctx.restore();
+ return;
+ }
+ ctx.restore();
+
+ tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+ },
+
+ drawFlowArrows_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
+ ctx.lineWidth = 1;
+
+ const events =
+ this.model_.flowIntervalTree.findIntersection(viewLWorld, viewRWorld);
+
+ // When not showing flow events, show only highlighted/selected ones.
+ const onlyHighlighted = !this.viewport.showFlowEvents;
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ for (let i = 0; i < events.length; ++i) {
+ if (onlyHighlighted &&
+ events[i].selectionState !== SelectionState.SELECTED &&
+ events[i].selectionState !== SelectionState.HIGHLIGHTED) {
+ continue;
+ }
+ this.drawFlowArrow_(ctx, events[i], canvasBounds);
+ }
+ },
+
+ drawFlowArrow_(ctx, flowEvent, canvasBounds) {
+ const dt = this.viewport.currentDisplayTransform;
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const startTrack = this.viewport.trackForEvent(flowEvent.startSlice);
+ const endTrack = this.viewport.trackForEvent(flowEvent.endSlice);
+
+ // TODO(nduca): Figure out how to draw flow arrows even when
+ // processes are collapsed, bug #931.
+ if (startTrack === undefined || endTrack === undefined) return;
+
+ const startBounds = startTrack.getBoundingClientRect();
+ const endBounds = endTrack.getBoundingClientRect();
+
+ if (flowEvent.selectionState === SelectionState.SELECTED) {
+ ctx.shadowBlur = 1;
+ ctx.shadowColor = 'red';
+ ctx.shadowOffsety = 2;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[
+ tr.b.ColorScheme.getVariantColorId(
+ flowEvent.colorId,
+ tr.b.ColorScheme.properties.brightenedOffsets[0])];
+ } else if (flowEvent.selectionState === SelectionState.HIGHLIGHTED) {
+ ctx.shadowBlur = 1;
+ ctx.shadowColor = 'red';
+ ctx.shadowOffsety = 2;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[
+ tr.b.ColorScheme.getVariantColorId(
+ flowEvent.colorId,
+ tr.b.ColorScheme.properties.brightenedOffsets[0])];
+ } else if (flowEvent.selectionState === SelectionState.DIMMED) {
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId];
+ } else {
+ let hasBoost = false;
+ const startSlice = flowEvent.startSlice;
+ hasBoost |= startSlice.selectionState === SelectionState.SELECTED;
+ hasBoost |= startSlice.selectionState === SelectionState.HIGHLIGHTED;
+ const endSlice = flowEvent.endSlice;
+ hasBoost |= endSlice.selectionState === SelectionState.SELECTED;
+ hasBoost |= endSlice.selectionState === SelectionState.HIGHLIGHTED;
+ if (hasBoost) {
+ ctx.shadowBlur = 1;
+ ctx.shadowColor = 'rgba(255, 0, 0, 0.4)';
+ ctx.shadowOffsety = 2;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[
+ tr.b.ColorScheme.getVariantColorId(
+ flowEvent.colorId,
+ tr.b.ColorScheme.properties.brightenedOffsets[0])];
+ } else {
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId];
+ }
+ }
+
+ const startSize = startBounds.left + startBounds.top +
+ startBounds.bottom + startBounds.right;
+ const endSize = endBounds.left + endBounds.top +
+ endBounds.bottom + endBounds.right;
+ // Nothing to do if both ends of the track are collapsed.
+ if (startSize === 0 && endSize === 0) return;
+
+ const startY = this.calculateTrackY_(startTrack, canvasBounds);
+ const endY = this.calculateTrackY_(endTrack, canvasBounds);
+
+ const pixelStartY = pixelRatio * startY;
+ const pixelEndY = pixelRatio * endY;
+
+ const startXView = dt.xWorldToView(flowEvent.start);
+ const endXView = dt.xWorldToView(flowEvent.end);
+ const midXView = (startXView + endXView) / 2;
+
+ ctx.beginPath();
+ ctx.moveTo(startXView, pixelStartY);
+ ctx.bezierCurveTo(
+ midXView, pixelStartY,
+ midXView, pixelEndY,
+ endXView, pixelEndY);
+ ctx.stroke();
+
+ const arrowWidth = 5 * pixelRatio;
+ const distance = endXView - startXView;
+ if (distance <= (2 * arrowWidth)) return;
+
+ const tipX = endXView;
+ const tipY = pixelEndY;
+ const arrowHeight = (endBounds.height / 4) * pixelRatio;
+ tr.ui.b.drawTriangle(ctx,
+ tipX, tipY,
+ tipX - arrowWidth, tipY - arrowHeight,
+ tipX - arrowWidth, tipY + arrowHeight);
+ ctx.fill();
+ },
+
+ /**
+ * Highlights VSync events on the model track (using "zebra" striping).
+ */
+ drawVSyncHighlight(ctx, dt, viewLWorld, viewRWorld, viewHeight) {
+ if (!this.viewport_.highlightVSync) {
+ return;
+ }
+
+ const stripes = ModelTrack.generateStripes_(
+ this.vSyncTimes_, viewLWorld, viewRWorld);
+ if (stripes.length === 0) {
+ return;
+ }
+
+ const vSyncHighlightColor =
+ new tr.b.Color(ColorScheme.getColorForReservedNameAsString(
+ 'vsync_highlight_color'));
+
+ const stripeRange = stripes[stripes.length - 1].max - stripes[0].min;
+ const stripeDensity =
+ stripeRange ? stripes.length / (dt.scaleX * stripeRange) : 0;
+ const clampedStripeDensity =
+ tr.b.math.clamp(stripeDensity, ModelTrack.VSYNC_DENSITY_OPAQUE,
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT);
+ const opacity =
+ (ModelTrack.VSYNC_DENSITY_TRANSPARENT - clampedStripeDensity) /
+ ModelTrack.VSYNC_DENSITY_RANGE;
+ if (opacity === 0) {
+ return;
+ }
+
+ ctx.fillStyle = vSyncHighlightColor.toStringWithAlphaOverride(
+ ModelTrack.VSYNC_HIGHLIGHT_ALPHA * opacity);
+
+ for (let i = 0; i < stripes.length; i++) {
+ const xLeftView = dt.xWorldToView(stripes[i].min);
+ const xRightView = dt.xWorldToView(stripes[i].max);
+ ctx.fillRect(xLeftView, 0, xRightView - xLeftView, viewHeight);
+ }
+ },
+
+ calculateTrackY_(track, canvasBounds) {
+ const bounds = track.getBoundingClientRect();
+ const size = bounds.left + bounds.top + bounds.bottom + bounds.right;
+ if (size === 0) {
+ return this.calculateTrackY_(
+ Polymer.dom(track).parentNode, canvasBounds);
+ }
+
+ return bounds.top - canvasBounds.top + (bounds.height / 2);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onPickHit(instantEvent) {
+ selection.push(instantEvent);
+ }
+ const instantEventWidth = 3 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.model_.instantEvents,
+ function(x) { return x.start; },
+ function(x) { return x.duration + instantEventWidth; },
+ loWX, hiWX,
+ onPickHit.bind(this));
+
+ tr.ui.tracks.ContainerTrack.prototype.
+ addIntersectingEventsInRangeToSelectionInWorldSpace.
+ apply(this, arguments);
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.addClosestInstantEventToSelection(this.model_.instantEvents,
+ worldX, worldMaxDist, selection);
+ tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ }
+ };
+
+ return {
+ ModelTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html
new file mode 100644
index 00000000000..53e19146ae2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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/thread.html">
+<link rel="import" href="/tracing/ui/tracks/model_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Range = tr.b.math.Range;
+ const VIEW_L_WORLD = 100;
+ const VIEW_R_WORLD = 1000;
+
+ function testGenerateStripes(times, expectedRanges) {
+ const ranges = tr.ui.tracks.ModelTrack.generateStripes_(
+ times, VIEW_L_WORLD, VIEW_R_WORLD);
+
+ assert.sameDeepMembers(ranges, expectedRanges);
+ }
+
+ test('generateStripesInside', function() {
+ const range200To500 = Range.fromExplicitRange(200, 500);
+ const range800To900 = Range.fromExplicitRange(800, 900);
+ const range998To999 = Range.fromExplicitRange(998, 999);
+ testGenerateStripes([], []);
+ testGenerateStripes([200, 500], [range200To500]);
+ testGenerateStripes([200, 500, 800, 900], [range200To500, range800To900]);
+ testGenerateStripes(
+ [200, 500, 800, 900, 998, 999],
+ [range200To500, range800To900, range998To999]);
+ });
+
+ test('generateStripesOutside', function() {
+ const range101To999 = Range.fromExplicitRange(101, 999);
+ // Far left.
+ testGenerateStripes([0, 99], []);
+ testGenerateStripes([0, 10, 50, 99], []);
+ testGenerateStripes([0, 99, 101, 999], [range101To999]);
+ testGenerateStripes([0, 10, 50, 99, 101, 999], [range101To999]);
+
+ // Far right.
+ testGenerateStripes([1001, 2000], []);
+ testGenerateStripes([1001, 2000, 3000, 4000], []);
+ testGenerateStripes([101, 999, 1001, 2000], [range101To999]);
+ testGenerateStripes([101, 999, 1001, 2000, 3000, 4000], [range101To999]);
+
+ // Far both.
+ testGenerateStripes([0, 99, 1001, 2000], []);
+ testGenerateStripes([0, 10, 50, 99, 1001, 2000], []);
+ testGenerateStripes([0, 10, 50, 99, 1001, 2000, 3000, 4000], []);
+ testGenerateStripes([0, 99, 101, 999, 1001, 2000], [range101To999]);
+ });
+
+ test('generateStripesOverlap', function() {
+ const rangeLeftWorldTo101 = Range.fromExplicitRange(VIEW_L_WORLD, 101);
+ const range102To103 = Range.fromExplicitRange(102, 103);
+ const range200To900 = Range.fromExplicitRange(200, 900);
+ const range997To998 = Range.fromExplicitRange(997, 998);
+ const range999ToRightWorld = Range.fromExplicitRange(999, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+
+
+ // Left overlap.
+ testGenerateStripes([0, 101], [rangeLeftWorldTo101]);
+ testGenerateStripes([0, 1, 2, 101], [rangeLeftWorldTo101]);
+ testGenerateStripes(
+ [2, 101, 102, 103],
+ [rangeLeftWorldTo101, range102To103]);
+ testGenerateStripes(
+ [0, 1, 2, 101, 102, 103],
+ [rangeLeftWorldTo101, range102To103]);
+ testGenerateStripes(
+ [0, 1, 2, 101, 102, 103, 1001, 3000],
+ [rangeLeftWorldTo101, range102To103]);
+
+ // Right overlap.
+ testGenerateStripes([999, 2000], [range999ToRightWorld]);
+ testGenerateStripes([999, 2000, 3000, 4000], [range999ToRightWorld]);
+ testGenerateStripes(
+ [997, 998, 999, 2000],
+ [range997To998, range999ToRightWorld]);
+ testGenerateStripes(
+ [997, 998, 999, 2000, 3000, 4000],
+ [range997To998, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 997, 998, 999, 2000, 3000, 4000],
+ [range997To998, range999ToRightWorld]);
+
+ // Both overlap.
+ testGenerateStripes([0, 2000], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes(
+ [0, 101, 999, 2000],
+ [rangeLeftWorldTo101, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 101, 200, 900, 999, 2000],
+ [rangeLeftWorldTo101, range200To900, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 90, 101, 999, 2000, 3000, 4000],
+ [rangeLeftWorldTo101, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 90, 101, 200, 900, 999, 2000, 3000, 4000],
+ [rangeLeftWorldTo101, range200To900, range999ToRightWorld]);
+ });
+
+ test('generateStripesOdd', function() {
+ const range500To900 = Range.fromExplicitRange(500, 900);
+ const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200);
+ const rangeLeftWorldTo500 = Range.fromExplicitRange(VIEW_L_WORLD, 500);
+ const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+
+ // One VSync.
+ testGenerateStripes([0], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes([500], [range500ToRightWorld]);
+ testGenerateStripes([1500], []);
+
+ // Multiple VSyncs.
+ testGenerateStripes([0, 10, 20], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes([0, 500, 2000], [rangeLeftWorldTo500]);
+ testGenerateStripes([0, 10, 500], [range500ToRightWorld]);
+ testGenerateStripes([0, 10, 2000], []);
+ testGenerateStripes(
+ [0, 200, 500],
+ [rangeLeftWorldTo200, range500ToRightWorld]);
+ testGenerateStripes(
+ [0, 200, 500, 900],
+ [rangeLeftWorldTo200, range500To900]);
+ });
+
+ test('generateStripesBorder', function() {
+ const rangeLeftWorldToLeftWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_L_WORLD);
+ const rangeRightWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_R_WORLD, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+ const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200);
+ const range200To500 = Range.fromExplicitRange(200, 500);
+ const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD);
+ testGenerateStripes([0, VIEW_L_WORLD], [rangeLeftWorldToLeftWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, VIEW_L_WORLD],
+ [rangeLeftWorldToLeftWorld]);
+ testGenerateStripes(
+ [VIEW_R_WORLD, 2000],
+ [rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_R_WORLD, VIEW_R_WORLD],
+ [rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, VIEW_R_WORLD],
+ [rangeLeftWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, 200, 500, VIEW_R_WORLD],
+ [rangeLeftWorldTo200, range500ToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, 200, 500, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, range200To500,
+ rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, 10, VIEW_L_WORLD, VIEW_R_WORLD, 2000, 3000],
+ [rangeLeftWorldToRightWorld]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html
new file mode 100644
index 00000000000..8b5fd837f0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html
@@ -0,0 +1,240 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/model_settings.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a group of objects in multiple rows.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const MultiRowTrack = tr.ui.b.define(
+ 'multi-row-track', tr.ui.tracks.ContainerTrack);
+
+ MultiRowTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.tooltip_ = '';
+ this.heading_ = '';
+
+ this.groupingSource_ = undefined;
+ this.itemsToGroup_ = undefined;
+
+ this.defaultToCollapsedWhenSubRowCountMoreThan = 1;
+
+ this.currentSubRowsWithHeadings_ = undefined;
+ this.expanded_ = true;
+ },
+
+ get itemsToGroup() {
+ return this.itemsToGroup_;
+ },
+
+ setItemsToGroup(itemsToGroup, opt_groupingSource) {
+ this.itemsToGroup_ = itemsToGroup;
+ this.groupingSource_ = opt_groupingSource;
+ this.currentSubRowsWithHeadings_ = undefined;
+ this.updateContents_();
+ this.updateExpandedStateFromGroupingSource_();
+ },
+
+ /**
+ * Opt-out from using buildSubRows_() and provide prebuilt rows.
+ * Array of {row: [rowItems...], heading} dicts is expected as an argument.
+ */
+ setPrebuiltSubRows(groupingSource, subRowsWithHeadings) {
+ this.itemsToGroup_ = undefined;
+ this.groupingSource_ = groupingSource;
+ this.currentSubRowsWithHeadings_ = subRowsWithHeadings;
+ this.updateContents_();
+ this.updateExpandedStateFromGroupingSource_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(h) {
+ this.heading_ = h;
+ this.updateHeadingAndTooltip_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(t) {
+ this.tooltip_ = t;
+ this.updateHeadingAndTooltip_();
+ },
+
+ get subRows() {
+ return this.currentSubRowsWithHeadings_.map(elem => elem.row);
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 0;
+ },
+
+ get expanded() {
+ return this.expanded_;
+ },
+
+ set expanded(expanded) {
+ if (this.expanded_ === expanded) return;
+
+ this.expanded_ = expanded;
+ this.expandedStateChanged_();
+ },
+
+ onHeadingClicked_(e) {
+ if (this.subRows.length <= 1) return;
+
+ this.expanded = !this.expanded;
+
+ if (this.groupingSource_) {
+ const modelSettings = new tr.model.ModelSettings(
+ this.groupingSource_.model);
+ modelSettings.setSettingFor(this.groupingSource_, 'expanded',
+ this.expanded);
+ }
+
+ e.stopPropagation();
+ },
+
+ updateExpandedStateFromGroupingSource_() {
+ if (this.groupingSource_) {
+ const numSubRows = this.subRows.length;
+ const modelSettings = new tr.model.ModelSettings(
+ this.groupingSource_.model);
+ if (numSubRows > 1) {
+ let defaultExpanded;
+ if (numSubRows > this.defaultToCollapsedWhenSubRowCountMoreThan) {
+ defaultExpanded = false;
+ } else {
+ defaultExpanded = true;
+ }
+ this.expanded = modelSettings.getSettingFor(
+ this.groupingSource_, 'expanded', defaultExpanded);
+ } else {
+ this.expanded = undefined;
+ }
+ }
+ },
+
+ expandedStateChanged_() {
+ const minH = Math.max(2, Math.ceil(18 / this.children.length));
+ const h = (this.expanded_ ? 18 : minH) + 'px';
+
+ for (let i = 0; i < this.children.length; i++) {
+ this.children[i].height = h;
+ if (i === 0) {
+ this.children[i].arrowVisible = true;
+ }
+ this.children[i].expanded = this.expanded;
+ }
+
+ if (this.children.length === 1) {
+ this.children[0].expanded = true;
+ this.children[0].arrowVisible = false;
+ }
+ },
+
+ updateContents_() {
+ tr.ui.tracks.ContainerTrack.prototype.updateContents_.call(this);
+ this.detach(); // Clear sub-tracks.
+
+ if (this.currentSubRowsWithHeadings_ === undefined) {
+ // No prebuilt rows, build it.
+ if (this.itemsToGroup_ === undefined) {
+ return;
+ }
+ const subRows = this.buildSubRows_(this.itemsToGroup_);
+ this.currentSubRowsWithHeadings_ = subRows.map(row => {
+ return {row, heading: undefined};
+ });
+ }
+ if (this.currentSubRowsWithHeadings_ === undefined ||
+ this.currentSubRowsWithHeadings_.length === 0) {
+ return;
+ }
+
+ const addSubTrackEx = (items, opt_heading) => {
+ const track = this.addSubTrack_(items);
+ if (opt_heading !== undefined) {
+ track.heading = opt_heading;
+ }
+ track.addEventListener(
+ 'heading-clicked', this.onHeadingClicked_.bind(this));
+ };
+
+ if (this.currentSubRowsWithHeadings_[0].heading !== undefined &&
+ this.currentSubRowsWithHeadings_[0].heading !== this.heading_) {
+ // Create an empty row to render the group's title there.
+ addSubTrackEx([]);
+ }
+
+ for (const subRowWithHeading of this.currentSubRowsWithHeadings_) {
+ const subRow = subRowWithHeading.row;
+ if (subRow.length === 0) {
+ continue;
+ }
+ addSubTrackEx(subRow, subRowWithHeading.heading);
+ }
+
+ this.updateHeadingAndTooltip_();
+ this.expandedStateChanged_();
+ },
+
+ updateHeadingAndTooltip_() {
+ if (!Polymer.dom(this).firstChild) return;
+
+ Polymer.dom(this).firstChild.heading = this.heading_;
+ Polymer.dom(this).firstChild.tooltip = this.tooltip_;
+ },
+
+ /**
+ * Breaks up the list of slices into N rows, each of which is a list of
+ * slices that are non overlapping.
+ */
+ buildSubRows_(itemsToGroup) {
+ throw new Error('Not implemented');
+ },
+
+ addSubTrack_(subRowItems) {
+ throw new Error('Not implemented');
+ },
+
+ areArrayContentsSame_(a, b) {
+ if (!a || !b) return false;
+
+ if (!a.length || !b.length) return false;
+
+ if (a.length !== b.length) return false;
+
+ for (let i = 0; i < a.length; ++i) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+ }
+ };
+
+ return {
+ MultiRowTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html
new file mode 100644
index 00000000000..b97b48c3ef0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/object_instance_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a ObjectInstanceGroup.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const ObjectInstanceGroupTrack = tr.ui.b.define(
+ 'object-instance-group-track', tr.ui.tracks.MultiRowTrack);
+
+ ObjectInstanceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('object-instance-group-track');
+ this.objectInstances_ = undefined;
+ },
+
+ get objectInstances() {
+ return this.itemsToGroup;
+ },
+
+ set objectInstances(objectInstances) {
+ this.setItemsToGroup(objectInstances);
+ },
+
+ addSubTrack_(objectInstances) {
+ const hasMultipleRows = this.subRows.length > 1;
+ const track = new tr.ui.tracks.ObjectInstanceTrack(this.viewport);
+ track.objectInstances = objectInstances;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ },
+
+ buildSubRows_(objectInstances) {
+ objectInstances.sort(function(x, y) {
+ return x.creationTs - y.creationTs;
+ });
+
+ const subRows = [];
+ for (let i = 0; i < objectInstances.length; i++) {
+ const objectInstance = objectInstances[i];
+
+ let found = false;
+ for (let j = 0; j < subRows.length; j++) {
+ const subRow = subRows[j];
+ const lastItemInSubRow = subRow[subRow.length - 1];
+ if (objectInstance.creationTs >= lastItemInSubRow.deletionTs) {
+ found = true;
+ subRow.push(objectInstance);
+ break;
+ }
+ }
+ if (!found) {
+ subRows.push([objectInstance]);
+ }
+ }
+ return subRows;
+ },
+ updateHeadingAndTooltip_() {
+ }
+ };
+
+ return {
+ ObjectInstanceGroupTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css
new file mode 100644
index 00000000000..0919e85524e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2012 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.
+ */
+
+.object-instance-track {
+ height: 18px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html
new file mode 100644
index 00000000000..f21a87d04db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html
@@ -0,0 +1,294 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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="stylesheet" href="/tracing/ui/tracks/object_instance_track.css">
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const SelectionState = tr.model.SelectionState;
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ /**
+ * A track that displays an array of Slice objects.
+ * @constructor
+ * @extends {Track}
+ */
+ const ObjectInstanceTrack = tr.ui.b.define(
+ 'object-instance-track', tr.ui.tracks.Track);
+
+ ObjectInstanceTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('object-instance-track');
+ this.objectInstances_ = [];
+ this.objectSnapshots_ = [];
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get objectInstances() {
+ return this.objectInstances_;
+ },
+
+ set objectInstances(objectInstances) {
+ if (!objectInstances || objectInstances.length === 0) {
+ this.heading = '';
+ this.objectInstances_ = [];
+ this.objectSnapshots_ = [];
+ return;
+ }
+ this.heading = objectInstances[0].baseTypeName;
+ this.objectInstances_ = objectInstances;
+ this.objectSnapshots_ = [];
+ this.objectInstances_.forEach(function(instance) {
+ this.objectSnapshots_.push.apply(
+ this.objectSnapshots_, instance.snapshots);
+ }, this);
+ this.objectSnapshots_.sort(function(a, b) {
+ return a.ts - b.ts;
+ });
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ get snapshotRadiusView() {
+ return 7 * (window.devicePixelRatio || 1);
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawObjectInstances_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawObjectInstances_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const height = bounds.height * pixelRatio;
+ const halfHeight = height * 0.5;
+ const twoPi = Math.PI * 2;
+
+ // Culling parameters.
+ const dt = this.viewport.currentDisplayTransform;
+ const snapshotRadiusView = this.snapshotRadiusView;
+ const snapshotRadiusWorld = dt.xViewVectorToWorld(height);
+
+ // Instances
+ const objectInstances = this.objectInstances_;
+ let loI = tr.b.findLowIndexInSortedArray(
+ objectInstances,
+ function(instance) {
+ return instance.deletionTs;
+ },
+ viewLWorld);
+ ctx.save();
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ for (let i = loI; i < objectInstances.length; ++i) {
+ const instance = objectInstances[i];
+ const x = instance.creationTs;
+ if (x > viewRWorld) break;
+
+ const right = instance.deletionTs === Number.MAX_VALUE ?
+ viewRWorld : instance.deletionTs;
+ const xView = dt.xWorldToView(x);
+ const widthView = dt.xWorldVectorToView(right - x);
+ ctx.fillStyle = EventPresenter.getObjectInstanceColor(instance);
+ ctx.fillRect(xView, pixelRatio, widthView, height - 2 * pixelRatio);
+ }
+ ctx.restore();
+
+ // Snapshots. Has to run in worldspace because ctx.arc gets transformed.
+ const objectSnapshots = this.objectSnapshots_;
+ loI = tr.b.findLowIndexInSortedArray(
+ objectSnapshots,
+ function(snapshot) {
+ return snapshot.ts + snapshotRadiusWorld;
+ },
+ viewLWorld);
+ for (let i = loI; i < objectSnapshots.length; ++i) {
+ const snapshot = objectSnapshots[i];
+ const x = snapshot.ts;
+ if (x - snapshotRadiusWorld > viewRWorld) break;
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.fillStyle = EventPresenter.getObjectSnapshotColor(snapshot);
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, snapshotRadiusView, 0, twoPi);
+ ctx.fill();
+ if (snapshot.selected) {
+ ctx.lineWidth = 5;
+ ctx.strokeStyle = 'rgb(100,100,0)';
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, snapshotRadiusView - 1, 0, twoPi);
+ ctx.lineWidth = 2;
+ ctx.strokeStyle = 'rgb(255,255,0)';
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.stroke();
+ }
+ }
+ ctx.lineWidth = 1;
+
+ // For performance reasons we only check the SelectionState of the first
+ // instance. If it's DIMMED we assume that all are DIMMED.
+ // TODO(egraether): Allow partial highlight.
+ let selectionState = SelectionState.NONE;
+ if (objectInstances.length &&
+ objectInstances[0].selectionState === SelectionState.DIMMED) {
+ selectionState = SelectionState.DIMMED;
+ }
+
+ // Dim the track when there is an active highlight.
+ if (selectionState === SelectionState.DIMMED) {
+ const width = bounds.width * pixelRatio;
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+ }
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.objectInstance_ !== undefined) {
+ this.objectInstance_.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ }
+
+ if (this.objectSnapshots_ !== undefined) {
+ this.objectSnapshots_.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ }
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ // Pick snapshots first.
+ let foundSnapshot = false;
+ function onSnapshot(snapshot) {
+ selection.push(snapshot);
+ foundSnapshot = true;
+ }
+ const snapshotRadiusView = this.snapshotRadiusView;
+ const snapshotRadiusWorld = viewPixWidthWorld * snapshotRadiusView;
+ tr.b.iterateOverIntersectingIntervals(
+ this.objectSnapshots_,
+ function(x) { return x.ts - snapshotRadiusWorld; },
+ function(x) { return 2 * snapshotRadiusWorld; },
+ loWX, hiWX,
+ onSnapshot);
+ if (foundSnapshot) return;
+
+ // Try picking instances.
+ tr.b.iterateOverIntersectingIntervals(
+ this.objectInstances_,
+ function(x) { return x.creationTs; },
+ function(x) { return x.deletionTs - x.creationTs; },
+ loWX, hiWX,
+ (value) => { selection.push(value); });
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {event} The current event item.
+ * @param {Number} offset Number of slices away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ let events;
+ if (event instanceof tr.model.ObjectSnapshot) {
+ events = this.objectSnapshots_;
+ } else if (event instanceof tr.model.ObjectInstance) {
+ events = this.objectInstances_;
+ } else {
+ throw new Error('Unrecognized event');
+ }
+
+ const index = events.indexOf(event);
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < events.length) {
+ selection.push(events[newIndex]);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const snapshot = tr.b.findClosestElementInSortedArray(
+ this.objectSnapshots_,
+ function(x) { return x.ts; },
+ worldX,
+ worldMaxDist);
+
+ if (!snapshot) return;
+
+ selection.push(snapshot);
+
+ // TODO(egraether): Search for object instances as well, which was not
+ // implemented because it makes little sense with the current visual and
+ // needs to take care of overlapping intervals.
+ }
+ };
+
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(ObjectInstanceTrack, options);
+
+ return {
+ ObjectInstanceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html
new file mode 100644
index 00000000000..8312d0ba7e5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/event_set.html">
+<link rel="import" href="/tracing/model/object_collection.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const ObjectInstanceTrack = tr.ui.tracks.ObjectInstanceTrack;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createObjects = function() {
+ const objects = new tr.model.ObjectCollection({});
+ const scopedId1 = new tr.model.ScopedId('ptr', '0x1000');
+ objects.idWasCreated(scopedId1, 'tr.e.cc', 'Frame', 10);
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 10, 'snapshot-1');
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 25, 'snapshot-2');
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 40, 'snapshot-3');
+ objects.idWasDeleted(scopedId1, 'tr.e.cc', 'Frame', 45);
+
+ const scopedId2 = new tr.model.ScopedId('ptr', '0x1001');
+ objects.idWasCreated(scopedId2, 'skia', 'Picture', 20);
+ objects.addSnapshot(scopedId2, 'skia', 'Picture', 20, 'snapshot-1');
+ objects.idWasDeleted(scopedId2, 'skia', 'Picture', 25);
+ return objects;
+ };
+
+ test('instantiate', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+ frames[0].snapshots[1].selectionState =
+ tr.model.SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = ObjectInstanceTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasic';
+ track.objectInstances = frames;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('selectionHitTestingWithThreadTrack', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+
+ const track = ObjectInstanceTrack(new Viewport());
+ track.objectInstances = frames;
+
+ // Hit outside range
+ let selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 8, 8.1, 0.1, selection);
+ assert.strictEqual(selection.length, 0);
+
+ // Hit the first snapshot, via pixel-nearness.
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 9.98, 9.99, 0.1, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectSnapshot);
+
+ // Hit the instance, between the 1st and 2nd snapshots
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 20, 20.1, 0.1, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectInstance);
+ });
+
+ test('addEventNearToProvidedEventToSelection', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+
+ const track = ObjectInstanceTrack(new Viewport());
+ track.objectInstances = frames;
+
+ const instance = new tr.model.ObjectInstance(
+ {}, new tr.model.ScopedId('ptr', '0x1000'), 'cat', 'n', 10);
+
+ assert.doesNotThrow(function() {
+ track.addEventNearToProvidedEventToSelection(instance, 0, undefined);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html
new file mode 100644
index 00000000000..e43bce0cec2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html
@@ -0,0 +1,105 @@
+<!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/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays threads with only scheduling information but no
+ * slices. By default it's collapsed to minimize initial visual difference
+ * while allowing the user to drill-down into whatever process is
+ * interesting to them.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const OtherThreadsTrack = tr.ui.b.define(
+ 'other-threads-track', tr.ui.tracks.OtherThreadsTrack);
+
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ OtherThreadsTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+
+ this.header_ = document.createElement('tr-ui-b-heading');
+ this.header_.addEventListener('click', this.onHeaderClick_.bind(this));
+ this.header_.heading = 'Other Threads';
+ this.header_.tooltip = 'Threads with only scheduling information';
+ this.header_.arrowVisible = true;
+
+ this.threads_ = [];
+ this.expanded = false;
+ this.collapsible_ = true;
+ },
+
+ set threads(threads) {
+ this.threads_ = threads;
+ this.updateContents_();
+ },
+
+ set collapsible(collapsible) {
+ this.collapsible_ = collapsible;
+ this.updateContents_();
+ },
+
+ onHeaderClick_(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.expanded = !this.expanded;
+ },
+
+ get expanded() {
+ return this.header_.expanded;
+ },
+
+ set expanded(expanded) {
+ expanded = !!expanded;
+
+ if (this.expanded === expanded) return;
+
+ this.header_.expanded = expanded;
+
+ // Expanding and collapsing tracks is, essentially, growing and shrinking
+ // the viewport. We dispatch a change event to trigger any processing
+ // to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.detach();
+ if (this.collapsible_) {
+ Polymer.dom(this).appendChild(this.header_);
+ }
+ if (this.expanded || !this.collapsible_) {
+ for (const thread of this.threads_) {
+ const track = new tr.ui.tracks.ThreadTrack(this.viewport);
+ track.thread = thread;
+ if (!track.hasVisibleContent) return;
+
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ }
+ }
+ };
+
+ return {
+ OtherThreadsTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html
new file mode 100644
index 00000000000..d32cd21e9a3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html
@@ -0,0 +1,81 @@
+<!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/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<style>
+.power-series-track {
+ height: 90px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+
+ /**
+ * A track that displays a PowerSeries.
+ *
+ * @constructor
+ * @extends {ChartTrack}
+ */
+ const PowerSeriesTrack = tr.ui.b.define('power-series-track', ChartTrack);
+
+ PowerSeriesTrack.prototype = {
+ __proto__: ChartTrack.prototype,
+
+ decorate(viewport) {
+ ChartTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('power-series-track');
+ this.heading = 'Power';
+ this.powerSeries_ = undefined;
+ },
+
+ set powerSeries(powerSeries) {
+ this.powerSeries_ = powerSeries;
+
+ this.series = this.buildChartSeries_();
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ get hasVisibleContent() {
+ return (this.powerSeries_ && this.powerSeries_.samples.length > 0);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ containerToTrackMap.addContainer(this.powerSeries_, this);
+ },
+
+ buildChartSeries_() {
+ if (!this.hasVisibleContent) return [];
+
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+ const pts = this.powerSeries_.samples.map(function(smpl) {
+ return new tr.ui.tracks.ChartPoint(smpl, smpl.start, smpl.powerInW);
+ });
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: ColorScheme.getColorIdForGeneralPurposeString(this.heading)
+ };
+
+ return [new tr.ui.tracks.ChartSeries(pts, seriesYAxis, renderingConfig)];
+ }
+ };
+
+ return {
+ PowerSeriesTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html
new file mode 100644
index 00000000000..9e8b03aa168
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html
@@ -0,0 +1,121 @@
+<!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/model/device.html'>
+<link rel='import' href='/tracing/model/model.html'>
+<link rel='import' href='/tracing/model/power_series.html'>
+<link rel='import' href='/tracing/ui/base/constants.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel='import' href='/tracing/ui/tracks/container_to_track_map.html'>
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+<link rel="import" href="/tracing/ui/tracks/power_series_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Device = tr.model.Device;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+ const PowerSeriesTrack = tr.ui.tracks.PowerSeriesTrack;
+
+ const createDrawingContainer = function(series) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ if (series) {
+ series.updateBounds();
+ setDisplayTransformFromBounds(viewport, series.bounds);
+ }
+
+ return drawingContainer;
+ };
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ const setDisplayTransformFromBounds = function(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ };
+
+ test('instantiate', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(0.5, 2);
+ series.addPowerSample(1, 3);
+ series.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(series);
+ const track = new PowerSeriesTrack(drawingContainer.viewport);
+ track.powerSeries = series;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+ });
+
+ test('hasVisibleContent_trueWithPowerSamplesPresent', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(0.5, 2);
+ series.addPowerSample(1, 3);
+ series.addPowerSample(1.5, 4);
+
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ track.powerSeries = series;
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedPowerSeries', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ track.powerSeries = undefined;
+
+ assert.notOk(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithEmptyPowerSeries', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ const series = new PowerSeries(new Model().device);
+ track.powerSeries = series;
+
+ assert.notOk(track.hasVisibleContent);
+ });
+
+ test('addContainersToTrackMap', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const powerSeriesTrack = new PowerSeriesTrack(viewport);
+ const series = new PowerSeries(new Model().device);
+ powerSeriesTrack.powerSeries = series;
+
+ const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+ powerSeriesTrack.addContainersToTrackMap(containerToTrackMap);
+
+ assert.strictEqual(
+ containerToTrackMap.getTrackByStableId('Device.PowerSeries'),
+ powerSeriesTrack);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html
new file mode 100644
index 00000000000..247d707f58f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html
@@ -0,0 +1,70 @@
+<!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/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ALLOCATED_MEMORY_TRACK_HEIGHT = 50;
+
+ /**
+ * A track that displays an array of ProcessMemoryDump objects.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const ProcessMemoryDumpTrack = tr.ui.b.define(
+ 'process-memory-dump-track', tr.ui.tracks.ContainerTrack);
+
+ ProcessMemoryDumpTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.memoryDumps_ = undefined;
+ },
+
+ get memoryDumps() {
+ return this.memoryDumps_;
+ },
+
+ set memoryDumps(memoryDumps) {
+ this.memoryDumps_ = memoryDumps;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ // Show no tracks if there are no dumps.
+ if (!this.memoryDumps_ || !this.memoryDumps_.length) return;
+
+ this.appendAllocatedMemoryTrack_();
+ },
+
+ appendAllocatedMemoryTrack_() {
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ this.memoryDumps_);
+ if (!series) return;
+
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = 'Memory per component';
+ track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ };
+
+ return {
+ ProcessMemoryDumpTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html
new file mode 100644
index 00000000000..897d2883c62
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html
@@ -0,0 +1,58 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Viewport = tr.ui.TimelineViewport;
+ const ProcessMemoryDumpTrack = tr.ui.tracks.ProcessMemoryDumpTrack;
+ const createTestProcessMemoryDumps =
+ tr.ui.tracks.createTestProcessMemoryDumps;
+
+ function instantiateTrack(withVMRegions, withAllocatorDumps,
+ expectedTrackCount) {
+ const dumps = createTestProcessMemoryDumps(
+ withVMRegions, withAllocatorDumps);
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new ProcessMemoryDumpTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.invalidate();
+
+ track.memoryDumps = dumps;
+
+ // TODO(petrcermak): Check that the div has indeed zero size.
+ if (expectedTrackCount > 0) {
+ this.addHTMLOutput(div);
+ }
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.lengthOf(track.tracks_, expectedTrackCount);
+ }
+
+ test('instantiate_withoutMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, false, 0);
+ });
+ test('instantiate_withMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, true, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html
new file mode 100644
index 00000000000..c6560f40118
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html
@@ -0,0 +1,130 @@
+<!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/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ /**
+ * Visualizes a Process's state using a series of rects to represent activity.
+ * @constructor
+ */
+ const ProcessSummaryTrack = tr.ui.b.define('process-summary-track',
+ tr.ui.tracks.RectTrack);
+
+ ProcessSummaryTrack.buildRectsFromProcess = function(process) {
+ if (!process) return [];
+
+ const ops = [];
+ // build list of start/end ops for each top level or important slice
+ const pushOp = function(isStart, time, slice) {
+ ops.push({
+ isStart,
+ time,
+ slice
+ });
+ };
+ for (const tid in process.threads) {
+ const sliceGroup = process.threads[tid].sliceGroup;
+
+ sliceGroup.topLevelSlices.forEach(function(slice) {
+ pushOp(true, slice.start, undefined);
+ pushOp(false, slice.end, undefined);
+ });
+ sliceGroup.slices.forEach(function(slice) {
+ if (slice.important) {
+ pushOp(true, slice.start, slice);
+ pushOp(false, slice.end, slice);
+ }
+ });
+ }
+ ops.sort(function(a, b) { return a.time - b.time; });
+
+ const rects = [];
+ /**
+ * Build a row of rects which display one way for unimportant activity,
+ * and during important slices, show up as those important slices.
+ *
+ * If an important slice starts in the middle of another,
+ * just drop it on the floor.
+ */
+ const genericColorId = ColorScheme.getColorIdForReservedName(
+ 'generic_work');
+ const pushRect = function(start, end, slice) {
+ rects.push(new tr.ui.tracks.Rect(
+ slice, /* modelItem: show selection state of slice if present */
+ slice ? slice.title : '', /* title */
+ slice ? slice.colorId : genericColorId, /* colorId */
+ start, /* start */
+ end - start /* duration */));
+ };
+ let depth = 0;
+ let currentSlice = undefined;
+ let lastStart = undefined;
+ ops.forEach(function(op) {
+ depth += op.isStart ? 1 : -1;
+
+ if (currentSlice) {
+ // simply find end of current important slice
+ if (!op.isStart && op.slice === currentSlice) {
+ // important slice has ended
+ pushRect(lastStart, op.time, currentSlice);
+ lastStart = depth >= 1 ? op.time : undefined;
+ currentSlice = undefined;
+ }
+ } else {
+ if (op.isStart) {
+ if (depth === 1) {
+ lastStart = op.time;
+ currentSlice = op.slice;
+ } else if (op.slice) {
+ // switch to slice
+ if (op.time !== lastStart) {
+ pushRect(lastStart, op.time, undefined);
+ lastStart = op.time;
+ }
+ currentSlice = op.slice;
+ }
+ } else {
+ if (depth === 0) {
+ pushRect(lastStart, op.time, undefined);
+ lastStart = undefined;
+ }
+ }
+ }
+ });
+ return rects;
+ };
+
+ ProcessSummaryTrack.prototype = {
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get process() {
+ return this.process_;
+ },
+
+ set process(process) {
+ this.process_ = process;
+ this.rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+ }
+ };
+
+ return {
+ ProcessSummaryTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html
new file mode 100644
index 00000000000..1d071f9d0ce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html
@@ -0,0 +1,110 @@
+<!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/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/ui/tracks/process_summary_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ProcessSummaryTrack = tr.ui.tracks.ProcessSummaryTrack;
+
+ test('buildRectSimple', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ process = model.getOrCreateProcess(1);
+ // XXXX
+ // XXXX
+ const thread1 = process.getOrCreateThread(1);
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 1, duration: 4}));
+ const thread2 = process.getOrCreateThread(2);
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 4, duration: 4}));
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(rects.length, 1);
+ const rect = rects[0];
+ assert.closeTo(rect.start, 1, 1e-5);
+ assert.closeTo(rect.end, 8, 1e-5);
+ });
+
+ test('buildRectComplex', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ process = model.getOrCreateProcess(1);
+ // XXXX X X XX
+ // XXXX XXX X
+ const thread1 = process.getOrCreateThread(1);
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 1, duration: 4}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 9, duration: 1}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 11, duration: 1}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 13, duration: 2}));
+ const thread2 = process.getOrCreateThread(2);
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 4, duration: 4}));
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 9, duration: 3}));
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 16, duration: 1}));
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(4, rects.length);
+ assert.closeTo(rects[0].start, 1, 1e-5);
+ assert.closeTo(rects[0].end, 8, 1e-5);
+ assert.closeTo(rects[1].start, 9, 1e-5);
+ assert.closeTo(rects[1].end, 12, 1e-5);
+ assert.closeTo(rects[2].start, 13, 1e-5);
+ assert.closeTo(rects[2].end, 15, 1e-5);
+ assert.closeTo(rects[3].start, 16, 1e-5);
+ assert.closeTo(rects[3].end, 17, 1e-5);
+ });
+
+ test('buildRectImportantSlice', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ // [ unimportant ]
+ // [important]
+ const a = tr.c.TestUtils.newSliceEx(
+ {title: 'unimportant', start: 4, duration: 21});
+ const b = tr.c.TestUtils.newSliceEx(
+ {title: 'important', start: 9, duration: 11});
+ b.important = true;
+ process = model.getOrCreateProcess(1);
+ process.getOrCreateThread(1).sliceGroup.pushSlices([a, b]);
+
+ model.importantSlice = b;
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(3, rects.length);
+ assert.closeTo(rects[0].start, 4, 1e-5);
+ assert.closeTo(rects[0].end, 9, 1e-5);
+ assert.closeTo(rects[1].start, 9, 1e-5);
+ assert.closeTo(rects[1].end, 20, 1e-5);
+ assert.closeTo(rects[2].start, 20, 1e-5);
+ assert.closeTo(rects[2].end, 25, 1e-5);
+
+ // middle rect represents important slice, so colorId & title are preserved
+ assert.strictEqual(rects[1].title, model.importantSlice.title);
+ assert.strictEqual(rects[1].colorId, model.importantSlice.colorId);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html
new file mode 100644
index 00000000000..1be51cbf4cb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/draw_helpers.html">
+<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track_base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase;
+
+ /**
+ * @constructor
+ */
+ const ProcessTrack = tr.ui.b.define('process-track', ProcessTrackBase);
+
+ ProcessTrack.prototype = {
+ __proto__: ProcessTrackBase.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ProcessTrackBase.prototype.decorate.call(this, viewport);
+ },
+
+ drawTrack(type) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.INSTANT_EVENT: {
+ if (!this.processBase.instantEvents ||
+ this.processBase.instantEvents.length === 0) {
+ break;
+ }
+
+ const ctx = this.context();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+
+ tr.ui.b.drawInstantSlicesAsLines(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.processBase.instantEvents,
+ 2);
+
+ ctx.restore();
+
+ break;
+ }
+
+ case tr.ui.tracks.DrawType.BACKGROUND:
+ this.drawBackground_();
+ // Don't bother recursing further, Process is the only level that
+ // draws backgrounds.
+ return;
+ }
+
+ tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+ },
+
+ drawBackground_() {
+ const ctx = this.context();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ let draw = false;
+ ctx.fillStyle = '#eee';
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track) ||
+ (this.children[i] instanceof tr.ui.tracks.SpacingTrack)) {
+ continue;
+ }
+
+ draw = !draw;
+ if (!draw) continue;
+
+ const bounds = this.children[i].getBoundingClientRect();
+ ctx.fillRect(0, pixelRatio * (bounds.top - canvasBounds.top),
+ ctx.canvas.width, pixelRatio * bounds.height);
+ }
+ },
+
+ // Process maps to processBase because we derive from ProcessTrackBase.
+ set process(process) {
+ this.processBase = process;
+ },
+
+ get process() {
+ return this.processBase;
+ },
+
+ get eventContainer() {
+ return this.process;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.process, this);
+ },
+
+ appendMemoryDumpTrack_() {
+ const processMemoryDumps = this.process.memoryDumps;
+ if (processMemoryDumps.length) {
+ const pmdt = new tr.ui.tracks.ProcessMemoryDumpTrack(this.viewport_);
+ pmdt.memoryDumps = processMemoryDumps;
+ Polymer.dom(this).appendChild(pmdt);
+ }
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onPickHit(instantEvent) {
+ selection.push(instantEvent);
+ }
+ const instantEventWidth = 2 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.processBase.instantEvents,
+ function(x) { return x.start; },
+ function(x) { return x.duration + instantEventWidth; },
+ loWX, hiWX,
+ onPickHit.bind(this));
+
+ tr.ui.tracks.ContainerTrack.prototype.
+ addIntersectingEventsInRangeToSelectionInWorldSpace.
+ apply(this, arguments);
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.addClosestInstantEventToSelection(this.processBase.instantEvents,
+ worldX, worldMaxDist, selection);
+ tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ }
+ };
+
+ return {
+ ProcessTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css
new file mode 100644
index 00000000000..25fa5f015b7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css
@@ -0,0 +1,39 @@
+/* Copyright (c) 2013 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.
+ */
+
+.process-track-header {
+ display: flex;
+ flex: 0 0 auto;
+ background-image: -webkit-gradient(linear,
+ 0 0, 100% 0,
+ from(#E5E5E5),
+ to(#D1D1D1));
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ font-size: 75%;
+}
+
+.process-track-name {
+ flex-grow: 1;
+}
+
+.process-track-name:before {
+ content: '\25B8'; /* Right triangle */
+ padding: 0 5px;
+}
+
+.process-track-base.expanded .process-track-name:before {
+ content: '\25BE'; /* Down triangle */
+}
+
+.process-track-close {
+ color: black;
+ border: 1px solid transparent;
+ padding: 0px 2px;
+}
+
+.process-track-close:hover {
+ border: 1px solid grey;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html
new file mode 100644
index 00000000000..89358b8411e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html
@@ -0,0 +1,313 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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="stylesheet" href="/tracing/ui/tracks/process_track_base.css">
+
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/model_settings.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/counter_track.html">
+<link rel="import" href="/tracing/ui/tracks/frame_track.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/other_threads_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_summary_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ObjectSnapshotView = tr.ui.analysis.ObjectSnapshotView;
+ const ObjectInstanceView = tr.ui.analysis.ObjectInstanceView;
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ /**
+ * Visualizes a Process by building ThreadTracks and CounterTracks.
+ * @constructor
+ */
+ const ProcessTrackBase =
+ tr.ui.b.define('process-track-base', tr.ui.tracks.ContainerTrack);
+
+ ProcessTrackBase.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+
+ this.processBase_ = undefined;
+
+ Polymer.dom(this).classList.add('process-track-base');
+ Polymer.dom(this).classList.add('expanded');
+
+ this.processNameEl_ = tr.ui.b.createSpan();
+ Polymer.dom(this.processNameEl_).classList.add('process-track-name');
+
+ this.closeEl_ = tr.ui.b.createSpan();
+ Polymer.dom(this.closeEl_).classList.add('process-track-close');
+ this.closeEl_.textContent = 'X';
+
+ this.headerEl_ = tr.ui.b.createDiv({className: 'process-track-header'});
+ Polymer.dom(this.headerEl_).appendChild(this.processNameEl_);
+ Polymer.dom(this.headerEl_).appendChild(this.closeEl_);
+ this.headerEl_.addEventListener('click', this.onHeaderClick_.bind(this));
+
+ Polymer.dom(this).appendChild(this.headerEl_);
+ },
+
+ get processBase() {
+ return this.processBase_;
+ },
+
+ set processBase(processBase) {
+ this.processBase_ = processBase;
+
+ if (this.processBase_) {
+ const modelSettings = new tr.model.ModelSettings(
+ this.processBase_.model);
+ const defaultValue = this.processBase_.important;
+ this.expanded = modelSettings.getSettingFor(
+ this.processBase_, 'expanded', defaultValue);
+ }
+
+ this.updateContents_();
+ },
+
+ get expanded() {
+ return Polymer.dom(this).classList.contains('expanded');
+ },
+
+ set expanded(expanded) {
+ expanded = !!expanded;
+
+ if (this.expanded === expanded) return;
+
+ Polymer.dom(this).classList.toggle('expanded');
+
+ // Expanding and collapsing tracks is, essentially, growing and shrinking
+ // the viewport. We dispatch a change event to trigger any processing
+ // to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ if (!this.processBase_) return;
+
+ const modelSettings = new tr.model.ModelSettings(this.processBase_.model);
+ modelSettings.setSettingFor(this.processBase_, 'expanded', expanded);
+ this.updateContents_();
+ this.viewport.rebuildEventToTrackMap();
+ this.viewport.rebuildContainerToTrackMap();
+ },
+
+ set visible(visible) {
+ if (visible === this.visible) return;
+ this.hidden = !visible;
+
+ tr.b.dispatchSimpleEvent(this, 'visibility');
+ // Changing the visibility of the tracks can grow and shrink the viewport.
+ // We dispatch a change event to trigger any processing to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ if (!this.processBase_) return;
+
+ this.updateContents_();
+ this.viewport.rebuildEventToTrackMap();
+ this.viewport.rebuildContainerToTrackMap();
+ },
+
+ get visible() {
+ return !this.hidden;
+ },
+
+ get hasVisibleContent() {
+ if (this.expanded) {
+ return this.children.length > 1;
+ }
+ return true;
+ },
+
+ onHeaderClick_(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (e.target === this.closeEl_) {
+ this.visible = false;
+ } else {
+ this.expanded = !this.expanded;
+ }
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ if (!this.processBase_) return;
+
+ Polymer.dom(this.processNameEl_).textContent =
+ this.processBase_.userFriendlyName;
+ this.headerEl_.title = this.processBase_.userFriendlyDetails;
+
+ // Create the object instance tracks for this process.
+ this.willAppendTracks_();
+ if (this.expanded) {
+ this.appendMemoryDumpTrack_();
+ this.appendObjectInstanceTracks_();
+ this.appendCounterTracks_();
+ this.appendFrameTrack_();
+ this.appendThreadTracks_();
+ } else {
+ this.appendSummaryTrack_();
+ }
+ this.didAppendTracks_();
+ },
+
+ willAppendTracks_() {
+ },
+
+ didAppendTracks_() {
+ },
+
+ appendMemoryDumpTrack_() {
+ },
+
+ appendSummaryTrack_() {
+ const track = new tr.ui.tracks.ProcessSummaryTrack(this.viewport);
+ track.process = this.process;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ // no spacing track, since this track only shown in collapsed state
+ },
+
+ appendFrameTrack_() {
+ const frames = this.process ? this.process.frames : undefined;
+ if (!frames || !frames.length) return;
+
+ const track = new tr.ui.tracks.FrameTrack(this.viewport);
+ track.frames = frames;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendObjectInstanceTracks_() {
+ const instancesByTypeName =
+ this.processBase_.objects.getAllInstancesByTypeName();
+ const instanceTypeNames = Object.keys(instancesByTypeName);
+ instanceTypeNames.sort();
+
+ let didAppendAtLeastOneTrack = false;
+ instanceTypeNames.forEach(function(typeName) {
+ const allInstances = instancesByTypeName[typeName];
+
+ // If a object snapshot has a view it will be shown,
+ // unless the view asked for it to not be shown.
+ let instanceViewInfo = ObjectInstanceView.getTypeInfo(
+ undefined, typeName);
+ let snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+ undefined, typeName);
+ if (instanceViewInfo && !instanceViewInfo.metadata.showInTrackView) {
+ instanceViewInfo = undefined;
+ }
+ if (snapshotViewInfo && !snapshotViewInfo.metadata.showInTrackView) {
+ snapshotViewInfo = undefined;
+ }
+ const hasViewInfo = instanceViewInfo || snapshotViewInfo;
+
+ // There are some instances that don't merit their own track in
+ // the UI. Filter them out.
+ const visibleInstances = [];
+ for (let i = 0; i < allInstances.length; i++) {
+ const instance = allInstances[i];
+
+ // Do not create tracks for instances that have no snapshots.
+ if (instance.snapshots.length === 0) continue;
+
+ // Do not create tracks for instances that have implicit snapshots
+ // and don't have a view.
+ if (instance.hasImplicitSnapshots && !hasViewInfo) continue;
+
+ visibleInstances.push(instance);
+ }
+ if (visibleInstances.length === 0) return;
+
+ // Look up the constructor for this track, or use the default
+ // constructor if none exists.
+ let trackConstructor =
+ tr.ui.tracks.ObjectInstanceTrack.getConstructor(
+ undefined, typeName);
+ if (!trackConstructor) {
+ snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+ undefined, typeName);
+ if (snapshotViewInfo && snapshotViewInfo.metadata.showInstances) {
+ trackConstructor = tr.ui.tracks.ObjectInstanceGroupTrack;
+ } else {
+ trackConstructor = tr.ui.tracks.ObjectInstanceTrack;
+ }
+ }
+ const track = new trackConstructor(this.viewport);
+ track.objectInstances = visibleInstances;
+ Polymer.dom(this).appendChild(track);
+ didAppendAtLeastOneTrack = true;
+ }, this);
+ if (didAppendAtLeastOneTrack) {
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ },
+
+ appendCounterTracks_() {
+ // Add counter tracks for this process.
+ const counters = Object.values(this.processBase.counters);
+ counters.sort(tr.model.Counter.compare);
+
+ // Create the counters for this process.
+ counters.forEach(function(counter) {
+ const track = new tr.ui.tracks.CounterTrack(this.viewport);
+ track.counter = counter;
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }.bind(this));
+ },
+
+ appendThreadTracks_() {
+ // Get a sorted list of threads.
+ const threads = Object.values(this.processBase.threads);
+ threads.sort(tr.model.Thread.compare);
+
+ // Create the threads.
+ const otherThreads = [];
+ let hasVisibleThreads = false;
+ threads.forEach(function(thread) {
+ const track = new tr.ui.tracks.ThreadTrack(this.viewport);
+ track.thread = thread;
+ if (!track.hasVisibleContent) return;
+
+ if (track.hasSlices) {
+ hasVisibleThreads = true;
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ } else if (track.hasTimeSlices) {
+ otherThreads.push(thread);
+ }
+ }.bind(this));
+
+ if (otherThreads.length > 0) {
+ // If there's only 1 thread with scheduling-only information don't
+ // bother making a group, just display it directly
+ // Similarly if we are a process with only scheduling-only threads
+ // don't bother making a group as the process itself serves
+ // as the collapsable group
+ const track = new tr.ui.tracks.OtherThreadsTrack(this.viewport);
+ track.threads = otherThreads;
+ track.collapsible = otherThreads.length > 1 && hasVisibleThreads;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+ };
+
+ return {
+ ProcessTrackBase,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css
new file mode 100644
index 00000000000..0467c91562c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2014 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.
+ */
+
+.rect-track {
+ height: 18px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html
new file mode 100644
index 00000000000..65e073d32ee
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html
@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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="stylesheet" href="/tracing/ui/tracks/rect_track.css">
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/fast_rect_renderer.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Rect objects.
+ * @constructor
+ * @extends {Track}
+ */
+ const RectTrack = tr.ui.b.define(
+ 'rect-track', tr.ui.tracks.Track);
+
+ RectTrack.prototype = {
+
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('rect-track');
+ this.asyncStyle_ = false;
+ this.rects_ = null;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ set selectionGenerator(generator) {
+ this.heading_.selectionGenerator = generator;
+ },
+
+ set expanded(expanded) {
+ this.heading_.expanded = !!expanded;
+ },
+
+ set arrowVisible(arrowVisible) {
+ this.heading_.arrowVisible = !!arrowVisible;
+ },
+
+ get expanded() {
+ return this.heading_.expanded;
+ },
+
+ get asyncStyle() {
+ return this.asyncStyle_;
+ },
+
+ set asyncStyle(v) {
+ this.asyncStyle_ = !!v;
+ },
+
+ get rects() {
+ return this.rects_;
+ },
+
+ set rects(rects) {
+ this.rects_ = rects || [];
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ this.invalidateDrawingContainer();
+ },
+
+ get hasVisibleContent() {
+ return this.rects_.length > 0;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawRects_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawRects_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+
+ ctx.save();
+ const bounds = this.getBoundingClientRect();
+ tr.ui.b.drawSlices(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.rects_,
+ this.asyncStyle_);
+ ctx.restore();
+
+ if (bounds.height <= 6) return;
+
+ let fontSize;
+ let yOffset;
+ if (bounds.height < 15) {
+ fontSize = 6;
+ yOffset = 1.0;
+ } else {
+ fontSize = 10;
+ yOffset = 2.5;
+ }
+ tr.ui.b.drawLabels(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ this.rects_,
+ this.asyncStyle_,
+ fontSize,
+ yOffset);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.rects_ === undefined || this.rects_ === null) {
+ return;
+ }
+
+ this.rects_.forEach(function(rect) {
+ rect.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onRect(rect) {
+ rect.addToSelection(selection);
+ }
+ onRect = onRect.bind(this);
+ const instantEventWidth = 2 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.rects_,
+ function(x) { return x.start; },
+ function(x) {
+ return x.duration === 0 ?
+ x.duration + instantEventWidth :
+ x.duration;
+ },
+ loWX, hiWX,
+ onRect);
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {rect} The current rect.
+ * @param {Number} offset Number of rects away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ const index = this.rects_.findIndex(rect => rect.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex < 0 || newIndex >= this.rects_.length) return false;
+
+ this.rects_[newIndex].addToSelection(selection);
+ return true;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ for (let i = 0; i < this.rects_.length; ++i) {
+ // TODO(petrcermak): Rather than unpacking the proxy item here,
+ // we should probably add an addToSelectionIfMatching(selection, filter)
+ // method to SelectableItem (#900).
+ const modelItem = this.rects_[i].modelItem;
+ if (!modelItem) continue;
+
+ if (filter.matchSlice(modelItem)) {
+ selection.push(modelItem);
+ }
+ }
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const rect = tr.b.findClosestIntervalInSortedIntervals(
+ this.rects_,
+ function(x) { return x.start; },
+ function(x) { return x.end; },
+ worldX,
+ worldMaxDist);
+
+ if (!rect) return;
+
+ rect.addToSelection(selection);
+ }
+ };
+
+ /**
+ * A filled rectangle with a title.
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function Rect(modelItem, title, colorId, start, duration) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.title = title;
+ this.colorId = colorId;
+ this.start = start;
+ this.duration = duration;
+ this.end = start + duration;
+ }
+
+ Rect.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype
+ };
+
+ return {
+ RectTrack,
+ Rect,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html
new file mode 100644
index 00000000000..ec81a8835d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html
@@ -0,0 +1,412 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/event_set.html">
+<link rel="import" href="/tracing/model/slice.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const RectTrack = tr.ui.tracks.RectTrack;
+ const Rect = tr.ui.tracks.Rect;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const Viewport = tr.ui.TimelineViewport;
+
+ test('instantiate_withRects', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasicRects';
+ track.rects = [
+ new Rect(undefined, 'a', 0, 1, 1),
+ new Rect(undefined, 'b', 1, 2.1, 4.8),
+ new Rect(undefined, 'b', 1, 7, 0.5),
+ new Rect(undefined, 'c', 2, 7.6, 0.4)
+ ];
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 8.8, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_withSlices', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasicSlices';
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 8.8, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_shrinkingRectSize', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testShrinkingRectSizes';
+ let x = 0;
+ const widths = [10, 5, 4, 3, 2, 1, 0.5, 0.4, 0.3, 0.2, 0.1, 0.05];
+ const slices = [];
+ for (let i = 0; i < widths.length; i++) {
+ const s = new Rect(undefined, 'a', 1, x, widths[i]);
+ x += s.duration + 0.5;
+ slices.push(s);
+ }
+ track.rects = slices;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 1.1 * x, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_elide', function() {
+ const optDicts = [{ trackName: 'elideOff', elide: false },
+ { trackName: 'elideOn', elide: true }];
+
+ const tooLongTitle = 'Unless eliding this SHOULD NOT BE DISPLAYED. ';
+ const bigTitle = 'Very big title name that goes on longer ' +
+ 'than you may expect';
+
+ for (const dictIndex in optDicts) {
+ const dict = optDicts[dictIndex];
+
+ const div = document.createElement('div');
+ Polymer.dom(div).appendChild(document.createTextNode(dict.trackName));
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.SHOULD_ELIDE_TEXT = dict.elide;
+ track.heading = 'Visual: ' + dict.trackName;
+ track.rects = [
+ // title, colorId, start, args, opt_duration
+ new Rect(undefined, 'a ' + tooLongTitle + bigTitle, 0, 1, 1),
+ new Rect(undefined, bigTitle, 1, 2.1, 4.8),
+ new Rect(undefined, 'cccc cccc cccc', 1, 7, 0.5),
+ new Rect(undefined, 'd', 2, 7.6, 1.0)
+ ];
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 9.5, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ }
+ });
+
+ test('findAllObjectsMatchingInRectTrack', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+ const selection = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('b'), selection);
+
+ const predictedSelection = new EventSet(
+ [track.rects[1].modelItem, track.rects[2].modelItem]);
+ assert.isTrue(selection.equals(predictedSelection));
+ });
+
+ test('selectionHitTesting', function() {
+ const testEl = document.createElement('div');
+ Polymer.dom(testEl).appendChild(
+ tr.ui.b.createScopedStyle('heading { width: 100px; }'));
+ testEl.style.width = '600px';
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ track.heading = 'testSelectionHitTesting';
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 5, {}, 4.8)
+ ];
+ const y = track.getBoundingClientRect().top + 5;
+ const pixelRatio = window.devicePixelRatio || 1;
+ const wW = 10;
+ const vW = drawingContainer.canvas.getBoundingClientRect().width;
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, wW, vW * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let selection = new EventSet();
+ let x = (1.5 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(new EventSet(track.rects[0].modelItem)));
+
+ selection = new EventSet();
+ x = (2.1 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.strictEqual(0, selection.length);
+
+ selection = new EventSet();
+ x = (6.8 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(new EventSet(track.rects[1].modelItem)));
+
+ selection = new EventSet();
+ x = (9.9 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.strictEqual(0, selection.length);
+ });
+
+ test('elide', function() {
+ const testEl = document.createElement('div');
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ const bigtitle = 'Super duper long long title ' +
+ 'holy moly when did you get so verbose?';
+ const smalltitle = 'small';
+ track.heading = 'testElide';
+ track.rects = [
+ // title, colorId, start, args, opt_duration
+ new ThreadSlice('', bigtitle, 0, 1, {}, 1),
+ new ThreadSlice('', smalltitle, 1, 2, {}, 1)
+ ];
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 3.3, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let stringWidthPair = undefined;
+ const pixWidth = dt.xViewVectorToWorld(1);
+
+ // Small titles on big slices are not elided.
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ smalltitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ smalltitle),
+ 1);
+ assert.strictEqual(smalltitle, stringWidthPair.string);
+
+ // Keep shrinking the slice until eliding starts.
+ let elidedWhenSmallEnough = false;
+ for (let sliceLength = 1; sliceLength >= 0.00001; sliceLength /= 2.0) {
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ smalltitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ smalltitle),
+ sliceLength);
+ if (stringWidthPair.string.length < smalltitle.length) {
+ elidedWhenSmallEnough = true;
+ break;
+ }
+ }
+ assert.isTrue(elidedWhenSmallEnough);
+
+ // Big titles are elided immediately.
+ let superBigTitle = '';
+ for (let x = 0; x < 10; x++) {
+ superBigTitle += bigtitle;
+ }
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ superBigTitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ superBigTitle),
+ 1);
+ assert.isTrue(stringWidthPair.string.length < superBigTitle.length);
+
+ // And elided text ends with ...
+ const len = stringWidthPair.string.length;
+ assert.strictEqual('...', stringWidthPair.string.substring(len - 3, len));
+ });
+
+ test('rectTrackAddItemNearToProvidedEvent', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+ let sel = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('b'), sel);
+
+ // Select to the right of B.
+ const selRight = new EventSet();
+ let ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), 1, selRight);
+ assert.isTrue(ret);
+ assert.strictEqual(
+ track.rects[2].modelItem, tr.b.getFirstElement(selRight));
+
+ // Select to the right of the 2nd b.
+ const selRight2 = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), 2, selRight2);
+ assert.isTrue(ret);
+ assert.strictEqual(
+ track.rects[3].modelItem, tr.b.getFirstElement(selRight2));
+
+ // Select to 2 to the right of the 2nd b.
+ const selRightOfRight = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(selRight), 1, selRightOfRight);
+ assert.isTrue(ret);
+ assert.strictEqual(track.rects[3].modelItem,
+ tr.b.getFirstElement(selRightOfRight));
+
+ // Select to the right of the rightmost slice.
+ let selNone = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(selRightOfRight), 1, selNone);
+ assert.isFalse(ret);
+ assert.strictEqual(0, selNone.length);
+
+ // Select A and then select left.
+ sel = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('a'), sel);
+
+ selNone = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), -1, selNone);
+ assert.isFalse(ret);
+ assert.strictEqual(0, selNone.length);
+ });
+
+ test('rectTrackAddClosestEventToSelection', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+
+ // Before with not range.
+ let sel = new EventSet();
+ track.addClosestEventToSelection(0, 0, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Before with negative range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.5, -10, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Before first slice.
+ sel = new EventSet();
+ track.addClosestEventToSelection(0.5, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem)));
+
+ // Within first slice closer to start.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem)));
+
+ // Between slices with good range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(2.08, 3, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem)));
+
+ // Between slices with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(2.05, 0.03, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Within slice closer to end.
+ sel = new EventSet();
+ track.addClosestEventToSelection(6, 100, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem)));
+
+ // Within slice with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.8, 0.1, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // After last slice with good range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(8.5, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[3].modelItem)));
+
+ // After last slice with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(10, 1, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html
new file mode 100644
index 00000000000..1f764019cfc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Sample objects.
+ * @constructor
+ * @extends {RectTrack}
+ */
+ const SampleTrack = tr.ui.b.define(
+ 'sample-track', tr.ui.tracks.RectTrack);
+
+ SampleTrack.prototype = {
+
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get samples() {
+ return this.rects;
+ },
+
+ set samples(samples) {
+ this.rects = samples;
+ }
+ };
+
+ return {
+ SampleTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html
new file mode 100644
index 00000000000..0fb17df65f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/profile_node.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/sample_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SampleTrack = tr.ui.tracks.SampleTrack;
+ const Sample = tr.model.Sample;
+ const ProfileNode = tr.model.ProfileNode;
+
+ test('modelMapping', function() {
+ const track = new SampleTrack(new tr.ui.TimelineViewport());
+ const node = new ProfileNode(1, {
+ functionName: 'a'
+ }, undefined);
+ const sample = new Sample(10, 'instructions_retired', node);
+ track.samples = [sample];
+ const me0 = track.rects[0].modelItem;
+ assert.strictEqual(me0, sample);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html
new file mode 100644
index 00000000000..36f09566b07
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a SliceGroup.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const SliceGroupTrack = tr.ui.b.define(
+ 'slice-group-track', tr.ui.tracks.MultiRowTrack);
+
+ SliceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('slice-group-track');
+ this.group_ = undefined;
+ // Set the collapse threshold so we don't collapse by default, but the
+ // user can explicitly collapse if they want it.
+ this.defaultToCollapsedWhenSubRowCountMoreThan = 100;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(group) {
+ this.group_ = group;
+ this.setItemsToGroup(this.group_.slices, this.group_);
+ },
+
+ get eventContainer() {
+ return this.group;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.group, this);
+ },
+
+ /**
+ * Breaks up the list of slices into N rows, each of which is a list of
+ * slices that are non overlapping.
+ */
+ buildSubRows_(slices) {
+ const precisionUnit = this.group.model.intrinsicTimeUnit;
+
+ // This function works by walking through slices by start time.
+ //
+ // The basic idea here is to insert each slice as deep into the subrow
+ // list as it can go such that every subSlice is fully contained by its
+ // parent slice.
+ //
+ // Visually, if we start with this:
+ // 0: [ a ]
+ // 1: [ b ]
+ // 2: [c][d]
+ //
+ // To place this slice:
+ // [e]
+ // We first check row 2's last item, [d]. [e] wont fit into [d] (they dont
+ // even intersect). So we go to row 1. That gives us [b], and [d] wont fit
+ // into that either. So, we go to row 0 and its last slice, [a]. That can
+ // completely contain [e], so that means we should add [e] as a subchild
+ // of [a]. That puts it on row 1, yielding:
+ // 0: [ a ]
+ // 1: [ b ][e]
+ // 2: [c][d]
+ //
+ // If we then get this slice:
+ // [f]
+ // We do the same deepest-to-shallowest walk of the subrows trying to fit
+ // it. This time, it doesn't fit in any open slice. So, we simply append
+ // it to row 0:
+ // 0: [ a ] [f]
+ // 1: [ b ][e]
+ // 2: [c][d]
+ if (!slices.length) return [];
+
+ const ops = [];
+ for (let i = 0; i < slices.length; i++) {
+ if (slices[i].subSlices) {
+ slices[i].subSlices.splice(0,
+ slices[i].subSlices.length);
+ }
+ ops.push(i);
+ }
+
+ ops.sort(function(ix, iy) {
+ const x = slices[ix];
+ const y = slices[iy];
+ if (x.start !== y.start) return x.start - y.start;
+
+ // Elements get inserted into the slices array in order of when the
+ // slices start. Because slices must be properly nested, we break
+ // start-time ties by assuming that the elements appearing earlier in
+ // the slices array (and thus ending earlier) start earlier.
+ return ix - iy;
+ });
+
+ const subRows = [[]];
+ this.badSlices_ = []; // TODO(simonjam): Connect this again.
+
+ for (let i = 0; i < ops.length; i++) {
+ const op = ops[i];
+ const slice = slices[op];
+
+ // Try to fit the slice into the existing subrows.
+ let inserted = false;
+ for (let j = subRows.length - 1; j >= 0; j--) {
+ if (subRows[j].length === 0) continue;
+
+ const insertedSlice = subRows[j][subRows[j].length - 1];
+ if (slice.start < insertedSlice.start) {
+ this.badSlices_.push(slice);
+ inserted = true;
+ }
+ if (insertedSlice.bounds(slice, precisionUnit)) {
+ // Insert it into subRow j + 1.
+ while (subRows.length <= j + 1) {
+ subRows.push([]);
+ }
+ subRows[j + 1].push(slice);
+ if (insertedSlice.subSlices) {
+ insertedSlice.subSlices.push(slice);
+ }
+ inserted = true;
+ break;
+ }
+ }
+ if (inserted) continue;
+
+ // Append it to subRow[0] as a root.
+ subRows[0].push(slice);
+ }
+
+ return subRows;
+ }
+ };
+
+ return {
+ SliceGroupTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html
new file mode 100644
index 00000000000..a8b5842f945
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html
@@ -0,0 +1,299 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/slice_group.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ProcessTrack = tr.ui.tracks.ProcessTrack;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const SliceGroup = tr.model.SliceGroup;
+ const SliceGroupTrack = tr.ui.tracks.SliceGroupTrack;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('subRowBuilderBasic', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+ const sB = group.pushSlice(newSliceEx({title: 'a', start: 3, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderBasic2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderNestedExactly', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 4}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sB]);
+ assert.deepEqual(subRows[1], [sA]);
+ });
+
+ test('subRowBuilderInstantEvents', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderTwoInstantEvents', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOutOfOrderAddition', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ][ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderOutOfOrderAddition2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 5}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOnNestedZeroLength', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b1 ] []<- b2 where b2.duration = 0 and b2.end === a.end.
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB1 = group.pushSlice(newSliceEx(
+ {title: 'b1', start: 1, duration: 2}));
+ const sB2 = group.pushSlice(newSliceEx(
+ {title: 'b2', start: 4, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB1, sB2]);
+ });
+
+ test('subRowBuilderOnGroup1', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ] [ c ]
+ // [ b ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx({title: 'c', start: 5, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA, sC]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOnGroup2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ] [ d ]
+ // [ b ]
+ // [ c ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx(
+ {title: 'c', start: 1.75, duration: 0.5}));
+ const sD = group.pushSlice(newSliceEx(
+ {title: 'c', start: 5, duration: 0.25}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+
+ const subRows = track.subRows;
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 3);
+ assert.deepEqual(subRows[0], [sA, sD]);
+ assert.deepEqual(subRows[1], [sB]);
+ assert.deepEqual(subRows[2], [sC]);
+ });
+
+ test('trackFiltering', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+
+ assert.strictEqual(track.subRows.length, 2);
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('sliceGroupContainerMap', function() {
+ const vp = new tr.ui.TimelineViewport();
+ const containerToTrack = vp.containerToTrackMap;
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new SliceGroup(thread);
+
+ const processTrack = new ProcessTrack(vp);
+ const threadTrack = new ThreadTrack(vp);
+ const groupTrack = new SliceGroupTrack(vp);
+ processTrack.process = process;
+ threadTrack.thread = thread;
+ groupTrack.group = group;
+ Polymer.dom(processTrack).appendChild(threadTrack);
+ Polymer.dom(threadTrack).appendChild(groupTrack);
+
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+
+ assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+ assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+ assert.isUndefined(
+ containerToTrack.getTrackByStableId('123.456.SliceGroup'));
+
+ vp.modelTrackContainer = {
+ addContainersToTrackMap(containerToTrackMap) {
+ processTrack.addContainersToTrackMap(containerToTrackMap);
+ },
+ addEventListener() {}
+ };
+ vp.rebuildContainerToTrackMap();
+
+ // Check that all tracks call childs' addContainersToTrackMap()
+ // by checking the resulting map.
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123'), processTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456'), threadTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456.SliceGroup'), groupTrack);
+
+ // Check the track's eventContainer getter.
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html
new file mode 100644
index 00000000000..1e1386bff66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Slice objects.
+ * @constructor
+ * @extends {RectTrack}
+ */
+ const SliceTrack = tr.ui.b.define(
+ 'slice-track', tr.ui.tracks.RectTrack);
+
+ SliceTrack.prototype = {
+
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get slices() {
+ return this.rects;
+ },
+
+ set slices(slices) {
+ this.rects = slices;
+ }
+ };
+
+ return {
+ SliceTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html
new file mode 100644
index 00000000000..7ba42d3dc79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/slice.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SliceTrack = tr.ui.tracks.SliceTrack;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ test('modelMapping', function() {
+ const track = new SliceTrack(new tr.ui.TimelineViewport());
+ const slice = new ThreadSlice('', 'a', 0, 1, {}, 1);
+ track.slices = [slice];
+ const me0 = track.rects[0].modelItem;
+ assert.strictEqual(slice, me0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css
new file mode 100644
index 00000000000..094eee0862d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css
@@ -0,0 +1,7 @@
+/* Copyright (c) 2013 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.
+ */
+.spacing-track {
+ height: 4px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html
new file mode 100644
index 00000000000..a321066daa2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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="stylesheet" href="/tracing/ui/tracks/spacing_track.css">
+
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track used to provide whitespace between the tracks above and below it.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.Track}
+ */
+ const SpacingTrack = tr.ui.b.define('spacing-track', tr.ui.tracks.Track);
+
+ SpacingTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('spacing-track');
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ }
+ };
+
+ return {
+ SpacingTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html
new file mode 100644
index 00000000000..7a292c04113
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays traces as stacked bars.
+ * @constructor
+ * @extends {Track}
+ */
+ const StackedBarsTrack = tr.ui.b.define(
+ 'stacked-bars-track', tr.ui.tracks.Track);
+
+ StackedBarsTrack.prototype = {
+
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('stacked-bars-track');
+ this.objectInstance_ = null;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ const objectSnapshots = this.objectInstance_.snapshots;
+ objectSnapshots.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ },
+
+ /**
+ * Used to hit-test clicks in the graph.
+ */
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onSnapshot(snapshot) {
+ selection.push(snapshot);
+ }
+
+ const snapshots = this.objectInstance_.snapshots;
+ const maxBounds = this.objectInstance_.parent.model.bounds.max;
+
+ tr.b.iterateOverIntersectingIntervals(
+ snapshots,
+ function(x) { return x.ts; },
+ function(x, i) {
+ if (i === snapshots.length - 1) {
+ if (snapshots.length === 1) {
+ return maxBounds;
+ }
+
+ return snapshots[i].ts - snapshots[i - 1].ts;
+ }
+
+ return snapshots[i + 1].ts - snapshots[i].ts;
+ },
+ loWX, hiWX,
+ onSnapshot);
+ },
+
+ /**
+ * Add the item to the left or right of the provided item, if any, to the
+ * selection.
+ * @param {slice} The current slice.
+ * @param {Number} offset Number of slices away from the object to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (!(event instanceof tr.model.ObjectSnapshot)) {
+ throw new Error('Unrecognized event');
+ }
+ const objectSnapshots = this.objectInstance_.snapshots;
+ const index = objectSnapshots.indexOf(event);
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < objectSnapshots.length) {
+ selection.push(objectSnapshots[newIndex]);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const snapshot = tr.b.findClosestElementInSortedArray(
+ this.objectInstance_.snapshots,
+ function(x) { return x.ts; },
+ worldX,
+ worldMaxDist);
+
+ if (!snapshot) return;
+
+ selection.push(snapshot);
+ }
+ };
+
+ return {
+ StackedBarsTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css
new file mode 100644
index 00000000000..4e063bbad48
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css
@@ -0,0 +1,10 @@
+/* Copyright (c) 2012 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.
+ */
+
+.thread-track {
+ flex-direction: column;
+ display: flex;
+ position: relative;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html
new file mode 100644
index 00000000000..c6ea8fa576c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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="stylesheet" href="/tracing/ui/tracks/thread_track.css">
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/async_slice_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/sample_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * Visualizes a Thread using a series of SliceTracks.
+ * @constructor
+ */
+ const ThreadTrack = tr.ui.b.define('thread-track',
+ tr.ui.tracks.ContainerTrack);
+ ThreadTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('thread-track');
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ },
+
+ get thread() {
+ return this.thread_;
+ },
+
+ set thread(thread) {
+ this.thread_ = thread;
+ this.updateContents_();
+ },
+
+ get hasVisibleContent() {
+ return this.tracks_.length > 0;
+ },
+
+ get hasSlices() {
+ return this.thread_.asyncSliceGroup.length > 0 ||
+ this.thread_.sliceGroup.length > 0;
+ },
+
+ get hasTimeSlices() {
+ return this.thread_.timeSlices;
+ },
+
+ get eventContainer() {
+ return this.thread;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.thread, this);
+ },
+
+ updateContents_() {
+ this.detach();
+
+ if (!this.thread_) return;
+
+ this.heading_.heading = this.thread_.userFriendlyName;
+ this.heading_.tooltip = this.thread_.userFriendlyDetails;
+
+ if (this.thread_.asyncSliceGroup.length) {
+ this.appendAsyncSliceTracks_();
+ }
+
+ this.appendThreadSamplesTracks_();
+
+ let needsHeading = false;
+ if (this.thread_.timeSlices) {
+ const timeSlicesTrack = new tr.ui.tracks.SliceTrack(this.viewport);
+ timeSlicesTrack.heading = '';
+ timeSlicesTrack.height = tr.ui.b.THIN_SLICE_HEIGHT + 'px';
+ timeSlicesTrack.slices = this.thread_.timeSlices;
+ if (timeSlicesTrack.hasVisibleContent) {
+ needsHeading = true;
+ Polymer.dom(this).appendChild(timeSlicesTrack);
+ }
+ }
+
+ if (this.thread_.sliceGroup.length) {
+ const track = new tr.ui.tracks.SliceGroupTrack(this.viewport);
+ track.heading = this.thread_.userFriendlyName;
+ track.tooltip = this.thread_.userFriendlyDetails;
+ track.group = this.thread_.sliceGroup;
+ if (track.hasVisibleContent) {
+ needsHeading = false;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+
+ if (needsHeading) {
+ Polymer.dom(this).appendChild(this.heading_);
+ }
+ },
+
+ appendAsyncSliceTracks_() {
+ const subGroups = this.thread_.asyncSliceGroup.viewSubGroups;
+ // TODO(kraynov): Support nested sub-groups.
+ subGroups.forEach(function(subGroup) {
+ const asyncTrack = new tr.ui.tracks.AsyncSliceGroupTrack(this.viewport);
+ asyncTrack.group = subGroup;
+ asyncTrack.heading = subGroup.title;
+ if (asyncTrack.hasVisibleContent) {
+ Polymer.dom(this).appendChild(asyncTrack);
+ }
+ }, this);
+ },
+
+ appendThreadSamplesTracks_() {
+ const threadSamples = this.thread_.samples;
+ if (threadSamples === undefined || threadSamples.length === 0) {
+ return;
+ }
+ const samplesByTitle = {};
+ threadSamples.forEach(function(sample) {
+ if (samplesByTitle[sample.title] === undefined) {
+ samplesByTitle[sample.title] = [];
+ }
+ samplesByTitle[sample.title].push(sample);
+ });
+
+ const sampleTitles = Object.keys(samplesByTitle);
+ sampleTitles.sort();
+
+ sampleTitles.forEach(function(sampleTitle) {
+ const samples = samplesByTitle[sampleTitle];
+ const samplesTrack = new tr.ui.tracks.SampleTrack(this.viewport);
+ samplesTrack.group = this.thread_;
+ samplesTrack.samples = samples;
+ samplesTrack.heading = this.thread_.userFriendlyName + ': ' +
+ sampleTitle;
+ samplesTrack.tooltip = this.thread_.userFriendlyDetails;
+ samplesTrack.selectionGenerator = function() {
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < samplesTrack.samples.length; i++) {
+ selection.push(samplesTrack.samples[i]);
+ }
+ return selection;
+ };
+ Polymer.dom(this).appendChild(samplesTrack);
+ }, this);
+ },
+
+ collapsedDidChange(collapsed) {
+ if (collapsed) {
+ let h = parseInt(this.tracks[0].height);
+ for (let i = 0; i < this.tracks.length; ++i) {
+ if (h > 2) {
+ this.tracks[i].height = Math.floor(h) + 'px';
+ } else {
+ this.tracks[i].style.display = 'none';
+ }
+ h = h * 0.5;
+ }
+ } else {
+ for (let i = 0; i < this.tracks.length; ++i) {
+ this.tracks[i].height = this.tracks[0].height;
+ this.tracks[i].style.display = '';
+ }
+ }
+ }
+ };
+
+ return {
+ ThreadTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html
new file mode 100644
index 00000000000..1ece1aa3f93
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/model/event_set.html">
+<link rel="import" href="/tracing/model/instant_event.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const HighlightInstantEvent = tr.model.ThreadHighlightInstantEvent;
+ const Process = tr.model.Process;
+ const EventSet = tr.model.EventSet;
+ const StackFrame = tr.model.StackFrame;
+ const Sample = tr.model.Sample;
+ const Thread = tr.model.Thread;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const Viewport = tr.ui.TimelineViewport;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('selectionHitTestingWithThreadTrack', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4));
+ t1.sliceGroup.pushSlice(new ThreadSlice('', 'b', 0, 5.1, {}, 4));
+
+ const testEl = document.createElement('div');
+ Polymer.dom(testEl).appendChild(
+ tr.ui.b.createScopedStyle('heading { width: 100px; }'));
+ testEl.style.width = '600px';
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new ThreadTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.updateCanvasSizeIfNeeded_();
+ track.thread = t1;
+
+ const y = track.getBoundingClientRect().top;
+ const h = track.getBoundingClientRect().height;
+ const wW = 10;
+ const vW = drawingContainer.canvas.getBoundingClientRect().width;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, wW, vW);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let selection = new EventSet();
+ const x = (1.5 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(
+ new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]])));
+
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelection(
+ (1.5 / wW) * vW, (1.8 / wW) * vW,
+ y, y + h, selection);
+ assert.isTrue(selection.equals(
+ new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]])));
+ });
+
+ test('filterThreadSlices', function() {
+ const model = new tr.Model();
+ const thread = new Thread(new Process(model, 7), 1);
+ thread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'a', start: 0, duration: 0}));
+ thread.asyncSliceGroup.push(newAsyncSliceNamed('a', 0, 5, thread, thread));
+ const t = new ThreadTrack(new tr.ui.TimelineViewport());
+ t.thread = thread;
+
+ assert.strictEqual(t.tracks_.length, 2);
+ assert.instanceOf(t.tracks_[0], tr.ui.tracks.AsyncSliceGroupTrack);
+ assert.instanceOf(t.tracks_[1], tr.ui.tracks.SliceGroupTrack);
+ });
+
+ test('sampleThreadSlices', function() {
+ let thread;
+ let cpu;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ cpu = model.kernel.getOrCreateCpu(1);
+ thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+ const nodeA = tr.c.TestUtils.newProfileNode(model, 'a');
+ const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA);
+ const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB);
+ const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA);
+
+ model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread,
+ undefined, 10));
+ model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread,
+ undefined, 10));
+
+ model.samples.push(new Sample(25, 'page_fault', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(35, 'page_fault', nodeD, thread,
+ undefined, 10));
+ }
+ });
+
+ const t = new ThreadTrack(new tr.ui.TimelineViewport());
+ t.thread = thread;
+ assert.strictEqual(t.tracks_.length, 2);
+
+ // Instructions retired
+ const t0 = t.tracks_[0];
+ assert.notEqual(t0.heading.indexOf('instructions_retired'), -1);
+ assert.instanceOf(t0, tr.ui.tracks.SampleTrack);
+ assert.strictEqual(t0.samples.length, 4);
+ t0.samples.forEach(function(s) {
+ assert.instanceOf(s, tr.model.Sample);
+ });
+
+ // page_fault
+ const t1 = t.tracks_[1];
+ assert.notEqual(t1.heading.indexOf('page_fault'), -1);
+ assert.instanceOf(t1, tr.ui.tracks.SampleTrack);
+ assert.strictEqual(t1.samples.length, 2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css
new file mode 100644
index 00000000000..3d56eef5b8d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css
@@ -0,0 +1,33 @@
+/* Copyright (c) 2012 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.
+ */
+
+.track-button {
+ background-color: rgba(255, 255, 255, 0.5);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ color: rgba(0,0,0,0.2);
+ font-size: 10px;
+ height: 12px;
+ text-align: center;
+ width: 12px;
+}
+
+.track-button:hover {
+ background-color: rgba(255, 255, 255, 1.0);
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 1);
+}
+
+.track-close-button {
+ left: 2px;
+ position: absolute;
+ top: 2px;
+}
+
+.track-collapse-button {
+ left: 3px;
+ position: absolute;
+ top: 2px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html
new file mode 100644
index 00000000000..fccad427740
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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="stylesheet" href="/tracing/ui/tracks/track.css">
+
+<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * The base class for all tracks, which render data into a provided div.
+ * @constructor
+ */
+ const Track = tr.ui.b.define('track',
+ tr.ui.b.ContainerThatDecoratesItsChildren);
+ Track.prototype = {
+ __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype,
+
+ decorate(viewport) {
+ tr.ui.b.ContainerThatDecoratesItsChildren.prototype.decorate.call(this);
+ if (viewport === undefined) {
+ throw new Error('viewport is required when creating a Track.');
+ }
+
+ this.viewport_ = viewport;
+ Polymer.dom(this).classList.add('track');
+ },
+
+ get viewport() {
+ return this.viewport_;
+ },
+
+ get drawingContainer() {
+ if (this instanceof tr.ui.tracks.DrawingContainer) return this;
+ let cur = this.parentElement;
+ while (cur) {
+ if (cur instanceof tr.ui.tracks.DrawingContainer) return cur;
+ cur = cur.parentElement;
+ }
+ return undefined;
+ },
+
+ get eventContainer() {
+ },
+
+ invalidateDrawingContainer() {
+ const dc = this.drawingContainer;
+ if (dc) dc.invalidate();
+ },
+
+ context() {
+ // This is a little weird here, but we have to be able to walk up the
+ // parent tree to get the context.
+ if (!Polymer.dom(this).parentNode) return undefined;
+
+ if (!Polymer.dom(this).parentNode.context) {
+ throw new Error('Parent container does not support context() method.');
+ }
+ return Polymer.dom(this).parentNode.context();
+ },
+
+ decorateChild_(childTrack) {
+ },
+
+ undecorateChild_(childTrack) {
+ if (childTrack.detach) {
+ childTrack.detach();
+ }
+ },
+
+ updateContents_() {
+ },
+
+ /**
+ * Wrapper function around draw() that performs transformations on the
+ * context necessary for the track's contents to be drawn in the right place
+ * given the current pan and zoom.
+ */
+ drawTrack(type) {
+ const ctx = this.context();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ this.draw(type, viewLWorld, viewRWorld, viewHeight);
+ ctx.restore();
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ },
+
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loVY, hiVY, selection) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const dt = this.viewport.currentDisplayTransform;
+ const viewPixWidthWorld = dt.xViewVectorToWorld(1);
+ const loWX = dt.xViewToWorld(loVX * pixelRatio);
+ const hiWX = dt.xViewToWorld(hiVX * pixelRatio);
+
+ const clientRect = this.getBoundingClientRect();
+ const a = Math.max(loVY, clientRect.top);
+ const b = Math.min(hiVY, clientRect.bottom);
+ if (a > b) return;
+
+ this.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ },
+
+ /**
+ * Gets implemented by supporting track types. The method adds the event
+ * closest to worldX to the selection.
+ *
+ * @param {number} worldX The position that is looked for.
+ * @param {number} worldMaxDist The maximum distance allowed from worldX to
+ * the event.
+ * @param {number} loY Lower Y bound of the search interval in view space.
+ * @param {number} hiY Upper Y bound of the search interval in view space.
+ * @param {Selection} selection Selection to which to add hits.
+ */
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ },
+
+ addClosestInstantEventToSelection(instantEvents, worldX,
+ worldMaxDist, selection) {
+ const instantEvent = tr.b.findClosestElementInSortedArray(
+ instantEvents,
+ function(x) { return x.start; },
+ worldX,
+ worldMaxDist);
+
+ if (!instantEvent) return;
+
+ selection.push(instantEvent);
+ }
+ };
+
+ return {
+ Track,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html
new file mode 100644
index 00000000000..620a35c8040
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html
@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.x-axis-track {
+ height: 12px;
+}
+
+.x-axis-track.tall-mode {
+ height: 30px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays the x-axis.
+ * @constructor
+ * @extends {Track}
+ */
+ const XAxisTrack = tr.ui.b.define('x-axis-track', tr.ui.tracks.Track);
+
+ XAxisTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('x-axis-track');
+ this.strings_secs_ = [];
+ this.strings_msecs_ = [];
+ this.strings_usecs_ = [];
+ this.strings_nsecs_ = [];
+
+ this.viewportChange_ = this.viewportChange_.bind(this);
+ viewport.addEventListener('change', this.viewportChange_);
+
+ const heading = document.createElement('tr-ui-b-heading');
+ heading.arrowVisible = false;
+ Polymer.dom(this).appendChild(heading);
+ },
+
+ detach() {
+ tr.ui.tracks.Track.prototype.detach.call(this);
+ this.viewport.removeEventListener('change',
+ this.viewportChange_);
+ },
+
+ viewportChange_() {
+ if (this.viewport.interestRange.isEmpty) {
+ Polymer.dom(this).classList.remove('tall-mode');
+ } else {
+ Polymer.dom(this).classList.add('tall-mode');
+ }
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GRID:
+ this.drawGrid_(viewLWorld, viewRWorld);
+ break;
+ case tr.ui.tracks.DrawType.MARKERS:
+ this.drawMarkers_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawGrid_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ const trackBounds = this.getBoundingClientRect();
+ const width = canvasBounds.width * pixelRatio;
+ const height = trackBounds.height * pixelRatio;
+
+ const hasInterestRange = !this.viewport.interestRange.isEmpty;
+
+ const xAxisHeightPx = hasInterestRange ? (height * 2) / 5 : height;
+
+ const vp = this.viewport;
+ const dt = vp.currentDisplayTransform;
+
+ vp.updateMajorMarkData(viewLWorld, viewRWorld);
+ const majorMarkDistanceWorld = vp.majorMarkWorldPositions.length > 1 ?
+ vp.majorMarkWorldPositions[1] - vp.majorMarkWorldPositions[0] : 0;
+
+ const numTicksPerMajor = 5;
+ const minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
+ const minorMarkDistancePx = dt.xWorldVectorToView(minorMarkDistanceWorld);
+
+ const minorTickHeight = Math.floor(xAxisHeightPx * 0.25);
+
+ ctx.save();
+
+ ctx.lineWidth = Math.round(pixelRatio);
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ const crispLineCorrection = (ctx.lineWidth % 2) / 2;
+ ctx.translate(crispLineCorrection, -crispLineCorrection);
+
+ ctx.fillStyle = 'rgb(0, 0, 0)';
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'top';
+
+ ctx.font = (9 * pixelRatio) + 'px sans-serif';
+
+ const tickLabels = [];
+ ctx.beginPath();
+ for (let i = 0; i < vp.majorMarkWorldPositions.length; i++) {
+ const curXWorld = vp.majorMarkWorldPositions[i];
+ const curXView = dt.xWorldToView(curXWorld);
+ const displayText = vp.majorMarkUnit.format(
+ curXWorld, {deltaValue: majorMarkDistanceWorld});
+ ctx.fillText(displayText, curXView + (2 * pixelRatio), 0);
+
+ // Draw major mark.
+ tr.ui.b.drawLine(ctx, curXView, 0, curXView, xAxisHeightPx);
+
+ // Draw minor marks.
+ if (minorMarkDistancePx) {
+ for (let j = 1; j < numTicksPerMajor; ++j) {
+ const xView = Math.floor(curXView + minorMarkDistancePx * j);
+ tr.ui.b.drawLine(ctx,
+ xView, xAxisHeightPx - minorTickHeight,
+ xView, xAxisHeightPx);
+ }
+ }
+ }
+
+ // Draw bottom bar.
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ tr.ui.b.drawLine(ctx, 0, height, width, height);
+ ctx.stroke();
+
+ // Give distance between directly adjacent markers.
+ if (!hasInterestRange) return;
+
+ // Draw middle bar.
+ tr.ui.b.drawLine(ctx, 0, xAxisHeightPx, width, xAxisHeightPx);
+ ctx.stroke();
+
+ // Distance Variables.
+ let displayDistance;
+ const displayTextColor = 'rgb(0,0,0)';
+
+ // Arrow Variables.
+ const arrowSpacing = 10 * pixelRatio;
+ const arrowColor = 'rgb(128,121,121)';
+ const arrowPosY = xAxisHeightPx * 1.75;
+ const arrowWidthView = 3 * pixelRatio;
+ const arrowLengthView = 10 * pixelRatio;
+ const spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);
+
+ ctx.textBaseline = 'middle';
+ ctx.font = (14 * pixelRatio) + 'px sans-serif';
+ const textPosY = arrowPosY;
+
+ const interestRange = vp.interestRange;
+
+ // If the range is zero, draw it's min timestamp next to the line.
+ if (interestRange.range === 0) {
+ const markerWorld = interestRange.min;
+ const markerView = dt.xWorldToView(markerWorld);
+
+ const textToDraw = vp.majorMarkUnit.format(markerWorld);
+ let textLeftView = markerView + 4 * pixelRatio;
+ const textWidthView = ctx.measureText(textToDraw).width;
+
+ // Put text to the left in case it gets cut off.
+ if (textLeftView + textWidthView > width) {
+ textLeftView = markerView - 4 * pixelRatio - textWidthView;
+ }
+
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+ return;
+ }
+
+ const leftMarker = interestRange.min;
+ const rightMarker = interestRange.max;
+
+ const leftMarkerView = dt.xWorldToView(leftMarker);
+ const rightMarkerView = dt.xWorldToView(rightMarker);
+
+ const distanceBetweenMarkers = interestRange.range;
+ const distanceBetweenMarkersView =
+ dt.xWorldVectorToView(distanceBetweenMarkers);
+ const positionInMiddleOfMarkersView =
+ leftMarkerView + (distanceBetweenMarkersView / 2);
+
+ const textToDraw = vp.majorMarkUnit.format(distanceBetweenMarkers);
+ const textWidthView = ctx.measureText(textToDraw).width;
+ const spaceForArrowsAndTextView =
+ textWidthView + spaceForArrowsView + arrowSpacing;
+
+ // Set text positions.
+ let textLeftView = positionInMiddleOfMarkersView - textWidthView / 2;
+ const textRightView = textLeftView + textWidthView;
+
+ if (spaceForArrowsAndTextView > distanceBetweenMarkersView) {
+ // Print the display distance text right of the 2 markers.
+ textLeftView = rightMarkerView + 2 * arrowSpacing;
+
+ // Put text to the left in case it gets cut off.
+ if (textLeftView + textWidthView > width) {
+ textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView;
+ }
+
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+
+ // Draw the arrows pointing from outside in and a line in between.
+ ctx.strokeStyle = arrowColor;
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView,
+ arrowPosY);
+ ctx.stroke();
+
+ ctx.fillStyle = arrowColor;
+ tr.ui.b.drawArrow(ctx,
+ leftMarkerView - 1.5 * arrowSpacing, arrowPosY,
+ leftMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ tr.ui.b.drawArrow(ctx,
+ rightMarkerView + 1.5 * arrowSpacing, arrowPosY,
+ rightMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ } else if (spaceForArrowsView <= distanceBetweenMarkersView) {
+ let leftArrowStart;
+ let rightArrowStart;
+ if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
+ // Print the display distance text.
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+
+ leftArrowStart = textLeftView - arrowSpacing;
+ rightArrowStart = textRightView + arrowSpacing;
+ } else {
+ leftArrowStart = positionInMiddleOfMarkersView;
+ rightArrowStart = positionInMiddleOfMarkersView;
+ }
+
+ // Draw the arrows pointing inside out.
+ ctx.strokeStyle = arrowColor;
+ ctx.fillStyle = arrowColor;
+ tr.ui.b.drawArrow(ctx,
+ leftArrowStart, arrowPosY,
+ leftMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ tr.ui.b.drawArrow(ctx,
+ rightArrowStart, arrowPosY,
+ rightMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ }
+
+ ctx.restore();
+ },
+
+ drawMarkers_(viewLWorld, viewRWorld) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const trackBounds = this.getBoundingClientRect();
+ const viewHeight = trackBounds.height * pixelRatio;
+
+ if (!this.viewport.interestRange.isEmpty) {
+ this.viewport.interestRange.draw(this.context(),
+ viewLWorld, viewRWorld, viewHeight);
+ }
+ },
+
+ /**
+ * Adds items intersecting the given range to a selection.
+ * @param {number} loVX Lower X bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVX Upper X bound of the interval to search, in
+ * viewspace.
+ * @param {number} loVY Lower Y bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVY Upper Y bound of the interval to search, in
+ * viewspace.
+ * @param {Selection} selection Selection to which to add results.
+ */
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection) {
+ // Does nothing. There's nothing interesting to pick on the xAxis
+ // track.
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ }
+ };
+
+ return {
+ XAxisTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html
new file mode 100644
index 00000000000..459c05cd122
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/x_axis_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_interestRange', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.interestRange.min = 300;
+ viewport.interestRange.max = 300;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_singlePointInterestRange', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.interestRange.min = 300;
+ viewport.interestRange.max = 400;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ function testTimeMode(mode, testInstance, numDigits, opt_unit) {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.timeMode = mode;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const trackContext = drawingContainer.ctx_;
+ const oldFillText = trackContext.fillText;
+ const fillTextText = [];
+ const fillTextThis = [];
+ trackContext.fillText = function(text, xPos, yPos) {
+ fillTextText.push(text);
+ fillTextThis.push(this);
+ return oldFillText.call(this, text, xPos, yPos);
+ };
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ testInstance.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ const formatter =
+ new Intl.NumberFormat(undefined, { numDigits, numDigits });
+ const formatFunction = function(value) {
+ let valueString = value.toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: numDigits
+ });
+ if (opt_unit) valueString += opt_unit;
+ return valueString;
+ };
+ const expectedText = viewport.majorMarkWorldPositions.map(
+ formatFunction);
+ assert.strictEqual(fillTextText.length, fillTextThis.length);
+ for (let i = 0; i < fillTextText.length; i++) {
+ assert.deepEqual(fillTextText[i], expectedText[i]);
+ assert.strictEqual(fillTextThis[i], trackContext);
+ }
+ }
+
+ test('instantiate_timeModeMs', function() {
+ testTimeMode(tr.ui.TimelineViewport.TimeMode.TIME_IN_MS,
+ this, 3, ' ms');
+ });
+
+ test('instantiate_timeModeRevisions', function() {
+ testTimeMode(tr.ui.TimelineViewport.TimeMode.REVISIONS, this, 0);
+ });
+});
+</script>
+