| // 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 |
| }; |
| }); |