| OLD | NEW |
| (Empty) |
| 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: 138, g: 113, b: 152}, | |
| 15 {r: 175, g: 112, b: 133}, | |
| 16 {r: 127, g: 135, b: 225}, | |
| 17 {r: 93, g: 81, b: 137}, | |
| 18 {r: 116, g: 143, b: 119}, | |
| 19 {r: 178, g: 214, b: 122}, | |
| 20 {r: 87, g: 109, b: 147}, | |
| 21 {r: 119, g: 155, b: 95}, | |
| 22 {r: 114, g: 180, b: 160}, | |
| 23 {r: 132, g: 85, b: 103}, | |
| 24 {r: 157, g: 210, b: 150}, | |
| 25 {r: 148, g: 94, b: 86}, | |
| 26 {r: 164, g: 108, b: 138}, | |
| 27 {r: 139, g: 191, b: 150}, | |
| 28 {r: 110, g: 99, b: 145}, | |
| 29 {r: 80, g: 129, b: 109}, | |
| 30 {r: 125, g: 140, b: 149}, | |
| 31 {r: 93, g: 124, b: 132}, | |
| 32 {r: 140, g: 85, b: 140}, | |
| 33 {r: 104, g: 163, b: 162}, | |
| 34 {r: 132, g: 141, b: 178}, | |
| 35 {r: 131, g: 105, b: 147}, | |
| 36 {r: 135, g: 183, b: 98}, | |
| 37 {r: 152, g: 134, b: 177}, | |
| 38 {r: 141, g: 188, b: 141}, | |
| 39 {r: 133, g: 160, b: 210}, | |
| 40 {r: 126, g: 186, b: 148}, | |
| 41 {r: 112, g: 198, b: 205}, | |
| 42 {r: 180, g: 122, b: 195}, | |
| 43 {r: 203, g: 144, b: 152}]; | |
| 44 | |
| 45 function brighten(c) { | |
| 46 return {r: Math.min(255, c.r + Math.floor(c.r * 0.45)), | |
| 47 g: Math.min(255, c.g + Math.floor(c.g * 0.45)), | |
| 48 b: Math.min(255, c.b + Math.floor(c.b * 0.45))}; | |
| 49 } | |
| 50 function colorToString(c) { | |
| 51 return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')'; | |
| 52 } | |
| 53 | |
| 54 const selectedIdBoost = palletteBase.length; | |
| 55 | |
| 56 const pallette = palletteBase.concat(palletteBase.map(brighten)). | |
| 57 map(colorToString); | |
| 58 | |
| 59 var textWidthMap = { }; | |
| 60 function quickMeasureText(ctx, text) { | |
| 61 var w = textWidthMap[text]; | |
| 62 if (!w) { | |
| 63 w = ctx.measureText(text).width; | |
| 64 textWidthMap[text] = w; | |
| 65 } | |
| 66 return w; | |
| 67 } | |
| 68 | |
| 69 function addTrack(thisTrack, slices) { | |
| 70 var track = new TimelineSliceTrack(); | |
| 71 | |
| 72 track.heading = ''; | |
| 73 track.slices = slices; | |
| 74 track.viewport = thisTrack.viewport_; | |
| 75 | |
| 76 thisTrack.tracks_.push(track); | |
| 77 thisTrack.appendChild(track); | |
| 78 } | |
| 79 | |
| 80 /** | |
| 81 * Generic base class for timeline tracks | |
| 82 */ | |
| 83 TimelineThreadTrack = cr.ui.define('div'); | |
| 84 TimelineThreadTrack.prototype = { | |
| 85 __proto__: HTMLDivElement.prototype, | |
| 86 | |
| 87 decorate: function() { | |
| 88 this.className = 'timeline-thread-track'; | |
| 89 }, | |
| 90 | |
| 91 set thread(thread) { | |
| 92 this.thread_ = thread; | |
| 93 this.updateChildTracks_(); | |
| 94 }, | |
| 95 | |
| 96 set viewport(v) { | |
| 97 this.viewport_ = v; | |
| 98 for (var i = 0; i < this.tracks_.length; i++) | |
| 99 this.tracks_[i].viewport = v; | |
| 100 this.invalidate(); | |
| 101 }, | |
| 102 | |
| 103 invalidate: function() { | |
| 104 if (this.parentNode) | |
| 105 this.parentNode.invalidate(); | |
| 106 }, | |
| 107 | |
| 108 onResize: function() { | |
| 109 for (var i = 0; i < this.tracks_.length; i++) | |
| 110 this.tracks_[i].onResize(); | |
| 111 }, | |
| 112 | |
| 113 get firstCanvas() { | |
| 114 if (this.tracks_.length) | |
| 115 return this.tracks_[0].firstCanvas; | |
| 116 return undefined; | |
| 117 }, | |
| 118 | |
| 119 redraw: function() { | |
| 120 for (var i = 0; i < this.tracks_.length; i++) | |
| 121 this.tracks_[i].redraw(); | |
| 122 }, | |
| 123 | |
| 124 updateChildTracks_: function() { | |
| 125 this.textContent = ''; | |
| 126 this.tracks_ = []; | |
| 127 if (this.thread_) { | |
| 128 for (var srI = 0; srI < this.thread_.nonNestedSubRows.length; ++srI) { | |
| 129 addTrack(this, this.thread_.nonNestedSubRows[srI]); | |
| 130 } | |
| 131 for (var srI = 0; srI < this.thread_.subRows.length; ++srI) { | |
| 132 addTrack(this, this.thread_.subRows[srI]); | |
| 133 } | |
| 134 if (this.tracks_.length > 0) { | |
| 135 this.tracks_[0].heading = this.thread_.parent.pid + ': ' + | |
| 136 this.thread_.tid + ': '; | |
| 137 } | |
| 138 } | |
| 139 }, | |
| 140 | |
| 141 /** | |
| 142 * Picks a slice, if any, at a given location. | |
| 143 * @param {number} wX X location to search at, in worldspace. | |
| 144 * @param {number} wY Y location to search at, in offset space. | |
| 145 * offset space. | |
| 146 * @param {function():*} onHitCallback Callback to call with the slice, | |
| 147 * if one is found. | |
| 148 * @return {boolean} true if a slice was found, otherwise false. | |
| 149 */ | |
| 150 pick: function(wX, wY, onHitCallback) { | |
| 151 for (var i = 0; i < this.tracks_.length; i++) { | |
| 152 var trackClientRect = this.tracks_[i].getBoundingClientRect(); | |
| 153 if (wY >= trackClientRect.top && wY < trackClientRect.bottom) | |
| 154 return this.tracks_[i].pick(wX, onHitCallback); | |
| 155 } | |
| 156 return false; | |
| 157 }, | |
| 158 | |
| 159 /** | |
| 160 * Finds slices intersecting the given interval. | |
| 161 * @param {number} loWX Lower X bound of the interval to search, in | |
| 162 * worldspace. | |
| 163 * @param {number} hiWX Upper X bound of the interval to search, in | |
| 164 * worldspace. | |
| 165 * @param {number} loY Lower Y bound of the interval to search, in | |
| 166 * offset space. | |
| 167 * @param {number} hiY Upper Y bound of the interval to search, in | |
| 168 * offset space. | |
| 169 * @param {function():*} onHitCallback Function to call for each slice | |
| 170 * intersecting the interval. | |
| 171 */ | |
| 172 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { | |
| 173 for (var i = 0; i < this.tracks_.length; i++) { | |
| 174 var trackClientRect = this.tracks_[i].getBoundingClientRect(); | |
| 175 var a = Math.max(loY, trackClientRect.top); | |
| 176 var b = Math.min(hiY, trackClientRect.bottom); | |
| 177 if (a <= b) | |
| 178 this.tracks_[i].pickRange(loWX, hiWX, loY, hiY, onHitCallback); | |
| 179 } | |
| 180 } | |
| 181 }; | |
| 182 | |
| 183 /** | |
| 184 * Creates a new timeline track div element | |
| 185 * @constructor | |
| 186 * @extends {HTMLDivElement} | |
| 187 */ | |
| 188 TimelineSliceTrack = cr.ui.define('div'); | |
| 189 | |
| 190 TimelineSliceTrack.prototype = { | |
| 191 __proto__: HTMLDivElement.prototype, | |
| 192 | |
| 193 decorate: function() { | |
| 194 this.className = 'timeline-slice-track'; | |
| 195 this.slices_ = null; | |
| 196 | |
| 197 this.titleDiv_ = document.createElement('div'); | |
| 198 this.titleDiv_.className = 'timeline-slice-track-title'; | |
| 199 this.appendChild(this.titleDiv_); | |
| 200 | |
| 201 this.canvasContainer_ = document.createElement('div'); | |
| 202 this.canvasContainer_.className = 'timeline-slice-track-canvas-container'; | |
| 203 this.appendChild(this.canvasContainer_); | |
| 204 this.canvas_ = document.createElement('canvas'); | |
| 205 this.canvas_.className = 'timeline-slice-track-canvas'; | |
| 206 this.canvasContainer_.appendChild(this.canvas_); | |
| 207 | |
| 208 this.ctx_ = this.canvas_.getContext('2d'); | |
| 209 }, | |
| 210 | |
| 211 set heading(text) { | |
| 212 this.titleDiv_.textContent = text; | |
| 213 }, | |
| 214 | |
| 215 set slices(slices) { | |
| 216 this.slices_ = slices; | |
| 217 this.invalidate(); | |
| 218 }, | |
| 219 | |
| 220 set viewport(v) { | |
| 221 this.viewport_ = v; | |
| 222 this.invalidate(); | |
| 223 }, | |
| 224 | |
| 225 invalidate: function() { | |
| 226 if (this.parentNode) | |
| 227 this.parentNode.invalidate(); | |
| 228 }, | |
| 229 | |
| 230 get firstCanvas() { | |
| 231 return this.canvas_; | |
| 232 }, | |
| 233 | |
| 234 onResize: function() { | |
| 235 this.canvas_.width = this.canvasContainer_.clientWidth; | |
| 236 this.canvas_.height = this.canvasContainer_.clientHeight; | |
| 237 this.invalidate(); | |
| 238 }, | |
| 239 | |
| 240 redraw: function() { | |
| 241 if (!this.viewport_) | |
| 242 return; | |
| 243 var ctx = this.ctx_; | |
| 244 var canvasW = this.canvas_.width; | |
| 245 var canvasH = this.canvas_.height; | |
| 246 | |
| 247 ctx.clearRect(0, 0, canvasW, canvasH); | |
| 248 | |
| 249 // culling... | |
| 250 var vp = this.viewport_; | |
| 251 var pixWidth = vp.xViewVectorToWorld(1); | |
| 252 var viewLWorld = vp.xViewToWorld(0); | |
| 253 var viewRWorld = vp.xViewToWorld(canvasW); | |
| 254 | |
| 255 // Draw grid without a transform because the scale | |
| 256 // affects line width. | |
| 257 if (vp.gridEnabled) { | |
| 258 var x = vp.gridTimebase; | |
| 259 ctx.beginPath(); | |
| 260 while (x < viewRWorld) { | |
| 261 if (x >= viewLWorld) { | |
| 262 // Do conversion to viewspace here rather than on | |
| 263 // x to avoid precision issues. | |
| 264 var vx = vp.xWorldToView(x); | |
| 265 ctx.moveTo(vx, 0); | |
| 266 ctx.lineTo(vx, canvasH); | |
| 267 } | |
| 268 x += vp.gridStep; | |
| 269 } | |
| 270 ctx.strokeStyle = 'rgba(255,0,0,0.25)'; | |
| 271 ctx.stroke(); | |
| 272 } | |
| 273 | |
| 274 // begin rendering in world space | |
| 275 ctx.save(); | |
| 276 vp.applyTransformToCanavs(ctx); | |
| 277 | |
| 278 // tracks | |
| 279 var tr = new gpu.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth, | |
| 280 2 * pixWidth, viewRWorld, pallette); | |
| 281 tr.setYandH(0, canvasH); | |
| 282 var slices = this.slices_; | |
| 283 for (var i = 0; i < slices.length; ++i) { | |
| 284 var slice = slices[i]; | |
| 285 var x = slice.start; | |
| 286 // Less than 0.001 causes short events to disappear when zoomed in. | |
| 287 var w = Math.max(slice.duration, 0.001); | |
| 288 var colorId; | |
| 289 colorId = slice.selected ? | |
| 290 slice.colorId + selectedIdBoost : | |
| 291 slice.colorId; | |
| 292 | |
| 293 if (w < pixWidth) | |
| 294 w = pixWidth; | |
| 295 if (slice.duration > 0) { | |
| 296 tr.fillRect(x, w, colorId); | |
| 297 } else { | |
| 298 // Instant: draw a triangle. If zoomed too far, collapse | |
| 299 // into the FastRectRenderer. | |
| 300 if (pixWidth > 0.001) { | |
| 301 tr.fillRect(x, pixWidth, colorId); | |
| 302 } else { | |
| 303 ctx.fillStyle = pallette[colorId]; | |
| 304 ctx.beginPath(); | |
| 305 ctx.moveTo(x - (4 * pixWidth), canvasH); | |
| 306 ctx.lineTo(x, 0); | |
| 307 ctx.lineTo(x + (4 * pixWidth), canvasH); | |
| 308 ctx.closePath(); | |
| 309 ctx.fill(); | |
| 310 } | |
| 311 } | |
| 312 } | |
| 313 tr.flush(); | |
| 314 ctx.restore(); | |
| 315 | |
| 316 // labels | |
| 317 ctx.textAlign = 'center'; | |
| 318 ctx.textBaseline = 'top'; | |
| 319 ctx.font = '10px sans-serif'; | |
| 320 ctx.strokeStyle = 'rgb(0,0,0)'; | |
| 321 ctx.fillStyle = 'rgb(0,0,0)'; | |
| 322 var quickDiscardThresshold = pixWidth * 20; // dont render until 20px wide | |
| 323 for (var i = 0; i < slices.length; ++i) { | |
| 324 var slice = slices[i]; | |
| 325 if (slice.duration > quickDiscardThresshold) { | |
| 326 var title = slice.title; | |
| 327 if (slice.didNotFinish) { | |
| 328 title += " (Did Not Finish)"; | |
| 329 } | |
| 330 function labelWidth() { | |
| 331 return quickMeasureText(ctx, title) + 2; | |
| 332 } | |
| 333 function labelWidthWorld() { | |
| 334 return pixWidth * labelWidth(); | |
| 335 } | |
| 336 var elided = false; | |
| 337 while (labelWidthWorld() > slice.duration) { | |
| 338 title = title.substring(0, title.length * 0.75); | |
| 339 elided = true; | |
| 340 } | |
| 341 if (elided && title.length > 3) | |
| 342 title = title.substring(0, title.length - 3) + '...'; | |
| 343 var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); | |
| 344 ctx.fillText(title, cX, 2.5, labelWidthWorld()); | |
| 345 } | |
| 346 } | |
| 347 }, | |
| 348 | |
| 349 /** | |
| 350 * Picks a slice, if any, at a given location. | |
| 351 * @param {number} wX X location to search at, in worldspace. | |
| 352 * @param {number} wY Y location to search at, in offset space. | |
| 353 * offset space. | |
| 354 * @param {function():*} onHitCallback Callback to call with the slice, | |
| 355 * if one is found. | |
| 356 * @return {boolean} true if a slice was found, otherwise false. | |
| 357 */ | |
| 358 pick: function(wX, wY, onHitCallback) { | |
| 359 var clientRect = this.getBoundingClientRect(); | |
| 360 if (wY < clientRect.top || wY >= clientRect.bottom) | |
| 361 return false; | |
| 362 var x = gpu.findLowIndexInSortedIntervals(this.slices_, | |
| 363 function(x) { return x.start; }, | |
| 364 function(x) { return x.duration; }, | |
| 365 wX); | |
| 366 if (x >= 0 && x < this.slices_.length) { | |
| 367 onHitCallback('slice', this, this.slices_[x]); | |
| 368 return true; | |
| 369 } | |
| 370 return false; | |
| 371 }, | |
| 372 | |
| 373 /** | |
| 374 * Finds slices intersecting the given interval. | |
| 375 * @param {number} loWX Lower X bound of the interval to search, in | |
| 376 * worldspace. | |
| 377 * @param {number} hiWX Upper X bound of the interval to search, in | |
| 378 * worldspace. | |
| 379 * @param {number} loY Lower Y bound of the interval to search, in | |
| 380 * offset space. | |
| 381 * @param {number} hiY Upper Y bound of the interval to search, in | |
| 382 * offset space. | |
| 383 * @param {function():*} onHitCallback Function to call for each slice | |
| 384 * intersecting the interval. | |
| 385 */ | |
| 386 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { | |
| 387 var clientRect = this.getBoundingClientRect(); | |
| 388 var a = Math.max(loY, clientRect.top); | |
| 389 var b = Math.min(hiY, clientRect.bottom); | |
| 390 if (a > b) | |
| 391 return; | |
| 392 | |
| 393 var that = this; | |
| 394 function onPickHit(slice) { | |
| 395 onHitCallback('slice', that, slice); | |
| 396 } | |
| 397 gpu.iterateOverIntersectingIntervals(this.slices_, | |
| 398 function(x) { return x.start; }, | |
| 399 function(x) { return x.duration; }, | |
| 400 loWX, hiWX, | |
| 401 onPickHit); | |
| 402 }, | |
| 403 | |
| 404 /** | |
| 405 * Find the index for the given slice. | |
| 406 * @return {index} Index of the given slice, or undefined. | |
| 407 * @private | |
| 408 */ | |
| 409 indexOfSlice_: function(slice) { | |
| 410 var index = gpu.findLowIndexInSortedArray(this.slices_, | |
| 411 function(x) { return x.start; }, | |
| 412 slice.start); | |
| 413 while (index < this.slices_.length && | |
| 414 slice.start == this.slices_[index].start && | |
| 415 slice.colorId != this.slices_[index].colorId) { | |
| 416 index++; | |
| 417 } | |
| 418 return index < this.slices_.length ? index : undefined; | |
| 419 }, | |
| 420 | |
| 421 /** | |
| 422 * Return the next slice, if any, after the given slice. | |
| 423 * @param {slice} The previous slice. | |
| 424 * @return {slice} The next slice, or undefined. | |
| 425 * @private | |
| 426 */ | |
| 427 pickNext: function(slice) { | |
| 428 var index = this.indexOfSlice_(slice); | |
| 429 if (index != undefined) { | |
| 430 if (index < this.slices_.length - 1) | |
| 431 index++; | |
| 432 else | |
| 433 index = undefined; | |
| 434 } | |
| 435 return index != undefined ? this.slices_[index] : undefined; | |
| 436 }, | |
| 437 | |
| 438 /** | |
| 439 * Return the previous slice, if any, before the given slice. | |
| 440 * @param {slice} A slice. | |
| 441 * @return {slice} The previous slice, or undefined. | |
| 442 */ | |
| 443 pickPrevious: function(slice) { | |
| 444 var index = this.indexOfSlice_(slice); | |
| 445 if (index == 0) | |
| 446 return undefined; | |
| 447 else if ((index != undefined) && (index > 0)) | |
| 448 index--; | |
| 449 return index != undefined ? this.slices_[index] : undefined; | |
| 450 }, | |
| 451 | |
| 452 }; | |
| 453 | |
| 454 return { | |
| 455 TimelineSliceTrack: TimelineSliceTrack, | |
| 456 TimelineThreadTrack: TimelineThreadTrack | |
| 457 }; | |
| 458 }); | |
| OLD | NEW |