// 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.
/**
* @fileoverview Renders an array of slices into the provided div,
* using a child canvas element. Uses a FastRectRenderer to draw only
* the visible slices.
*/
cr.define('tracing', function() {
var pallette = tracing.getPallette();
var highlightIdBoost = tracing.getPalletteHighlightIdBoost();
// TODO(jrg): possibly obsoleted with the elided string cache.
// Consider removing.
var textWidthMap = { };
function quickMeasureText(ctx, text) {
var w = textWidthMap[text];
if (!w) {
w = ctx.measureText(text).width;
textWidthMap[text] = w;
}
return w;
}
/**
* Cache for elided strings.
* Moved from the ElidedTitleCache protoype to a "global" for speed
* (variable reference is 100x faster).
* key: String we wish to elide.
* value: Another dict whose key is width
* and value is an ElidedStringWidthPair.
*/
var elidedTitleCacheDict = {};
/**
* A generic track that contains other tracks as its children.
* @constructor
*/
var TimelineContainerTrack = cr.ui.define('div');
TimelineContainerTrack.prototype = {
__proto__: HTMLDivElement.prototype,
decorate: function() {
this.tracks_ = [];
},
detach: function() {
for (var i = 0; i < this.tracks_.length; i++)
this.tracks_[i].detach();
},
get viewport() {
return this.viewport_;
},
set viewport(v) {
this.viewport_ = v;
for (var i = 0; i < this.tracks_.length; i++)
this.tracks_[i].viewport = v;
this.updateChildTracks_();
},
get firstCanvas() {
if (this.tracks_.length)
return this.tracks_[0].firstCanvas;
return undefined;
},
/**
* Adds items intersecting a point to a selection.
* @param {number} wX X location to search at, in worldspace.
* @param {number} wY Y location to search at, in offset space.
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
* @return {boolean} true if a slice was found, otherwise false.
*/
addIntersectingItemsToSelection: function(wX, wY, selection) {
for (var i = 0; i < this.tracks_.length; i++) {
var trackClientRect = this.tracks_[i].getBoundingClientRect();
if (wY >= trackClientRect.top && wY < trackClientRect.bottom)
this.tracks_[i].addIntersectingItemsToSelection(wX, wY, selection);
}
return false;
},
/**
* Adds items intersecting the given range to a selection.
* @param {number} loWX Lower X bound of the interval to search, in
* worldspace.
* @param {number} hiWX Upper X bound of the interval to search, in
* worldspace.
* @param {number} loY Lower Y bound of the interval to search, in
* offset space.
* @param {number} hiY Upper Y bound of the interval to search, in
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
*/
addIntersectingItemsInRangeToSelection: function(
loWX, hiWX, loY, hiY, selection) {
for (var i = 0; i < this.tracks_.length; i++) {
var trackClientRect = this.tracks_[i].getBoundingClientRect();
var a = Math.max(loY, trackClientRect.top);
var b = Math.min(hiY, trackClientRect.bottom);
if (a <= b)
this.tracks_[i].addIntersectingItemsInRangeToSelection(
loWX, hiWX, loY, hiY, selection);
}
},
addAllObjectsMatchingFilterToSelection: function(filter, selection) {
for (var i = 0; i < this.tracks_.length; i++)
this.tracks_[i].addAllObjectsMatchingFilterToSelection(
filter, selection);
}
};
function addControlButtonElements(el, canCollapse) {
var closeEl = document.createElement('div');
closeEl.classList.add('timeline-track-button');
closeEl.classList.add('timeline-track-close-button');
closeEl.textContent = String.fromCharCode(215); // ×
closeEl.addEventListener('click', function() {
el.style.display = 'None';
});
el.appendChild(closeEl);
if (canCollapse) {
var collapseEl = document.createElement('div');
collapseEl.classList.add('timeline-track-button');
collapseEl.classList.add('timeline-track-collapse-button');
var minus = '\u2212'; // minus sign;
var plus = '\u002b'; // plus sign;
collapseEl.textContent = minus;
var collapsed = false;
collapseEl.addEventListener('click', function() {
collapsed = !collapsed;
el.collapsedDidChange(collapsed);
collapseEl.textContent = collapsed ? plus : minus;
});
el.appendChild(collapseEl);
}
}
/**
* Visualizes a TimelineThread using a series of of TimelineSliceTracks.
* @constructor
*/
var TimelineThreadTrack = cr.ui.define(TimelineContainerTrack);
TimelineThreadTrack.prototype = {
__proto__: TimelineContainerTrack.prototype,
decorate: function() {
this.classList.add('timeline-thread-track');
},
get thread() {
return this.thread_;
},
set thread(thread) {
this.thread_ = thread;
this.updateChildTracks_();
},
get tooltip() {
return this.tooltip_;
},
set tooltip(value) {
this.tooltip_ = value;
this.updateChildTracks_();
},
get heading() {
return this.heading_;
},
set heading(h) {
this.heading_ = h;
this.updateChildTracks_();
},
get headingWidth() {
return this.headingWidth_;
},
set headingWidth(width) {
this.headingWidth_ = width;
this.updateChildTracks_();
},
addTrack_: function(slices) {
var track = new TimelineSliceTrack();
track.heading = '';
track.slices = slices;
track.headingWidth = this.headingWidth_;
track.viewport = this.viewport_;
this.tracks_.push(track);
this.appendChild(track);
return track;
},
updateChildTracks_: function() {
this.detach();
this.textContent = '';
this.tracks_ = [];
if (this.thread_) {
if (this.thread_.cpuSlices) {
var track = this.addTrack_(this.thread_.cpuSlices);
track.height = '4px';
track.decorateHit = function(hit) {
hit.thread = this.thread_;
}
}
if (this.thread_.asyncSlices.length) {
var subRows = this.thread_.asyncSlices.subRows;
for (var srI = 0; srI < subRows.length; srI++) {
var track = this.addTrack_(subRows[srI]);
track.decorateHit = function(hit) {
// TODO(simonjam): figure out how to associate subSlice hits back
// to their parent slice.
}
track.asyncStyle = true;
}
}
for (var srI = 0; srI < this.thread_.subRows.length; srI++) {
var track = this.addTrack_(this.thread_.subRows[srI]);
track.decorateHit = function(hit) {
hit.thread = this.thread_;
}
}
if (this.tracks_.length > 0) {
if (this.thread_.cpuSlices) {
this.tracks_[1].heading = this.heading_;
this.tracks_[1].tooltip = this.tooltip_;
} else {
this.tracks_[0].heading = this.heading_;
this.tracks_[0].tooltip = this.tooltip_;
}
}
}
addControlButtonElements(this, this.tracks_.length >= 4);
},
collapsedDidChange: function(collapsed) {
if (collapsed) {
var h = parseInt(this.tracks_[0].height);
for (var 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 (var i = 0; i < this.tracks_.length; ++i) {
this.tracks_[i].height = this.tracks_[0].height;
this.tracks_[i].style.display = '';
}
}
}
};
/**
* Visualizes a TimelineCpu using a series of of TimelineSliceTracks.
* @constructor
*/
var TimelineCpuTrack = cr.ui.define(TimelineContainerTrack);
TimelineCpuTrack.prototype = {
__proto__: TimelineContainerTrack.prototype,
decorate: function() {
this.classList.add('timeline-thread-track');
},
get cpu() {
return this.cpu_;
},
set cpu(cpu) {
this.cpu_ = cpu;
this.updateChildTracks_();
},
get tooltip() {
return this.tooltip_;
},
set tooltip(value) {
this.tooltip_ = value;
this.updateChildTracks_();
},
get heading() {
return this.heading_;
},
set heading(h) {
this.heading_ = h;
this.updateChildTracks_();
},
get headingWidth() {
return this.headingWidth_;
},
set headingWidth(width) {
this.headingWidth_ = width;
this.updateChildTracks_();
},
updateChildTracks_: function() {
this.detach();
this.textContent = '';
this.tracks_ = [];
if (this.cpu_) {
var track = new TimelineSliceTrack();
track.slices = this.cpu_.slices;
track.headingWidth = this.headingWidth_;
track.viewport = this.viewport_;
this.tracks_.push(track);
this.appendChild(track);
this.tracks_[0].heading = this.heading_;
this.tracks_[0].tooltip = this.tooltip_;
}
addControlButtonElements(this, false);
}
};
/**
* A canvas-based track constructed. Provides the basic heading and
* invalidation-managment infrastructure. Subclasses must implement drawing
* and picking code.
* @constructor
* @extends {HTMLDivElement}
*/
var CanvasBasedTrack = cr.ui.define('div');
CanvasBasedTrack.prototype = {
__proto__: HTMLDivElement.prototype,
decorate: function() {
this.className = 'timeline-canvas-based-track';
this.slices_ = null;
this.headingDiv_ = document.createElement('div');
this.headingDiv_.className = 'timeline-canvas-based-track-title';
this.headingDiv_.onselectstart = function() { return false; };
this.appendChild(this.headingDiv_);
this.canvasContainer_ = document.createElement('div');
this.canvasContainer_.className =
'timeline-canvas-based-track-canvas-container';
this.appendChild(this.canvasContainer_);
this.canvas_ = document.createElement('canvas');
this.canvas_.className = 'timeline-canvas-based-track-canvas';
this.canvasContainer_.appendChild(this.canvas_);
this.ctx_ = this.canvas_.getContext('2d');
},
detach: function() {
if (this.viewport_)
this.viewport_.removeEventListener('change',
this.viewportChangeBoundToThis_);
},
set headingWidth(width) {
this.headingDiv_.style.width = width;
},
get heading() {
return this.headingDiv_.textContent;
},
set heading(text) {
this.headingDiv_.textContent = text;
},
set tooltip(text) {
this.headingDiv_.title = text;
},
get viewport() {
return this.viewport_;
},
set viewport(v) {
this.viewport_ = v;
if (this.viewport_)
this.viewport_.removeEventListener('change',
this.viewportChangeBoundToThis_);
this.viewport_ = v;
if (this.viewport_) {
this.viewportChangeBoundToThis_ = this.viewportChange_.bind(this);
this.viewport_.addEventListener('change',
this.viewportChangeBoundToThis_);
}
this.invalidate();
},
viewportChange_: function() {
this.invalidate();
},
invalidate: function() {
if (this.rafPending_)
return;
webkitRequestAnimationFrame(function() {
this.rafPending_ = false;
if (!this.viewport_)
return;
var style = window.getComputedStyle(this.canvasContainer_);
var style_width = parseInt(style.width);
var style_height = parseInt(style.height);
if (this.canvas_.width != style_width)
this.canvas_.width = style_width;
if (this.canvas_.height != style_height)
this.canvas_.height = style_height;
this.redraw();
}.bind(this), this);
this.rafPending_ = true;
},
get firstCanvas() {
return this.canvas_;
}
};
/**
* A pair representing an elided string and world-coordinate width
* to draw it.
* @constructor
*/
function ElidedStringWidthPair(string, width) {
this.string = string;
this.width = width;
}
/**
* A cache for elided strings.
* @constructor
*/
function ElidedTitleCache() {
}
ElidedTitleCache.prototype = {
/**
* Return elided text.
* @param {track} A timeline slice track or other object that defines
* functions labelWidth() and labelWidthWorld().
* @param {pixWidth} Pixel width.
* @param {title} Original title text.
* @param {width} Drawn width in world coords.
* @param {sliceDuration} Where the title must fit (in world coords).
* @return {ElidedStringWidthPair} Elided string and width.
*/
get: function(track, pixWidth, title, width, sliceDuration) {
var elidedDict = elidedTitleCacheDict[title];
if (!elidedDict) {
elidedDict = {};
elidedTitleCacheDict[title] = elidedDict;
}
var elidedDictForPixWidth = elidedDict[pixWidth];
if (!elidedDictForPixWidth) {
elidedDict[pixWidth] = {};
elidedDictForPixWidth = elidedDict[pixWidth];
}
var stringWidthPair = elidedDictForPixWidth[sliceDuration];
if (stringWidthPair === undefined) {
var newtitle = title;
var elided = false;
while (track.labelWidthWorld(newtitle, pixWidth) > sliceDuration) {
newtitle = newtitle.substring(0, newtitle.length * 0.75);
elided = true;
}
if (elided && newtitle.length > 3)
newtitle = newtitle.substring(0, newtitle.length - 3) + '...';
stringWidthPair = new ElidedStringWidthPair(
newtitle,
track.labelWidth(newtitle));
elidedDictForPixWidth[sliceDuration] = stringWidthPair;
}
return stringWidthPair;
}
};
/**
* A track that displays an array of TimelineSlice objects.
* @constructor
* @extends {CanvasBasedTrack}
*/
var TimelineSliceTrack = cr.ui.define(CanvasBasedTrack);
TimelineSliceTrack.prototype = {
__proto__: CanvasBasedTrack.prototype,
/**
* Should we elide text on trace labels?
* Without eliding, text that is too wide isn't drawn at all.
* Disable if you feel this causes a performance problem.
* This is a default value that can be overridden in tracks for testing.
* @const
*/
SHOULD_ELIDE_TEXT: true,
decorate: function() {
this.classList.add('timeline-slice-track');
this.elidedTitleCache = new ElidedTitleCache();
this.asyncStyle_ = false;
},
/**
* Called by all the addToSelection functions on the created selection
* hit objects. Override this function on parent classes to add
* context-specific information to the hit.
*/
decorateHit: function(hit) {
},
get asyncStyle() {
return this.asyncStyle_;
},
set asyncStyle(v) {
this.asyncStyle_ = !!v;
this.invalidate();
},
get slices() {
return this.slices_;
},
set slices(slices) {
this.slices_ = slices;
this.invalidate();
},
get height() {
return window.getComputedStyle(this).height;
},
set height(height) {
this.style.height = height;
this.invalidate();
},
labelWidth: function(title) {
return quickMeasureText(this.ctx_, title) + 2;
},
labelWidthWorld: function(title, pixWidth) {
return this.labelWidth(title) * pixWidth;
},
redraw: function() {
var ctx = this.ctx_;
var canvasW = this.canvas_.width;
var canvasH = this.canvas_.height;
ctx.clearRect(0, 0, canvasW, canvasH);
// Culling parameters.
var vp = this.viewport_;
var pixWidth = vp.xViewVectorToWorld(1);
var viewLWorld = vp.xViewToWorld(0);
var viewRWorld = vp.xViewToWorld(canvasW);
// Draw grid without a transform because the scale
// affects line width.
if (vp.gridEnabled) {
var x = vp.gridTimebase;
ctx.beginPath();
while (x < viewRWorld) {
if (x >= viewLWorld) {
// Do conversion to viewspace here rather than on
// x to avoid precision issues.
var vx = vp.xWorldToView(x);
ctx.moveTo(vx, 0);
ctx.lineTo(vx, canvasH);
}
x += vp.gridStep;
}
ctx.strokeStyle = 'rgba(255,0,0,0.25)';
ctx.stroke();
}
// Begin rendering in world space.
ctx.save();
vp.applyTransformToCanavs(ctx);
// Slices.
if (this.asyncStyle_)
ctx.globalAlpha = 0.25;
var tr = new tracing.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth,
2 * pixWidth, viewRWorld, pallette);
tr.setYandH(0, canvasH);
var slices = this.slices_;
for (var i = 0; i < slices.length; ++i) {
var slice = slices[i];
var x = slice.start;
// Less than 0.001 causes short events to disappear when zoomed in.
var w = Math.max(slice.duration, 0.001);
var colorId = slice.selected ?
slice.colorId + highlightIdBoost :
slice.colorId;
if (w < pixWidth)
w = pixWidth;
if (slice.duration > 0) {
tr.fillRect(x, w, colorId);
} else {
// Instant: draw a triangle. If zoomed too far, collapse
// into the FastRectRenderer.
if (pixWidth > 0.001) {
tr.fillRect(x, pixWidth, colorId);
} else {
ctx.fillStyle = pallette[colorId];
ctx.beginPath();
ctx.moveTo(x - (4 * pixWidth), canvasH);
ctx.lineTo(x, 0);
ctx.lineTo(x + (4 * pixWidth), canvasH);
ctx.closePath();
ctx.fill();
}
}
}
tr.flush();
ctx.restore();
// Labels.
if (canvasH > 8) {
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.font = '10px sans-serif';
ctx.strokeStyle = 'rgb(0,0,0)';
ctx.fillStyle = 'rgb(0,0,0)';
// Don't render text until until it is 20px wide
var quickDiscardThresshold = pixWidth * 20;
var shouldElide = this.SHOULD_ELIDE_TEXT;
for (var i = 0; i < slices.length; ++i) {
var slice = slices[i];
if (slice.duration > quickDiscardThresshold) {
var title = slice.title;
if (slice.didNotFinish) {
title += ' (Did Not Finish)';
}
var drawnTitle = title;
var drawnWidth = this.labelWidth(drawnTitle);
if (shouldElide &&
this.labelWidthWorld(drawnTitle, pixWidth) > slice.duration) {
var elidedValues = this.elidedTitleCache.get(
this, pixWidth,
drawnTitle, drawnWidth,
slice.duration);
drawnTitle = elidedValues.string;
drawnWidth = elidedValues.width;
}
if (drawnWidth * pixWidth < slice.duration) {
var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration);
ctx.fillText(drawnTitle, cX, 2.5, drawnWidth);
}
}
}
}
},
/**
* Finds slices intersecting the given interval.
* @param {number} wX X location to search at, in worldspace.
* @param {number} wY Y location to search at, in offset space.
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
* @return {boolean} true if a slice was found, otherwise false.
*/
addIntersectingItemsToSelection: function(wX, wY, selection) {
var clientRect = this.getBoundingClientRect();
if (wY < clientRect.top || wY >= clientRect.bottom)
return false;
var x = tracing.findLowIndexInSortedIntervals(this.slices_,
function(x) { return x.start; },
function(x) { return x.duration; },
wX);
if (x >= 0 && x < this.slices_.length) {
var hit = selection.addSlice(this, this.slices_[x]);
this.decorateHit(hit);
return true;
}
return false;
},
/**
* Adds items intersecting the given range to a selection.
* @param {number} loWX Lower X bound of the interval to search, in
* worldspace.
* @param {number} hiWX Upper X bound of the interval to search, in
* worldspace.
* @param {number} loY Lower Y bound of the interval to search, in
* offset space.
* @param {number} hiY Upper Y bound of the interval to search, in
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
*/
addIntersectingItemsInRangeToSelection: function(
loWX, hiWX, loY, hiY, selection) {
var clientRect = this.getBoundingClientRect();
var a = Math.max(loY, clientRect.top);
var b = Math.min(hiY, clientRect.bottom);
if (a > b)
return;
var that = this;
function onPickHit(slice) {
var hit = selection.addSlice(that, slice);
that.decorateHit(hit);
}
tracing.iterateOverIntersectingIntervals(this.slices_,
function(x) { return x.start; },
function(x) { return x.duration; },
loWX, hiWX,
onPickHit);
},
/**
* Find the index for the given slice.
* @return {index} Index of the given slice, or undefined.
* @private
*/
indexOfSlice_: function(slice) {
var index = tracing.findLowIndexInSortedArray(this.slices_,
function(x) { return x.start; },
slice.start);
while (index < this.slices_.length &&
slice.start == this.slices_[index].start &&
slice.colorId != this.slices_[index].colorId) {
index++;
}
return index < this.slices_.length ? index : undefined;
},
/**
* Add the item to the left or right of the provided hit, if any, to the
* selection.
* @param {slice} The current slice.
* @param {Number} offset Number of slices away from the hit to look.
* @param {TimelineSelection} selection The selection to add a hit to,
* if found.
* @return {boolean} Whether a hit was found.
* @private
*/
addItemNearToProvidedHitToSelection: function(hit, offset, selection) {
if (!hit.slice)
return false;
var index = this.indexOfSlice_(hit.slice);
if (index === undefined)
return false;
var newIndex = index + offset;
if (newIndex < 0 || newIndex >= this.slices_.length)
return false;
var hit = selection.addSlice(this, this.slices_[newIndex]);
this.decorateHit(hit);
return true;
},
addAllObjectsMatchingFilterToSelection: function(filter, selection) {
for (var i = 0; i < this.slices_.length; ++i) {
if (filter.matchSlice(this.slices_[i])) {
var hit = selection.addSlice(this, this.slices_[i]);
this.decorateHit(hit);
}
}
}
};
/**
* A track that displays the viewport size and scale.
* @constructor
* @extends {CanvasBasedTrack}
*/
var TimelineViewportTrack = cr.ui.define(CanvasBasedTrack);
var logOf10 = Math.log(10);
function log10(x) {
return Math.log(x) / logOf10;
}
TimelineViewportTrack.prototype = {
__proto__: CanvasBasedTrack.prototype,
decorate: function() {
this.classList.add('timeline-viewport-track');
this.strings_secs_ = [];
this.strings_msecs_ = [];
},
redraw: function() {
var ctx = this.ctx_;
var canvasW = this.canvas_.width;
var canvasH = this.canvas_.height;
ctx.clearRect(0, 0, canvasW, canvasH);
// Culling parametrs.
var vp = this.viewport_;
var pixWidth = vp.xViewVectorToWorld(1);
var viewLWorld = vp.xViewToWorld(0);
var viewRWorld = vp.xViewToWorld(canvasW);
var idealMajorMarkDistancePix = 150;
var idealMajorMarkDistanceWorld =
vp.xViewVectorToWorld(idealMajorMarkDistancePix);
// The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc
var conservativeGuess =
Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld)));
// Once we have a conservative guess, consider things that evenly add up
// to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still
// exceeds the ideal mark distance.
var divisors = [10, 5, 2, 1];
for (var i = 0; i < divisors.length; ++i) {
var tightenedGuess = conservativeGuess / divisors[i];
if (vp.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix)
continue;
majorMarkDistanceWorld = conservativeGuess / divisors[i - 1];
break;
}
var tickLabels = undefined;
if (majorMarkDistanceWorld < 100) {
unit = 'ms';
unitDivisor = 1;
tickLabels = this.strings_msecs_;
} else {
unit = 's';
unitDivisor = 1000;
tickLabels = this.strings_secs_;
}
var numTicksPerMajor = 5;
var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
var minorMarkDistancePx = vp.xWorldVectorToView(minorMarkDistanceWorld);
var firstMajorMark =
Math.floor(viewLWorld / majorMarkDistanceWorld) *
majorMarkDistanceWorld;
var minorTickH = Math.floor(canvasH * 0.25);
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.strokeStyle = 'rgb(0, 0, 0)';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.font = '9px sans-serif';
// Each iteration of this loop draws one major mark
// and numTicksPerMajor minor ticks.
//
// Rendering can't be done in world space because canvas transforms
// affect line width. So, do the conversions manually.
for (var curX = firstMajorMark;
curX < viewRWorld;
curX += majorMarkDistanceWorld) {
var curXView = Math.floor(vp.xWorldToView(curX));
var unitValue = curX / unitDivisor;
var roundedUnitValue = Math.floor(unitValue * 100000) / 100000;
if (!tickLabels[roundedUnitValue])
tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit;
ctx.fillText(tickLabels[roundedUnitValue], curXView + 2, 0);
ctx.beginPath();
// Major mark
ctx.moveTo(curXView, 0);
ctx.lineTo(curXView, canvasW);
// Minor marks
for (var i = 1; i < numTicksPerMajor; ++i) {
var xView = Math.floor(curXView + minorMarkDistancePx * i);
ctx.moveTo(xView, canvasH - minorTickH);
ctx.lineTo(xView, canvasH);
}
ctx.stroke();
}
},
/**
* Adds items intersecting a point to a selection.
* @param {number} wX X location to search at, in worldspace.
* @param {number} wY Y location to search at, in offset space.
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
* @return {boolean} true if a slice was found, otherwise false.
*/
addIntersectingItemsToSelection: function(wX, wY, selection) {
// Does nothing. There's nothing interesting to pick on the viewport
// track.
},
/**
* Adds items intersecting the given range to a selection.
* @param {number} loWX Lower X bound of the interval to search, in
* worldspace.
* @param {number} hiWX Upper X bound of the interval to search, in
* worldspace.
* @param {number} loY Lower Y bound of the interval to search, in
* offset space.
* @param {number} hiY Upper Y bound of the interval to search, in
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
*/
addIntersectingItemsInRangeToSelection: function(
loWX, hiWX, loY, hiY, selection) {
// Does nothing. There's nothing interesting to pick on the viewport
// track.
},
addAllObjectsMatchingFilterToSelection: function(filter, selection) {
}
};
/**
* A track that displays a TimelineCounter object.
* @constructor
* @extends {CanvasBasedTrack}
*/
var TimelineCounterTrack = cr.ui.define(CanvasBasedTrack);
TimelineCounterTrack.prototype = {
__proto__: CanvasBasedTrack.prototype,
decorate: function() {
this.classList.add('timeline-counter-track');
addControlButtonElements(this, false);
this.selectedSamples_ = {};
},
/**
* Called by all the addToSelection functions on the created selection
* hit objects. Override this function on parent classes to add
* context-specific information to the hit.
*/
decorateHit: function(hit) {
},
get counter() {
return this.counter_;
},
set counter(counter) {
this.counter_ = counter;
this.invalidate();
},
/**
* @return {Object} A sparce, mutable map from sample index to bool. Samples
* indices the map that are true are drawn as selected. Callers that mutate
* the map must manually call invalidate on the track to trigger a redraw.
*/
get selectedSamples() {
return this.selectedSamples_;
},
redraw: function() {
var ctr = this.counter_;
var ctx = this.ctx_;
var canvasW = this.canvas_.width;
var canvasH = this.canvas_.height;
ctx.clearRect(0, 0, canvasW, canvasH);
// Culling parametrs.
var vp = this.viewport_;
var pixWidth = vp.xViewVectorToWorld(1);
var viewLWorld = vp.xViewToWorld(0);
var viewRWorld = vp.xViewToWorld(canvasW);
// Drop sampels that are less than skipDistancePix apart.
var skipDistancePix = 1;
var skipDistanceWorld = vp.xViewVectorToWorld(skipDistancePix);
// Begin rendering in world space.
ctx.save();
vp.applyTransformToCanavs(ctx);
// Figure out where drawing should begin.
var numSeries = ctr.numSeries;
var numSamples = ctr.numSamples;
var startIndex = tracing.findLowIndexInSortedArray(ctr.timestamps,
function() {
},
viewLWorld);
// Draw indices one by one until we fall off the viewRWorld.
var yScale = canvasH / ctr.maxTotal;
for (var seriesIndex = ctr.numSeries - 1;
seriesIndex >= 0; seriesIndex--) {
var colorId = ctr.seriesColors[seriesIndex];
ctx.fillStyle = pallette[colorId];
ctx.beginPath();
// Set iLast and xLast such that the first sample we draw is the
// startIndex sample.
var iLast = startIndex - 1;
var xLast = iLast >= 0 ? ctr.timestamps[iLast] - skipDistanceWorld : -1;
var yLastView = canvasH;
// Iterate over samples from iLast onward until we either fall off the
// viewRWorld or we run out of samples. To avoid drawing too much, after
// drawing a sample at xLast, skip subsequent samples that are less than
// skipDistanceWorld from xLast.
var hasMoved = false;
while (true) {
var i = iLast + 1;
if (i >= numSamples) {
ctx.lineTo(xLast, yLastView);
ctx.lineTo(xLast + 8 * pixWidth, yLastView);
ctx.lineTo(xLast + 8 * pixWidth, canvasH);
break;
}
var x = ctr.timestamps[i];
var y = ctr.totals[i * numSeries + seriesIndex];
var yView = canvasH - (yScale * y);
if (x > viewRWorld) {
ctx.lineTo(x, yLastView);
ctx.lineTo(x, canvasH);
break;
}
if (x - xLast < skipDistanceWorld) {
iLast = i;
continue;
}
if (!hasMoved) {
ctx.moveTo(viewLWorld, canvasH);
hasMoved = true;
}
ctx.lineTo(x, yLastView);
ctx.lineTo(x, yView);
iLast = i;
xLast = x;
yLastView = yView;
}
ctx.closePath();
ctx.fill();
}
ctx.fillStyle = 'rgba(255, 0, 0, 1)';
for (var i in this.selectedSamples_) {
if (!this.selectedSamples_[i])
continue;
var x = ctr.timestamps[i];
for (var seriesIndex = ctr.numSeries - 1;
seriesIndex >= 0; seriesIndex--) {
var y = ctr.totals[i * numSeries + seriesIndex];
var yView = canvasH - (yScale * y);
ctx.fillRect(x - pixWidth, yView - 1, 3 * pixWidth, 3);
}
}
ctx.restore();
},
/**
* Adds items intersecting a point to a selection.
* @param {number} wX X location to search at, in worldspace.
* @param {number} wY Y location to search at, in offset space.
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
* @return {boolean} true if a slice was found, otherwise false.
*/
addIntersectingItemsToSelection: function(wX, wY, selection) {
var clientRect = this.getBoundingClientRect();
if (wY < clientRect.top || wY >= clientRect.bottom)
return false;
var ctr = this.counter_;
if (wX < this.counter_.timestamps[0])
return false;
var i = tracing.findLowIndexInSortedArray(ctr.timestamps,
function(x) { return x; },
wX);
if (i < 0 || i >= ctr.timestamps.length)
return false;
// Sample i is going to either be exactly at wX or slightly above it,
// E.g. asking for 7.5 in [7,8] gives i=1. So bump i back by 1 if needed.
if (i > 0 && wX > this.counter_.timestamps[i - 1])
i--;
// Some preliminaries.
var canvasH = this.getBoundingClientRect().height;
var yScale = canvasH / ctr.maxTotal;
/*
// Figure out which sample we hit
var seriesIndexHit;
for (var seriesIndex = 0; seriesIndex < ctr.numSeries; seriesIndex++) {
var y = ctr.totals[i * ctr.numSeries + seriesIndex];
var yView = canvasH - (yScale * y) + clientRect.top;
if (wY >= yView) {
seriesIndexHit = seriesIndex;
break;
}
}
if (seriesIndexHit === undefined)
return false;
*/
var hit = selection.addCounterSample(this, this.counter, i);
this.decorateHit(hit);
return true;
},
/**
* Adds items intersecting the given range to a selection.
* @param {number} loWX Lower X bound of the interval to search, in
* worldspace.
* @param {number} hiWX Upper X bound of the interval to search, in
* worldspace.
* @param {number} loY Lower Y bound of the interval to search, in
* offset space.
* @param {number} hiY Upper Y bound of the interval to search, in
* offset space.
* @param {TimelineSelection} selection Selection to which to add hits.
*/
addIntersectingItemsInRangeToSelection: function(
loWX, hiWX, loY, hiY, selection) {
var clientRect = this.getBoundingClientRect();
var a = Math.max(loY, clientRect.top);
var b = Math.min(hiY, clientRect.bottom);
if (a > b)
return;
var ctr = this.counter_;
var iLo = tracing.findLowIndexInSortedArray(ctr.timestamps,
function(x) { return x; },
loWX);
var iHi = tracing.findLowIndexInSortedArray(ctr.timestamps,
function(x) { return x; },
hiWX);
// Sample i is going to either be exactly at wX or slightly above it,
// E.g. asking for 7.5 in [7,8] gives i=1. So bump i back by 1 if needed.
if (iLo > 0 && loWX > ctr.timestamps[iLo - 1])
iLo--;
if (iHi > 0 && hiWX > ctr.timestamps[iHi - 1])
iHi--;
// Iterate over every sample intersecting..
for (var i = iLo; i <= iHi; i++) {
if (i >= ctr.timestamps.length)
continue;
// TODO(nduca): Pick the seriesIndexHit based on the loY - hiY values.
var hit = selection.addCounterSample(this, this.counter, i);
this.decorateHit(hit);
}
},
addAllObjectsMatchingFilterToSelection: function(filter, selection) {
}
};
return {
TimelineCounterTrack: TimelineCounterTrack,
TimelineSliceTrack: TimelineSliceTrack,
TimelineThreadTrack: TimelineThreadTrack,
TimelineViewportTrack: TimelineViewportTrack,
TimelineCpuTrack: TimelineCpuTrack
};
});