diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/ui/tracks')
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> + |