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 7 base.requireStylesheet('tracing.tracks.slice_track'); 8 9 base.require('base.sorted_array_utils'); 10 base.require('tracing.tracks.heading_track'); 11 base.require('tracing.fast_rect_renderer'); 12 base.require('tracing.color_scheme'); 13 base.require('ui'); 14 15 base.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 {HeadingTrack} 23 */ 24 25 var SliceTrack = ui.define( 26 'slice-track', tracing.tracks.HeadingTrack); 27 28 SliceTrack.prototype = { 29 30 __proto__: tracing.tracks.HeadingTrack.prototype, 31 32 /** 33 * Should we elide text on trace labels? 34 * Without eliding, text that is too wide isn't drawn at all. 35 * Disable if you feel this causes a performance problem. 36 * This is a default value that can be overridden in tracks for testing. 37 * @const 38 */ 39 SHOULD_ELIDE_TEXT: true, 40 41 decorate: function(viewport) { 42 tracing.tracks.HeadingTrack.prototype.decorate.call(this, viewport); 43 this.classList.add('slice-track'); 44 this.elidedTitleCache = new ElidedTitleCache(); 45 this.asyncStyle_ = false; 46 this.slices_ = null; 47 }, 48 49 get asyncStyle() { 50 return this.asyncStyle_; 51 }, 52 53 set asyncStyle(v) { 54 this.asyncStyle_ = !!v; 55 }, 56 57 get slices() { 58 return this.slices_; 59 }, 60 61 set slices(slices) { 62 this.slices_ = slices || []; 63 }, 64 65 get height() { 66 return window.getComputedStyle(this).height; 67 }, 68 69 set height(height) { 70 this.style.height = height; 71 }, 72 73 get hasVisibleContent() { 74 return this.slices.length > 0; 75 }, 76 77 labelWidth: function(title) { 78 return quickMeasureText(this.context(), title) + 2; 79 }, 80 81 labelWidthWorld: function(title, pixWidth) { 82 return this.labelWidth(title) * pixWidth; 83 }, 84 85 draw: function(type, viewLWorld, viewRWorld) { 86 switch (type) { 87 case tracing.tracks.DrawType.SLICE: 88 this.drawSlices_(viewLWorld, viewRWorld); 89 break; 90 } 91 }, 92 93 drawSlices_: function(viewLWorld, viewRWorld) { 94 var ctx = this.context(); 95 var pixelRatio = window.devicePixelRatio || 1; 96 97 var bounds = this.getBoundingClientRect(); 98 var height = bounds.height * pixelRatio; 99 100 // Culling parameters. 101 var vp = this.viewport; 102 var pixWidth = vp.xViewVectorToWorld(1); 103 104 // Begin rendering in world space. 105 ctx.save(); 106 vp.applyTransformToCanvas(ctx); 107 108 // Slices. 109 if (this.asyncStyle_) 110 ctx.globalAlpha = 0.25; 111 var tr = new tracing.FastRectRenderer(ctx, 2 * pixWidth, 2 * pixWidth, 112 palette); 113 tr.setYandH(0, height); 114 var slices = this.slices_; 115 var lowSlice = base.findLowIndexInSortedArray( 116 slices, 117 function(slice) { return slice.start + slice.duration; }, 118 viewLWorld); 119 120 for (var i = lowSlice; i < slices.length; ++i) { 121 var slice = slices[i]; 122 var x = slice.start; 123 if (x > viewRWorld) 124 break; 125 126 // Less than 0.001 causes short events to disappear when zoomed in. 127 var w = Math.max(slice.duration, 0.001); 128 var colorId = slice.selected ? 129 slice.colorId + highlightIdBoost : 130 slice.colorId; 131 132 if (w < pixWidth) 133 w = pixWidth; 134 if (slice.duration > 0) { 135 tr.fillRect(x, w, colorId); 136 } else { 137 // Instant: draw a triangle. If zoomed too far, collapse 138 // into the FastRectRenderer. 139 if (pixWidth > 0.001) { 140 tr.fillRect(x, pixWidth, colorId); 141 } else { 142 ctx.fillStyle = palette[colorId]; 143 ctx.beginPath(); 144 ctx.moveTo(x - (4 * pixWidth), height); 145 ctx.lineTo(x, 0); 146 ctx.lineTo(x + (4 * pixWidth), height); 147 ctx.closePath(); 148 ctx.fill(); 149 } 150 } 151 } 152 tr.flush(); 153 ctx.restore(); 154 155 // Labels. 156 if (height > 8) { 157 ctx.textAlign = 'center'; 158 ctx.textBaseline = 'top'; 159 ctx.font = (10 * pixelRatio) + 'px sans-serif'; 160 ctx.strokeStyle = 'rgb(0,0,0)'; 161 ctx.fillStyle = 'rgb(0,0,0)'; 162 163 // Don't render text until until it is 20px wide 164 var quickDiscardThresshold = pixWidth * 20; 165 var shouldElide = this.SHOULD_ELIDE_TEXT; 166 for (var i = lowSlice; i < slices.length; ++i) { 167 var slice = slices[i]; 168 if (slice.start > viewRWorld) 169 break; 170 171 if (slice.duration <= quickDiscardThresshold) 172 continue; 173 174 var title = slice.title + 175 (slice.didNotFinish ? ' (Did Not Finish)' : ''); 176 177 var drawnTitle = title; 178 var drawnWidth = this.labelWidth(drawnTitle); 179 if (shouldElide && 180 this.labelWidthWorld(drawnTitle, pixWidth) > slice.duration) { 181 var elidedValues = this.elidedTitleCache.get( 182 this, pixWidth, 183 drawnTitle, drawnWidth, 184 slice.duration); 185 drawnTitle = elidedValues.string; 186 drawnWidth = elidedValues.width; 187 } 188 189 if (drawnWidth * pixWidth < slice.duration) { 190 191 var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); 192 ctx.fillText(drawnTitle, cX, 2.5 * pixelRatio, drawnWidth); 193 } 194 } 195 } 196 }, 197 198 addIntersectingItemsInRangeToSelectionInWorldSpace: function( 199 loWX, hiWX, viewPixWidthWorld, selection) { 200 function onPickHit(slice) { 201 var hit = selection.addSlice(this, slice); 202 this.decorateHit(hit); 203 } 204 base.iterateOverIntersectingIntervals(this.slices_, 205 function(x) { return x.start; }, 206 function(x) { return x.duration; }, 207 loWX, hiWX, 208 onPickHit.bind(this)); 209 }, 210 211 /** 212 * Find the index for the given slice. 213 * @return {index} Index of the given slice, or undefined. 214 * @private 215 */ 216 indexOfSlice_: function(slice) { 217 var index = base.findLowIndexInSortedArray(this.slices_, 218 function(x) { return x.start; }, 219 slice.start); 220 while (index < this.slices_.length && 221 slice.start == this.slices_[index].start && 222 slice.colorId != this.slices_[index].colorId) { 223 index++; 224 } 225 return index < this.slices_.length ? index : undefined; 226 }, 227 228 /** 229 * Add the item to the left or right of the provided hit, if any, to the 230 * selection. 231 * @param {slice} The current slice. 232 * @param {Number} offset Number of slices away from the hit to look. 233 * @param {Selection} selection The selection to add a hit to, 234 * if found. 235 * @return {boolean} Whether a hit was found. 236 * @private 237 */ 238 addItemNearToProvidedHitToSelection: function(hit, offset, selection) { 239 if (!hit.slice) 240 return false; 241 242 var index = this.indexOfSlice_(hit.slice); 243 if (index === undefined) 244 return false; 245 246 var newIndex = index + offset; 247 if (newIndex < 0 || newIndex >= this.slices_.length) 248 return false; 249 250 var hit = selection.addSlice(this, this.slices_[newIndex]); 251 this.decorateHit(hit); 252 return true; 253 }, 254 255 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 256 for (var i = 0; i < this.slices_.length; ++i) { 257 if (filter.matchSlice(this.slices_[i])) { 258 var hit = selection.addSlice(this, this.slices_[i]); 259 this.decorateHit(hit); 260 } 261 } 262 } 263 }; 264 265 var highlightIdBoost = tracing.getColorPaletteHighlightIdBoost(); 266 267 // TODO(jrg): possibly obsoleted with the elided string cache. 268 // Consider removing. 269 var textWidthMap = { }; 270 function quickMeasureText(ctx, text) { 271 var w = textWidthMap[text]; 272 if (!w) { 273 w = ctx.measureText(text).width; 274 textWidthMap[text] = w; 275 } 276 return w; 277 } 278 279 /** 280 * Cache for elided strings. 281 * Moved from the ElidedTitleCache protoype to a "global" for speed 282 * (variable reference is 100x faster). 283 * key: String we wish to elide. 284 * value: Another dict whose key is width 285 * and value is an ElidedStringWidthPair. 286 */ 287 var elidedTitleCacheDict = {}; 288 289 /** 290 * A cache for elided strings. 291 * @constructor 292 */ 293 function ElidedTitleCache() { 294 } 295 296 ElidedTitleCache.prototype = { 297 /** 298 * Return elided text. 299 * @param {track} A slice track or other object that defines 300 * functions labelWidth() and labelWidthWorld(). 301 * @param {pixWidth} Pixel width. 302 * @param {title} Original title text. 303 * @param {width} Drawn width in world coords. 304 * @param {sliceDuration} Where the title must fit (in world coords). 305 * @return {ElidedStringWidthPair} Elided string and width. 306 */ 307 get: function(track, pixWidth, title, width, sliceDuration) { 308 var elidedDict = elidedTitleCacheDict[title]; 309 if (!elidedDict) { 310 elidedDict = {}; 311 elidedTitleCacheDict[title] = elidedDict; 312 } 313 var elidedDictForPixWidth = elidedDict[pixWidth]; 314 if (!elidedDictForPixWidth) { 315 elidedDict[pixWidth] = {}; 316 elidedDictForPixWidth = elidedDict[pixWidth]; 317 } 318 var stringWidthPair = elidedDictForPixWidth[sliceDuration]; 319 if (stringWidthPair === undefined) { 320 var newtitle = title; 321 var elided = false; 322 while (track.labelWidthWorld(newtitle, pixWidth) > sliceDuration) { 323 if (newtitle.length * 0.75 < 1) 324 break; 325 newtitle = newtitle.substring(0, newtitle.length * 0.75); 326 elided = true; 327 } 328 if (elided && newtitle.length > 3) 329 newtitle = newtitle.substring(0, newtitle.length - 3) + '...'; 330 stringWidthPair = new ElidedStringWidthPair( 331 newtitle, 332 track.labelWidth(newtitle)); 333 elidedDictForPixWidth[sliceDuration] = stringWidthPair; 334 } 335 return stringWidthPair; 336 } 337 }; 338 339 /** 340 * A pair representing an elided string and world-coordinate width 341 * to draw it. 342 * @constructor 343 */ 344 function ElidedStringWidthPair(string, width) { 345 this.string = string; 346 this.width = width; 347 } 348 349 return { 350 SliceTrack: SliceTrack 351 }; 352 }); 353