1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7base.requireStylesheet('tracks.slice_track'); 8 9base.require('tracks.canvas_based_track'); 10base.require('sorted_array_utils'); 11base.require('fast_rect_renderer'); 12base.require('color_scheme'); 13base.require('ui'); 14 15base.exportTo('tracing.tracks', function() { 16 17 var palette = tracing.getColorPalette(); 18 19 /** 20 * A track that displays an array of Slice objects. 21 * @constructor 22 * @extends {CanvasBasedTrack} 23 */ 24 25 var SliceTrack = tracing.ui.define(tracing.tracks.CanvasBasedTrack); 26 27 SliceTrack.prototype = { 28 29 __proto__: tracing.tracks.CanvasBasedTrack.prototype, 30 31 /** 32 * Should we elide text on trace labels? 33 * Without eliding, text that is too wide isn't drawn at all. 34 * Disable if you feel this causes a performance problem. 35 * This is a default value that can be overridden in tracks for testing. 36 * @const 37 */ 38 SHOULD_ELIDE_TEXT: true, 39 40 decorate: function() { 41 this.classList.add('slice-track'); 42 this.elidedTitleCache = new ElidedTitleCache(); 43 this.asyncStyle_ = false; 44 }, 45 46 /** 47 * Called by all the addToSelection functions on the created selection 48 * hit objects. Override this function on parent classes to add 49 * context-specific information to the hit. 50 */ 51 decorateHit: function(hit) { 52 }, 53 54 get asyncStyle() { 55 return this.asyncStyle_; 56 }, 57 58 set asyncStyle(v) { 59 this.asyncStyle_ = !!v; 60 this.invalidate(); 61 }, 62 63 get slices() { 64 return this.slices_; 65 }, 66 67 set slices(slices) { 68 this.slices_ = slices || []; 69 if (!slices) 70 this.visible = false; 71 this.invalidate(); 72 }, 73 74 get height() { 75 return window.getComputedStyle(this).height; 76 }, 77 78 set height(height) { 79 this.style.height = height; 80 this.invalidate(); 81 }, 82 83 labelWidth: function(title) { 84 return quickMeasureText(this.ctx_, title) + 2; 85 }, 86 87 labelWidthWorld: function(title, pixWidth) { 88 return this.labelWidth(title) * pixWidth; 89 }, 90 91 redraw: function() { 92 var ctx = this.ctx_; 93 var canvasW = this.canvas_.width; 94 var canvasH = this.canvas_.height; 95 96 ctx.clearRect(0, 0, canvasW, canvasH); 97 98 // Culling parameters. 99 var vp = this.viewport_; 100 var pixWidth = vp.xViewVectorToWorld(1); 101 var viewLWorld = vp.xViewToWorld(0); 102 var viewRWorld = vp.xViewToWorld(canvasW); 103 104 // Give the viewport a chance to draw onto this canvas. 105 vp.drawUnderContent(ctx, viewLWorld, viewRWorld, canvasH); 106 107 // Begin rendering in world space. 108 ctx.save(); 109 vp.applyTransformToCanvas(ctx); 110 111 // Slices. 112 if (this.asyncStyle_) 113 ctx.globalAlpha = 0.25; 114 var tr = new tracing.FastRectRenderer(ctx, 2 * pixWidth, 2 * pixWidth, 115 palette); 116 tr.setYandH(0, canvasH); 117 var slices = this.slices_; 118 var lowSlice = tracing.findLowIndexInSortedArray(slices, 119 function(slice) { 120 return slice.start + 121 slice.duration; 122 }, 123 viewLWorld); 124 for (var i = lowSlice; i < slices.length; ++i) { 125 var slice = slices[i]; 126 var x = slice.start; 127 if (x > viewRWorld) { 128 break; 129 } 130 // Less than 0.001 causes short events to disappear when zoomed in. 131 var w = Math.max(slice.duration, 0.001); 132 var colorId = slice.selected ? 133 slice.colorId + highlightIdBoost : 134 slice.colorId; 135 136 if (w < pixWidth) 137 w = pixWidth; 138 if (slice.duration > 0) { 139 tr.fillRect(x, w, colorId); 140 } else { 141 // Instant: draw a triangle. If zoomed too far, collapse 142 // into the FastRectRenderer. 143 if (pixWidth > 0.001) { 144 tr.fillRect(x, pixWidth, colorId); 145 } else { 146 ctx.fillStyle = palette[colorId]; 147 ctx.beginPath(); 148 ctx.moveTo(x - (4 * pixWidth), canvasH); 149 ctx.lineTo(x, 0); 150 ctx.lineTo(x + (4 * pixWidth), canvasH); 151 ctx.closePath(); 152 ctx.fill(); 153 } 154 } 155 } 156 tr.flush(); 157 ctx.restore(); 158 159 // Labels. 160 var pixelRatio = window.devicePixelRatio || 1; 161 if (canvasH > 8) { 162 ctx.textAlign = 'center'; 163 ctx.textBaseline = 'top'; 164 ctx.font = (10 * pixelRatio) + 'px sans-serif'; 165 ctx.strokeStyle = 'rgb(0,0,0)'; 166 ctx.fillStyle = 'rgb(0,0,0)'; 167 // Don't render text until until it is 20px wide 168 var quickDiscardThresshold = pixWidth * 20; 169 var shouldElide = this.SHOULD_ELIDE_TEXT; 170 for (var i = lowSlice; i < slices.length; ++i) { 171 var slice = slices[i]; 172 if (slice.start > viewRWorld) { 173 break; 174 } 175 if (slice.duration > quickDiscardThresshold) { 176 var title = slice.title; 177 if (slice.didNotFinish) { 178 title += ' (Did Not Finish)'; 179 } 180 var drawnTitle = title; 181 var drawnWidth = this.labelWidth(drawnTitle); 182 if (shouldElide && 183 this.labelWidthWorld(drawnTitle, pixWidth) > slice.duration) { 184 var elidedValues = this.elidedTitleCache.get( 185 this, pixWidth, 186 drawnTitle, drawnWidth, 187 slice.duration); 188 drawnTitle = elidedValues.string; 189 drawnWidth = elidedValues.width; 190 } 191 if (drawnWidth * pixWidth < slice.duration) { 192 var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); 193 ctx.fillText(drawnTitle, cX, 2.5 * pixelRatio, drawnWidth); 194 } 195 } 196 } 197 } 198 199 // Give the viewport a chance to draw over this canvas. 200 vp.drawOverContent(ctx, viewLWorld, viewRWorld, canvasH); 201 }, 202 203 /** 204 * Finds slices intersecting the given interval. 205 * @param {number} vX X location to search at, in viewspace. 206 * @param {number} vY Y location to search at, in viewspace. 207 * @param {Selection} selection Selection to which to add hits. 208 * @return {boolean} true if a slice was found, otherwise false. 209 */ 210 addIntersectingItemsToSelection: function(vX, vY, selection) { 211 var clientRect = this.getBoundingClientRect(); 212 if (vY < clientRect.top || vY >= clientRect.bottom) 213 return false; 214 var pixelRatio = window.devicePixelRatio || 1; 215 var wX = this.viewport_.xViewVectorToWorld(vX * devicePixelRatio); 216 var x = tracing.findLowIndexInSortedIntervals(this.slices_, 217 function(x) { return x.start; }, 218 function(x) { return x.duration; }, 219 wX); 220 if (x >= 0 && x < this.slices_.length) { 221 var hit = selection.addSlice(this, this.slices_[x]); 222 this.decorateHit(hit); 223 return true; 224 } 225 return false; 226 }, 227 228 /** 229 * Adds items intersecting the given range to a selection. 230 * @param {number} loVX Lower X bound of the interval to search, in 231 * viewspace. 232 * @param {number} hiVX Upper X bound of the interval to search, in 233 * viewspace. 234 * @param {number} loVY Lower Y bound of the interval to search, in 235 * viewspace. 236 * @param {number} hiVY Upper Y bound of the interval to search, in 237 * viewspace. 238 * @param {Selection} selection Selection to which to add hits. 239 */ 240 addIntersectingItemsInRangeToSelection: function( 241 loVX, hiVX, loVY, hiVY, selection) { 242 243 var pixelRatio = window.devicePixelRatio || 1; 244 var loWX = this.viewport_.xViewToWorld(loVX * pixelRatio); 245 var hiWX = this.viewport_.xViewToWorld(hiVX * pixelRatio); 246 247 var clientRect = this.getBoundingClientRect(); 248 var a = Math.max(loVY, clientRect.top); 249 var b = Math.min(hiVY, clientRect.bottom); 250 if (a > b) 251 return; 252 253 var that = this; 254 function onPickHit(slice) { 255 var hit = selection.addSlice(that, slice); 256 that.decorateHit(hit); 257 } 258 tracing.iterateOverIntersectingIntervals(this.slices_, 259 function(x) { return x.start; }, 260 function(x) { return x.duration; }, 261 loWX, hiWX, 262 onPickHit); 263 }, 264 265 /** 266 * Find the index for the given slice. 267 * @return {index} Index of the given slice, or undefined. 268 * @private 269 */ 270 indexOfSlice_: function(slice) { 271 var index = tracing.findLowIndexInSortedArray(this.slices_, 272 function(x) { return x.start; }, 273 slice.start); 274 while (index < this.slices_.length && 275 slice.start == this.slices_[index].start && 276 slice.colorId != this.slices_[index].colorId) { 277 index++; 278 } 279 return index < this.slices_.length ? index : undefined; 280 }, 281 282 /** 283 * Add the item to the left or right of the provided hit, if any, to the 284 * selection. 285 * @param {slice} The current slice. 286 * @param {Number} offset Number of slices away from the hit to look. 287 * @param {Selection} selection The selection to add a hit to, 288 * if found. 289 * @return {boolean} Whether a hit was found. 290 * @private 291 */ 292 addItemNearToProvidedHitToSelection: function(hit, offset, selection) { 293 if (!hit.slice) 294 return false; 295 296 var index = this.indexOfSlice_(hit.slice); 297 if (index === undefined) 298 return false; 299 300 var newIndex = index + offset; 301 if (newIndex < 0 || newIndex >= this.slices_.length) 302 return false; 303 304 var hit = selection.addSlice(this, this.slices_[newIndex]); 305 this.decorateHit(hit); 306 return true; 307 }, 308 309 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 310 for (var i = 0; i < this.slices_.length; ++i) { 311 if (filter.matchSlice(this.slices_[i])) { 312 var hit = selection.addSlice(this, this.slices_[i]); 313 this.decorateHit(hit); 314 } 315 } 316 } 317 }; 318 319 var highlightIdBoost = tracing.getColorPaletteHighlightIdBoost(); 320 321 // TODO(jrg): possibly obsoleted with the elided string cache. 322 // Consider removing. 323 var textWidthMap = { }; 324 function quickMeasureText(ctx, text) { 325 var w = textWidthMap[text]; 326 if (!w) { 327 w = ctx.measureText(text).width; 328 textWidthMap[text] = w; 329 } 330 return w; 331 } 332 333 /** 334 * Cache for elided strings. 335 * Moved from the ElidedTitleCache protoype to a "global" for speed 336 * (variable reference is 100x faster). 337 * key: String we wish to elide. 338 * value: Another dict whose key is width 339 * and value is an ElidedStringWidthPair. 340 */ 341 var elidedTitleCacheDict = {}; 342 343 /** 344 * A cache for elided strings. 345 * @constructor 346 */ 347 function ElidedTitleCache() { 348 } 349 350 ElidedTitleCache.prototype = { 351 /** 352 * Return elided text. 353 * @param {track} A slice track or other object that defines 354 * functions labelWidth() and labelWidthWorld(). 355 * @param {pixWidth} Pixel width. 356 * @param {title} Original title text. 357 * @param {width} Drawn width in world coords. 358 * @param {sliceDuration} Where the title must fit (in world coords). 359 * @return {ElidedStringWidthPair} Elided string and width. 360 */ 361 get: function(track, pixWidth, title, width, sliceDuration) { 362 var elidedDict = elidedTitleCacheDict[title]; 363 if (!elidedDict) { 364 elidedDict = {}; 365 elidedTitleCacheDict[title] = elidedDict; 366 } 367 var elidedDictForPixWidth = elidedDict[pixWidth]; 368 if (!elidedDictForPixWidth) { 369 elidedDict[pixWidth] = {}; 370 elidedDictForPixWidth = elidedDict[pixWidth]; 371 } 372 var stringWidthPair = elidedDictForPixWidth[sliceDuration]; 373 if (stringWidthPair === undefined) { 374 var newtitle = title; 375 var elided = false; 376 while (track.labelWidthWorld(newtitle, pixWidth) > sliceDuration) { 377 newtitle = newtitle.substring(0, newtitle.length * 0.75); 378 elided = true; 379 } 380 if (elided && newtitle.length > 3) 381 newtitle = newtitle.substring(0, newtitle.length - 3) + '...'; 382 stringWidthPair = new ElidedStringWidthPair( 383 newtitle, 384 track.labelWidth(newtitle)); 385 elidedDictForPixWidth[sliceDuration] = stringWidthPair; 386 } 387 return stringWidthPair; 388 } 389 }; 390 391 /** 392 * A pair representing an elided string and world-coordinate width 393 * to draw it. 394 * @constructor 395 */ 396 function ElidedStringWidthPair(string, width) { 397 this.string = string; 398 this.width = width; 399 } 400 401 return { 402 SliceTrack: SliceTrack 403 }; 404}); 405