1 // Copyright (c) 2011 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 6 /** 7 * @fileoverview Renders an array of slices into the provided div, 8 * using a child canvas element. Uses a FastRectRenderer to draw only 9 * the visible slices. 10 */ 11 cr.define('gpu', function() { 12 13 const palletteBase = [ 14 {r: 0x45, g: 0x85, b: 0xaa}, 15 {r: 0xdc, g: 0x73, b: 0xa8}, 16 {r: 0x77, g: 0xb6, b: 0x94}, 17 {r: 0x23, g: 0xae, b: 0x6e}, 18 {r: 0x76, g: 0x5d, b: 0x9e}, 19 {r: 0x48, g: 0xd8, b: 0xfb}, 20 {r: 0xa9, g: 0xd7, b: 0x93}, 21 {r: 0x7c, g: 0x2d, b: 0x52}, 22 {r: 0x69, g: 0xc2, b: 0x75}, 23 {r: 0x76, g: 0xcf, b: 0xee}, 24 {r: 0x3d, g: 0x85, b: 0xd1}, 25 {r: 0x71, g: 0x0b, b: 0x54}]; 26 27 function brighten(c) { 28 return {r: Math.min(255, c.r + Math.floor(c.r * 0.45)), 29 g: Math.min(255, c.g + Math.floor(c.g * 0.45)), 30 b: Math.min(255, c.b + Math.floor(c.b * 0.45))}; 31 } 32 function colorToString(c) { 33 return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')'; 34 } 35 36 const selectedIdBoost = palletteBase.length; 37 38 const pallette = palletteBase.concat(palletteBase.map(brighten)). 39 map(colorToString); 40 41 var textWidthMap = { }; 42 function quickMeasureText(ctx, text) { 43 var w = textWidthMap[text]; 44 if (!w) { 45 w = ctx.measureText(text).width; 46 textWidthMap[text] = w; 47 } 48 return w; 49 } 50 51 /** 52 * Generic base class for timeline tracks 53 */ 54 TimelineThreadTrack = cr.ui.define('div'); 55 TimelineThreadTrack.prototype = { 56 __proto__: HTMLDivElement.prototype, 57 58 decorate: function() { 59 this.className = 'timeline-thread-track'; 60 }, 61 62 set thread(thread) { 63 this.thread_ = thread; 64 this.updateChildTracks_(); 65 }, 66 67 set viewport(v) { 68 this.viewport_ = v; 69 for (var i = 0; i < this.tracks_.length; i++) 70 this.tracks_[i].viewport = v; 71 this.invalidate(); 72 }, 73 74 invalidate: function() { 75 if (this.parentNode) 76 this.parentNode.invalidate(); 77 }, 78 79 onResize: function() { 80 for (var i = 0; i < this.tracks_.length; i++) 81 this.tracks_[i].onResize(); 82 }, 83 84 get firstCanvas() { 85 if (this.tracks_.length) 86 return this.tracks_[0].firstCanvas; 87 return undefined; 88 }, 89 90 redraw: function() { 91 for (var i = 0; i < this.tracks_.length; i++) 92 this.tracks_[i].redraw(); 93 }, 94 95 updateChildTracks_: function() { 96 this.textContent = ''; 97 this.tracks_ = []; 98 if (this.thread_) { 99 for (var srI = 0; srI < this.thread_.subRows.length; ++srI) { 100 var track = new TimelineSliceTrack(); 101 102 if (srI == 0) 103 track.heading = this.thread_.parent.pid + ': ' + 104 this.thread_.tid + ': '; 105 else 106 track.heading = ''; 107 track.slices = this.thread_.subRows[srI]; 108 track.viewport = this.viewport_; 109 110 this.tracks_.push(track); 111 this.appendChild(track); 112 } 113 } 114 }, 115 116 /** 117 * Picks a slice, if any, at a given location. 118 * @param {number} wX X location to search at, in worldspace. 119 * @param {number} wY Y location to search at, in offset space. 120 * offset space. 121 * @param {function():*} onHitCallback Callback to call with the slice, 122 * if one is found. 123 * @return {boolean} true if a slice was found, otherwise false. 124 */ 125 pick: function(wX, wY, onHitCallback) { 126 for (var i = 0; i < this.tracks_.length; i++) { 127 var track = this.tracks_[i]; 128 if (wY >= track.offsetTop && wY < track.offsetTop + track.offsetHeight) 129 return track.pick(wX, onHitCallback); 130 } 131 return false; 132 }, 133 134 /** 135 * Finds slices intersecting the given interval. 136 * @param {number} loWX Lower X bound of the interval to search, in 137 * worldspace. 138 * @param {number} hiWX Upper X bound of the interval to search, in 139 * worldspace. 140 * @param {number} loY Lower Y bound of the interval to search, in 141 * offset space. 142 * @param {number} hiY Upper Y bound of the interval to search, in 143 * offset space. 144 * @param {function():*} onHitCallback Function to call for each slice 145 * intersecting the interval. 146 */ 147 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { 148 for (var i = 0; i < this.tracks_.length; i++) { 149 var a = Math.max(loY, this.tracks_[i].offsetTop); 150 var b = Math.min(hiY, this.tracks_[i].offsetTop + 151 this.tracks_[i].offsetHeight); 152 if (a <= b) 153 this.tracks_[i].pickRange(loWX, hiWX, loY, hiY, onHitCallback); 154 } 155 } 156 }; 157 158 /** 159 * Creates a new timeline track div element 160 * @constructor 161 * @extends {HTMLDivElement} 162 */ 163 TimelineSliceTrack = cr.ui.define('div'); 164 165 TimelineSliceTrack.prototype = { 166 __proto__: HTMLDivElement.prototype, 167 168 decorate: function() { 169 this.className = 'timeline-slice-track'; 170 this.slices_ = null; 171 172 this.titleDiv_ = document.createElement('div'); 173 this.titleDiv_.className = 'timeline-slice-track-title'; 174 this.appendChild(this.titleDiv_); 175 176 this.canvasContainer_ = document.createElement('div'); 177 this.canvasContainer_.className = 'timeline-slice-track-canvas-container'; 178 this.appendChild(this.canvasContainer_); 179 this.canvas_ = document.createElement('canvas'); 180 this.canvas_.className = 'timeline-slice-track-canvas'; 181 this.canvasContainer_.appendChild(this.canvas_); 182 183 this.ctx_ = this.canvas_.getContext('2d'); 184 }, 185 186 set heading(text) { 187 this.titleDiv_.textContent = text; 188 }, 189 190 set slices(slices) { 191 this.slices_ = slices; 192 this.invalidate(); 193 }, 194 195 set viewport(v) { 196 this.viewport_ = v; 197 this.invalidate(); 198 }, 199 200 invalidate: function() { 201 if (this.parentNode) 202 this.parentNode.invalidate(); 203 }, 204 205 get firstCanvas() { 206 return this.canvas_; 207 }, 208 209 onResize: function() { 210 this.canvas_.width = this.canvasContainer_.clientWidth; 211 this.canvas_.height = this.canvasContainer_.clientHeight; 212 this.invalidate(); 213 }, 214 215 redraw: function() { 216 if (!this.viewport_) 217 return; 218 var ctx = this.ctx_; 219 var canvasW = this.canvas_.width; 220 var canvasH = this.canvas_.height; 221 222 ctx.clearRect(0, 0, canvasW, canvasH); 223 224 // culling... 225 var vp = this.viewport_; 226 var pixWidth = vp.xViewVectorToWorld(1); 227 var viewLWorld = vp.xViewToWorld(0); 228 var viewRWorld = vp.xViewToWorld(this.width); 229 230 // begin rendering in world space 231 ctx.save(); 232 vp.applyTransformToCanavs(ctx); 233 234 // tracks 235 var tr = new gpu.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth, 236 2 * pixWidth, viewRWorld, pallette); 237 tr.setYandH(0, canvasH); 238 var slices = this.slices_; 239 for (var i = 0; i < slices.length; ++i) { 240 var slice = slices[i]; 241 var x = slice.start; 242 var w = slice.duration; 243 var colorId; 244 colorId = slice.selected ? 245 slice.colorId + selectedIdBoost : 246 slice.colorId; 247 248 if (w < pixWidth) 249 w = pixWidth; 250 tr.fillRect(x, w, colorId); 251 } 252 tr.flush(); 253 ctx.restore(); 254 255 // labels 256 ctx.textAlign = 'center'; 257 ctx.textBaseline = 'top'; 258 ctx.font = '10px sans-serif'; 259 ctx.strokeStyle = 'rgb(0,0,0)'; 260 ctx.fillStyle = 'rgb(0,0,0)'; 261 var quickDiscardThresshold = pixWidth * 20; // dont render until 20px wide 262 for (var i = 0; i < slices.length; ++i) { 263 var slice = slices[i]; 264 if (slice.duration > quickDiscardThresshold) { 265 var labelWidth = quickMeasureText(ctx, slice.title) + 2; 266 var labelWidthWorld = pixWidth * labelWidth; 267 if (labelWidthWorld < slice.duration) { 268 var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); 269 ctx.fillText(slice.title, cX, 2.5); 270 } 271 } 272 } 273 }, 274 275 /** 276 * Picks a slice, if any, at a given location. 277 * @param {number} wX X location to search at, in worldspace. 278 * @param {number} wY Y location to search at, in offset space. 279 * offset space. 280 * @param {function():*} onHitCallback Callback to call with the slice, 281 * if one is found. 282 * @return {boolean} true if a slice was found, otherwise false. 283 */ 284 pick: function(wX, wY, onHitCallback) { 285 if (wY < this.offsetTop || wY >= this.offsetTop + this.offsetHeight) 286 return false; 287 var x = gpu.findLowIndexInSortedIntervals(this.slices_, 288 function(x) { return x.start; }, 289 function(x) { return x.duration; }, 290 wX); 291 if (x >= 0 && x < this.slices_.length) { 292 onHitCallback('slice', this, this.slices_[x]); 293 return true; 294 } 295 return false; 296 }, 297 298 /** 299 * Finds slices intersecting the given interval. 300 * @param {number} loWX Lower X bound of the interval to search, in 301 * worldspace. 302 * @param {number} hiWX Upper X bound of the interval to search, in 303 * worldspace. 304 * @param {number} loY Lower Y bound of the interval to search, in 305 * offset space. 306 * @param {number} hiY Upper Y bound of the interval to search, in 307 * offset space. 308 * @param {function():*} onHitCallback Function to call for each slice 309 * intersecting the interval. 310 */ 311 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { 312 var a = Math.max(loY, this.offsetTop); 313 var b = Math.min(hiY, this.offsetTop + this.offsetHeight); 314 if (a > b) 315 return; 316 317 function onPickHit(slice) { 318 onHitCallback('slice', this, slice); 319 } 320 gpu.iterateOverIntersectingIntervals(this.slices_, 321 function(x) { return x.start; }, 322 function(x) { return x.duration; }, 323 loWX, hiWX, 324 onPickHit); 325 } 326 327 }; 328 329 return { 330 TimelineSliceTrack: TimelineSliceTrack, 331 TimelineThreadTrack: TimelineThreadTrack 332 }; 333 }); 334