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 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('tracing', function() { 12 13 var pallette = tracing.getPallette(); 14 var highlightIdBoost = tracing.getPalletteHighlightIdBoost(); 15 16 // TODO(jrg): possibly obsoleted with the elided string cache. 17 // Consider removing. 18 var textWidthMap = { }; 19 function quickMeasureText(ctx, text) { 20 var w = textWidthMap[text]; 21 if (!w) { 22 w = ctx.measureText(text).width; 23 textWidthMap[text] = w; 24 } 25 return w; 26 } 27 28 /** 29 * Cache for elided strings. 30 * Moved from the ElidedTitleCache protoype to a "global" for speed 31 * (variable reference is 100x faster). 32 * key: String we wish to elide. 33 * value: Another dict whose key is width 34 * and value is an ElidedStringWidthPair. 35 */ 36 var elidedTitleCacheDict = {}; 37 38 /** 39 * A generic track that contains other tracks as its children. 40 * @constructor 41 */ 42 var TimelineContainerTrack = cr.ui.define('div'); 43 TimelineContainerTrack.prototype = { 44 __proto__: HTMLDivElement.prototype, 45 46 decorate: function() { 47 this.tracks_ = []; 48 }, 49 50 detach: function() { 51 for (var i = 0; i < this.tracks_.length; i++) 52 this.tracks_[i].detach(); 53 }, 54 55 get viewport() { 56 return this.viewport_; 57 }, 58 59 set viewport(v) { 60 this.viewport_ = v; 61 for (var i = 0; i < this.tracks_.length; i++) 62 this.tracks_[i].viewport = v; 63 this.updateChildTracks_(); 64 }, 65 66 get firstCanvas() { 67 if (this.tracks_.length) 68 return this.tracks_[0].firstCanvas; 69 return undefined; 70 }, 71 72 /** 73 * Adds items intersecting a point to a selection. 74 * @param {number} wX X location to search at, in worldspace. 75 * @param {number} wY Y location to search at, in offset space. 76 * offset space. 77 * @param {TimelineSelection} selection Selection to which to add hits. 78 * @return {boolean} true if a slice was found, otherwise false. 79 */ 80 addIntersectingItemsToSelection: function(wX, wY, selection) { 81 for (var i = 0; i < this.tracks_.length; i++) { 82 var trackClientRect = this.tracks_[i].getBoundingClientRect(); 83 if (wY >= trackClientRect.top && wY < trackClientRect.bottom) 84 this.tracks_[i].addIntersectingItemsToSelection(wX, wY, selection); 85 } 86 return false; 87 }, 88 89 /** 90 * Adds items intersecting the given range to a selection. 91 * @param {number} loWX Lower X bound of the interval to search, in 92 * worldspace. 93 * @param {number} hiWX Upper X bound of the interval to search, in 94 * worldspace. 95 * @param {number} loY Lower Y bound of the interval to search, in 96 * offset space. 97 * @param {number} hiY Upper Y bound of the interval to search, in 98 * offset space. 99 * @param {TimelineSelection} selection Selection to which to add hits. 100 */ 101 addIntersectingItemsInRangeToSelection: function( 102 loWX, hiWX, loY, hiY, selection) { 103 for (var i = 0; i < this.tracks_.length; i++) { 104 var trackClientRect = this.tracks_[i].getBoundingClientRect(); 105 var a = Math.max(loY, trackClientRect.top); 106 var b = Math.min(hiY, trackClientRect.bottom); 107 if (a <= b) 108 this.tracks_[i].addIntersectingItemsInRangeToSelection( 109 loWX, hiWX, loY, hiY, selection); 110 } 111 }, 112 113 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 114 for (var i = 0; i < this.tracks_.length; i++) 115 this.tracks_[i].addAllObjectsMatchingFilterToSelection( 116 filter, selection); 117 } 118 }; 119 120 function addControlButtonElements(el, canCollapse) { 121 var closeEl = document.createElement('div'); 122 closeEl.classList.add('timeline-track-button'); 123 closeEl.classList.add('timeline-track-close-button'); 124 closeEl.textContent = String.fromCharCode(215); // × 125 closeEl.addEventListener('click', function() { 126 el.style.display = 'None'; 127 }); 128 el.appendChild(closeEl); 129 130 if (canCollapse) { 131 var collapseEl = document.createElement('div'); 132 collapseEl.classList.add('timeline-track-button'); 133 collapseEl.classList.add('timeline-track-collapse-button'); 134 var minus = '\u2212'; // minus sign; 135 var plus = '\u002b'; // plus sign; 136 collapseEl.textContent = minus; 137 var collapsed = false; 138 collapseEl.addEventListener('click', function() { 139 collapsed = !collapsed; 140 el.collapsedDidChange(collapsed); 141 collapseEl.textContent = collapsed ? plus : minus; 142 }); 143 el.appendChild(collapseEl); 144 } 145 } 146 147 /** 148 * Visualizes a TimelineThread using a series of of TimelineSliceTracks. 149 * @constructor 150 */ 151 var TimelineThreadTrack = cr.ui.define(TimelineContainerTrack); 152 TimelineThreadTrack.prototype = { 153 __proto__: TimelineContainerTrack.prototype, 154 155 decorate: function() { 156 this.classList.add('timeline-thread-track'); 157 }, 158 159 get thread() { 160 return this.thread_; 161 }, 162 163 set thread(thread) { 164 this.thread_ = thread; 165 this.updateChildTracks_(); 166 }, 167 168 get tooltip() { 169 return this.tooltip_; 170 }, 171 172 set tooltip(value) { 173 this.tooltip_ = value; 174 this.updateChildTracks_(); 175 }, 176 177 get heading() { 178 return this.heading_; 179 }, 180 181 set heading(h) { 182 this.heading_ = h; 183 this.updateChildTracks_(); 184 }, 185 186 get headingWidth() { 187 return this.headingWidth_; 188 }, 189 190 set headingWidth(width) { 191 this.headingWidth_ = width; 192 this.updateChildTracks_(); 193 }, 194 195 addTrack_: function(slices) { 196 var track = new TimelineSliceTrack(); 197 track.heading = ''; 198 track.slices = slices; 199 track.headingWidth = this.headingWidth_; 200 track.viewport = this.viewport_; 201 202 this.tracks_.push(track); 203 this.appendChild(track); 204 return track; 205 }, 206 207 updateChildTracks_: function() { 208 this.detach(); 209 this.textContent = ''; 210 this.tracks_ = []; 211 if (this.thread_) { 212 if (this.thread_.cpuSlices) { 213 var track = this.addTrack_(this.thread_.cpuSlices); 214 track.height = '4px'; 215 track.decorateHit = function(hit) { 216 hit.thread = this.thread_; 217 } 218 } 219 220 if (this.thread_.asyncSlices.length) { 221 var subRows = this.thread_.asyncSlices.subRows; 222 for (var srI = 0; srI < subRows.length; srI++) { 223 var track = this.addTrack_(subRows[srI]); 224 track.decorateHit = function(hit) { 225 // TODO(simonjam): figure out how to associate subSlice hits back 226 // to their parent slice. 227 } 228 track.asyncStyle = true; 229 } 230 } 231 232 for (var srI = 0; srI < this.thread_.subRows.length; srI++) { 233 var track = this.addTrack_(this.thread_.subRows[srI]); 234 track.decorateHit = function(hit) { 235 hit.thread = this.thread_; 236 } 237 } 238 239 if (this.tracks_.length > 0) { 240 if (this.thread_.cpuSlices) { 241 this.tracks_[1].heading = this.heading_; 242 this.tracks_[1].tooltip = this.tooltip_; 243 } else { 244 this.tracks_[0].heading = this.heading_; 245 this.tracks_[0].tooltip = this.tooltip_; 246 } 247 } 248 } 249 addControlButtonElements(this, this.tracks_.length >= 4); 250 }, 251 252 collapsedDidChange: function(collapsed) { 253 if (collapsed) { 254 var h = parseInt(this.tracks_[0].height); 255 for (var i = 0; i < this.tracks_.length; ++i) { 256 if (h > 2) { 257 this.tracks_[i].height = Math.floor(h) + 'px'; 258 } else { 259 this.tracks_[i].style.display = 'None'; 260 } 261 h = h * 0.5; 262 } 263 } else { 264 for (var i = 0; i < this.tracks_.length; ++i) { 265 this.tracks_[i].height = this.tracks_[0].height; 266 this.tracks_[i].style.display = ''; 267 } 268 } 269 } 270 }; 271 272 /** 273 * Visualizes a TimelineCpu using a series of of TimelineSliceTracks. 274 * @constructor 275 */ 276 var TimelineCpuTrack = cr.ui.define(TimelineContainerTrack); 277 TimelineCpuTrack.prototype = { 278 __proto__: TimelineContainerTrack.prototype, 279 280 decorate: function() { 281 this.classList.add('timeline-thread-track'); 282 }, 283 284 get cpu() { 285 return this.cpu_; 286 }, 287 288 set cpu(cpu) { 289 this.cpu_ = cpu; 290 this.updateChildTracks_(); 291 }, 292 293 get tooltip() { 294 return this.tooltip_; 295 }, 296 297 set tooltip(value) { 298 this.tooltip_ = value; 299 this.updateChildTracks_(); 300 }, 301 302 get heading() { 303 return this.heading_; 304 }, 305 306 set heading(h) { 307 this.heading_ = h; 308 this.updateChildTracks_(); 309 }, 310 311 get headingWidth() { 312 return this.headingWidth_; 313 }, 314 315 set headingWidth(width) { 316 this.headingWidth_ = width; 317 this.updateChildTracks_(); 318 }, 319 320 updateChildTracks_: function() { 321 this.detach(); 322 this.textContent = ''; 323 this.tracks_ = []; 324 if (this.cpu_) { 325 var track = new TimelineSliceTrack(); 326 track.slices = this.cpu_.slices; 327 track.headingWidth = this.headingWidth_; 328 track.viewport = this.viewport_; 329 330 this.tracks_.push(track); 331 this.appendChild(track); 332 333 this.tracks_[0].heading = this.heading_; 334 this.tracks_[0].tooltip = this.tooltip_; 335 } 336 addControlButtonElements(this, false); 337 } 338 }; 339 340 /** 341 * A canvas-based track constructed. Provides the basic heading and 342 * invalidation-managment infrastructure. Subclasses must implement drawing 343 * and picking code. 344 * @constructor 345 * @extends {HTMLDivElement} 346 */ 347 var CanvasBasedTrack = cr.ui.define('div'); 348 349 CanvasBasedTrack.prototype = { 350 __proto__: HTMLDivElement.prototype, 351 352 decorate: function() { 353 this.className = 'timeline-canvas-based-track'; 354 this.slices_ = null; 355 356 this.headingDiv_ = document.createElement('div'); 357 this.headingDiv_.className = 'timeline-canvas-based-track-title'; 358 this.headingDiv_.onselectstart = function() { return false; }; 359 this.appendChild(this.headingDiv_); 360 361 this.canvasContainer_ = document.createElement('div'); 362 this.canvasContainer_.className = 363 'timeline-canvas-based-track-canvas-container'; 364 this.appendChild(this.canvasContainer_); 365 this.canvas_ = document.createElement('canvas'); 366 this.canvas_.className = 'timeline-canvas-based-track-canvas'; 367 this.canvasContainer_.appendChild(this.canvas_); 368 369 this.ctx_ = this.canvas_.getContext('2d'); 370 }, 371 372 detach: function() { 373 if (this.viewport_) 374 this.viewport_.removeEventListener('change', 375 this.viewportChangeBoundToThis_); 376 }, 377 378 set headingWidth(width) { 379 this.headingDiv_.style.width = width; 380 }, 381 382 get heading() { 383 return this.headingDiv_.textContent; 384 }, 385 386 set heading(text) { 387 this.headingDiv_.textContent = text; 388 }, 389 390 set tooltip(text) { 391 this.headingDiv_.title = text; 392 }, 393 394 get viewport() { 395 return this.viewport_; 396 }, 397 398 set viewport(v) { 399 this.viewport_ = v; 400 if (this.viewport_) 401 this.viewport_.removeEventListener('change', 402 this.viewportChangeBoundToThis_); 403 this.viewport_ = v; 404 if (this.viewport_) { 405 this.viewportChangeBoundToThis_ = this.viewportChange_.bind(this); 406 this.viewport_.addEventListener('change', 407 this.viewportChangeBoundToThis_); 408 } 409 this.invalidate(); 410 }, 411 412 viewportChange_: function() { 413 this.invalidate(); 414 }, 415 416 invalidate: function() { 417 if (this.rafPending_) 418 return; 419 webkitRequestAnimationFrame(function() { 420 this.rafPending_ = false; 421 if (!this.viewport_) 422 return; 423 424 var style = window.getComputedStyle(this.canvasContainer_); 425 var style_width = parseInt(style.width); 426 var style_height = parseInt(style.height); 427 if (this.canvas_.width != style_width) 428 this.canvas_.width = style_width; 429 if (this.canvas_.height != style_height) 430 this.canvas_.height = style_height; 431 432 this.redraw(); 433 }.bind(this), this); 434 this.rafPending_ = true; 435 }, 436 437 get firstCanvas() { 438 return this.canvas_; 439 } 440 441 }; 442 443 /** 444 * A pair representing an elided string and world-coordinate width 445 * to draw it. 446 * @constructor 447 */ 448 function ElidedStringWidthPair(string, width) { 449 this.string = string; 450 this.width = width; 451 } 452 453 /** 454 * A cache for elided strings. 455 * @constructor 456 */ 457 function ElidedTitleCache() { 458 } 459 460 ElidedTitleCache.prototype = { 461 /** 462 * Return elided text. 463 * @param {track} A timeline slice track or other object that defines 464 * functions labelWidth() and labelWidthWorld(). 465 * @param {pixWidth} Pixel width. 466 * @param {title} Original title text. 467 * @param {width} Drawn width in world coords. 468 * @param {sliceDuration} Where the title must fit (in world coords). 469 * @return {ElidedStringWidthPair} Elided string and width. 470 */ 471 get: function(track, pixWidth, title, width, sliceDuration) { 472 var elidedDict = elidedTitleCacheDict[title]; 473 if (!elidedDict) { 474 elidedDict = {}; 475 elidedTitleCacheDict[title] = elidedDict; 476 } 477 var elidedDictForPixWidth = elidedDict[pixWidth]; 478 if (!elidedDictForPixWidth) { 479 elidedDict[pixWidth] = {}; 480 elidedDictForPixWidth = elidedDict[pixWidth]; 481 } 482 var stringWidthPair = elidedDictForPixWidth[sliceDuration]; 483 if (stringWidthPair === undefined) { 484 var newtitle = title; 485 var elided = false; 486 while (track.labelWidthWorld(newtitle, pixWidth) > sliceDuration) { 487 newtitle = newtitle.substring(0, newtitle.length * 0.75); 488 elided = true; 489 } 490 if (elided && newtitle.length > 3) 491 newtitle = newtitle.substring(0, newtitle.length - 3) + '...'; 492 stringWidthPair = new ElidedStringWidthPair( 493 newtitle, 494 track.labelWidth(newtitle)); 495 elidedDictForPixWidth[sliceDuration] = stringWidthPair; 496 } 497 return stringWidthPair; 498 } 499 }; 500 501 /** 502 * A track that displays an array of TimelineSlice objects. 503 * @constructor 504 * @extends {CanvasBasedTrack} 505 */ 506 507 var TimelineSliceTrack = cr.ui.define(CanvasBasedTrack); 508 509 TimelineSliceTrack.prototype = { 510 511 __proto__: CanvasBasedTrack.prototype, 512 513 /** 514 * Should we elide text on trace labels? 515 * Without eliding, text that is too wide isn't drawn at all. 516 * Disable if you feel this causes a performance problem. 517 * This is a default value that can be overridden in tracks for testing. 518 * @const 519 */ 520 SHOULD_ELIDE_TEXT: true, 521 522 decorate: function() { 523 this.classList.add('timeline-slice-track'); 524 this.elidedTitleCache = new ElidedTitleCache(); 525 this.asyncStyle_ = false; 526 }, 527 528 /** 529 * Called by all the addToSelection functions on the created selection 530 * hit objects. Override this function on parent classes to add 531 * context-specific information to the hit. 532 */ 533 decorateHit: function(hit) { 534 }, 535 536 get asyncStyle() { 537 return this.asyncStyle_; 538 }, 539 540 set asyncStyle(v) { 541 this.asyncStyle_ = !!v; 542 this.invalidate(); 543 }, 544 545 get slices() { 546 return this.slices_; 547 }, 548 549 set slices(slices) { 550 this.slices_ = slices; 551 this.invalidate(); 552 }, 553 554 get height() { 555 return window.getComputedStyle(this).height; 556 }, 557 558 set height(height) { 559 this.style.height = height; 560 this.invalidate(); 561 }, 562 563 labelWidth: function(title) { 564 return quickMeasureText(this.ctx_, title) + 2; 565 }, 566 567 labelWidthWorld: function(title, pixWidth) { 568 return this.labelWidth(title) * pixWidth; 569 }, 570 571 redraw: function() { 572 var ctx = this.ctx_; 573 var canvasW = this.canvas_.width; 574 var canvasH = this.canvas_.height; 575 576 ctx.clearRect(0, 0, canvasW, canvasH); 577 578 // Culling parameters. 579 var vp = this.viewport_; 580 var pixWidth = vp.xViewVectorToWorld(1); 581 var viewLWorld = vp.xViewToWorld(0); 582 var viewRWorld = vp.xViewToWorld(canvasW); 583 584 // Draw grid without a transform because the scale 585 // affects line width. 586 if (vp.gridEnabled) { 587 var x = vp.gridTimebase; 588 ctx.beginPath(); 589 while (x < viewRWorld) { 590 if (x >= viewLWorld) { 591 // Do conversion to viewspace here rather than on 592 // x to avoid precision issues. 593 var vx = vp.xWorldToView(x); 594 ctx.moveTo(vx, 0); 595 ctx.lineTo(vx, canvasH); 596 } 597 x += vp.gridStep; 598 } 599 ctx.strokeStyle = 'rgba(255,0,0,0.25)'; 600 ctx.stroke(); 601 } 602 603 // Begin rendering in world space. 604 ctx.save(); 605 vp.applyTransformToCanavs(ctx); 606 607 // Slices. 608 if (this.asyncStyle_) 609 ctx.globalAlpha = 0.25; 610 var tr = new tracing.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth, 611 2 * pixWidth, viewRWorld, pallette); 612 tr.setYandH(0, canvasH); 613 var slices = this.slices_; 614 for (var i = 0; i < slices.length; ++i) { 615 var slice = slices[i]; 616 var x = slice.start; 617 // Less than 0.001 causes short events to disappear when zoomed in. 618 var w = Math.max(slice.duration, 0.001); 619 var colorId = slice.selected ? 620 slice.colorId + highlightIdBoost : 621 slice.colorId; 622 623 if (w < pixWidth) 624 w = pixWidth; 625 if (slice.duration > 0) { 626 tr.fillRect(x, w, colorId); 627 } else { 628 // Instant: draw a triangle. If zoomed too far, collapse 629 // into the FastRectRenderer. 630 if (pixWidth > 0.001) { 631 tr.fillRect(x, pixWidth, colorId); 632 } else { 633 ctx.fillStyle = pallette[colorId]; 634 ctx.beginPath(); 635 ctx.moveTo(x - (4 * pixWidth), canvasH); 636 ctx.lineTo(x, 0); 637 ctx.lineTo(x + (4 * pixWidth), canvasH); 638 ctx.closePath(); 639 ctx.fill(); 640 } 641 } 642 } 643 tr.flush(); 644 ctx.restore(); 645 646 // Labels. 647 if (canvasH > 8) { 648 ctx.textAlign = 'center'; 649 ctx.textBaseline = 'top'; 650 ctx.font = '10px sans-serif'; 651 ctx.strokeStyle = 'rgb(0,0,0)'; 652 ctx.fillStyle = 'rgb(0,0,0)'; 653 // Don't render text until until it is 20px wide 654 var quickDiscardThresshold = pixWidth * 20; 655 var shouldElide = this.SHOULD_ELIDE_TEXT; 656 for (var i = 0; i < slices.length; ++i) { 657 var slice = slices[i]; 658 if (slice.duration > quickDiscardThresshold) { 659 var title = slice.title; 660 if (slice.didNotFinish) { 661 title += ' (Did Not Finish)'; 662 } 663 var drawnTitle = title; 664 var drawnWidth = this.labelWidth(drawnTitle); 665 if (shouldElide && 666 this.labelWidthWorld(drawnTitle, pixWidth) > slice.duration) { 667 var elidedValues = this.elidedTitleCache.get( 668 this, pixWidth, 669 drawnTitle, drawnWidth, 670 slice.duration); 671 drawnTitle = elidedValues.string; 672 drawnWidth = elidedValues.width; 673 } 674 if (drawnWidth * pixWidth < slice.duration) { 675 var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); 676 ctx.fillText(drawnTitle, cX, 2.5, drawnWidth); 677 } 678 } 679 } 680 } 681 }, 682 683 /** 684 * Finds slices intersecting the given interval. 685 * @param {number} wX X location to search at, in worldspace. 686 * @param {number} wY Y location to search at, in offset space. 687 * offset space. 688 * @param {TimelineSelection} selection Selection to which to add hits. 689 * @return {boolean} true if a slice was found, otherwise false. 690 */ 691 addIntersectingItemsToSelection: function(wX, wY, selection) { 692 var clientRect = this.getBoundingClientRect(); 693 if (wY < clientRect.top || wY >= clientRect.bottom) 694 return false; 695 var x = tracing.findLowIndexInSortedIntervals(this.slices_, 696 function(x) { return x.start; }, 697 function(x) { return x.duration; }, 698 wX); 699 if (x >= 0 && x < this.slices_.length) { 700 var hit = selection.addSlice(this, this.slices_[x]); 701 this.decorateHit(hit); 702 return true; 703 } 704 return false; 705 }, 706 707 /** 708 * Adds items intersecting the given range to a selection. 709 * @param {number} loWX Lower X bound of the interval to search, in 710 * worldspace. 711 * @param {number} hiWX Upper X bound of the interval to search, in 712 * worldspace. 713 * @param {number} loY Lower Y bound of the interval to search, in 714 * offset space. 715 * @param {number} hiY Upper Y bound of the interval to search, in 716 * offset space. 717 * @param {TimelineSelection} selection Selection to which to add hits. 718 */ 719 addIntersectingItemsInRangeToSelection: function( 720 loWX, hiWX, loY, hiY, selection) { 721 var clientRect = this.getBoundingClientRect(); 722 var a = Math.max(loY, clientRect.top); 723 var b = Math.min(hiY, clientRect.bottom); 724 if (a > b) 725 return; 726 727 var that = this; 728 function onPickHit(slice) { 729 var hit = selection.addSlice(that, slice); 730 that.decorateHit(hit); 731 } 732 tracing.iterateOverIntersectingIntervals(this.slices_, 733 function(x) { return x.start; }, 734 function(x) { return x.duration; }, 735 loWX, hiWX, 736 onPickHit); 737 }, 738 739 /** 740 * Find the index for the given slice. 741 * @return {index} Index of the given slice, or undefined. 742 * @private 743 */ 744 indexOfSlice_: function(slice) { 745 var index = tracing.findLowIndexInSortedArray(this.slices_, 746 function(x) { return x.start; }, 747 slice.start); 748 while (index < this.slices_.length && 749 slice.start == this.slices_[index].start && 750 slice.colorId != this.slices_[index].colorId) { 751 index++; 752 } 753 return index < this.slices_.length ? index : undefined; 754 }, 755 756 /** 757 * Add the item to the left or right of the provided hit, if any, to the 758 * selection. 759 * @param {slice} The current slice. 760 * @param {Number} offset Number of slices away from the hit to look. 761 * @param {TimelineSelection} selection The selection to add a hit to, 762 * if found. 763 * @return {boolean} Whether a hit was found. 764 * @private 765 */ 766 addItemNearToProvidedHitToSelection: function(hit, offset, selection) { 767 if (!hit.slice) 768 return false; 769 770 var index = this.indexOfSlice_(hit.slice); 771 if (index === undefined) 772 return false; 773 774 var newIndex = index + offset; 775 if (newIndex < 0 || newIndex >= this.slices_.length) 776 return false; 777 778 var hit = selection.addSlice(this, this.slices_[newIndex]); 779 this.decorateHit(hit); 780 return true; 781 }, 782 783 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 784 for (var i = 0; i < this.slices_.length; ++i) { 785 if (filter.matchSlice(this.slices_[i])) { 786 var hit = selection.addSlice(this, this.slices_[i]); 787 this.decorateHit(hit); 788 } 789 } 790 } 791 }; 792 793 /** 794 * A track that displays the viewport size and scale. 795 * @constructor 796 * @extends {CanvasBasedTrack} 797 */ 798 799 var TimelineViewportTrack = cr.ui.define(CanvasBasedTrack); 800 801 var logOf10 = Math.log(10); 802 function log10(x) { 803 return Math.log(x) / logOf10; 804 } 805 806 TimelineViewportTrack.prototype = { 807 808 __proto__: CanvasBasedTrack.prototype, 809 810 decorate: function() { 811 this.classList.add('timeline-viewport-track'); 812 this.strings_secs_ = []; 813 this.strings_msecs_ = []; 814 }, 815 816 redraw: function() { 817 var ctx = this.ctx_; 818 var canvasW = this.canvas_.width; 819 var canvasH = this.canvas_.height; 820 821 ctx.clearRect(0, 0, canvasW, canvasH); 822 823 // Culling parametrs. 824 var vp = this.viewport_; 825 var pixWidth = vp.xViewVectorToWorld(1); 826 var viewLWorld = vp.xViewToWorld(0); 827 var viewRWorld = vp.xViewToWorld(canvasW); 828 829 var idealMajorMarkDistancePix = 150; 830 var idealMajorMarkDistanceWorld = 831 vp.xViewVectorToWorld(idealMajorMarkDistancePix); 832 833 // The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc 834 var conservativeGuess = 835 Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld))); 836 837 // Once we have a conservative guess, consider things that evenly add up 838 // to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still 839 // exceeds the ideal mark distance. 840 var divisors = [10, 5, 2, 1]; 841 for (var i = 0; i < divisors.length; ++i) { 842 var tightenedGuess = conservativeGuess / divisors[i]; 843 if (vp.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix) 844 continue; 845 majorMarkDistanceWorld = conservativeGuess / divisors[i - 1]; 846 break; 847 } 848 var tickLabels = undefined; 849 if (majorMarkDistanceWorld < 100) { 850 unit = 'ms'; 851 unitDivisor = 1; 852 tickLabels = this.strings_msecs_; 853 } else { 854 unit = 's'; 855 unitDivisor = 1000; 856 tickLabels = this.strings_secs_; 857 } 858 859 var numTicksPerMajor = 5; 860 var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor; 861 var minorMarkDistancePx = vp.xWorldVectorToView(minorMarkDistanceWorld); 862 863 var firstMajorMark = 864 Math.floor(viewLWorld / majorMarkDistanceWorld) * 865 majorMarkDistanceWorld; 866 867 var minorTickH = Math.floor(canvasH * 0.25); 868 869 ctx.fillStyle = 'rgb(0, 0, 0)'; 870 ctx.strokeStyle = 'rgb(0, 0, 0)'; 871 ctx.textAlign = 'left'; 872 ctx.textBaseline = 'top'; 873 ctx.font = '9px sans-serif'; 874 875 // Each iteration of this loop draws one major mark 876 // and numTicksPerMajor minor ticks. 877 // 878 // Rendering can't be done in world space because canvas transforms 879 // affect line width. So, do the conversions manually. 880 for (var curX = firstMajorMark; 881 curX < viewRWorld; 882 curX += majorMarkDistanceWorld) { 883 884 var curXView = Math.floor(vp.xWorldToView(curX)); 885 886 var unitValue = curX / unitDivisor; 887 var roundedUnitValue = Math.floor(unitValue * 100000) / 100000; 888 if (!tickLabels[roundedUnitValue]) 889 tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit; 890 ctx.fillText(tickLabels[roundedUnitValue], curXView + 2, 0); 891 ctx.beginPath(); 892 893 // Major mark 894 ctx.moveTo(curXView, 0); 895 ctx.lineTo(curXView, canvasW); 896 897 // Minor marks 898 for (var i = 1; i < numTicksPerMajor; ++i) { 899 var xView = Math.floor(curXView + minorMarkDistancePx * i); 900 ctx.moveTo(xView, canvasH - minorTickH); 901 ctx.lineTo(xView, canvasH); 902 } 903 904 ctx.stroke(); 905 } 906 }, 907 908 /** 909 * Adds items intersecting a point to a selection. 910 * @param {number} wX X location to search at, in worldspace. 911 * @param {number} wY Y location to search at, in offset space. 912 * offset space. 913 * @param {TimelineSelection} selection Selection to which to add hits. 914 * @return {boolean} true if a slice was found, otherwise false. 915 */ 916 addIntersectingItemsToSelection: function(wX, wY, selection) { 917 // Does nothing. There's nothing interesting to pick on the viewport 918 // track. 919 }, 920 921 /** 922 * Adds items intersecting the given range to a selection. 923 * @param {number} loWX Lower X bound of the interval to search, in 924 * worldspace. 925 * @param {number} hiWX Upper X bound of the interval to search, in 926 * worldspace. 927 * @param {number} loY Lower Y bound of the interval to search, in 928 * offset space. 929 * @param {number} hiY Upper Y bound of the interval to search, in 930 * offset space. 931 * @param {TimelineSelection} selection Selection to which to add hits. 932 */ 933 addIntersectingItemsInRangeToSelection: function( 934 loWX, hiWX, loY, hiY, selection) { 935 // Does nothing. There's nothing interesting to pick on the viewport 936 // track. 937 }, 938 939 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 940 } 941 942 }; 943 944 /** 945 * A track that displays a TimelineCounter object. 946 * @constructor 947 * @extends {CanvasBasedTrack} 948 */ 949 950 var TimelineCounterTrack = cr.ui.define(CanvasBasedTrack); 951 952 TimelineCounterTrack.prototype = { 953 954 __proto__: CanvasBasedTrack.prototype, 955 956 decorate: function() { 957 this.classList.add('timeline-counter-track'); 958 addControlButtonElements(this, false); 959 this.selectedSamples_ = {}; 960 }, 961 962 /** 963 * Called by all the addToSelection functions on the created selection 964 * hit objects. Override this function on parent classes to add 965 * context-specific information to the hit. 966 */ 967 decorateHit: function(hit) { 968 }, 969 970 get counter() { 971 return this.counter_; 972 }, 973 974 set counter(counter) { 975 this.counter_ = counter; 976 this.invalidate(); 977 }, 978 979 /** 980 * @return {Object} A sparce, mutable map from sample index to bool. Samples 981 * indices the map that are true are drawn as selected. Callers that mutate 982 * the map must manually call invalidate on the track to trigger a redraw. 983 */ 984 get selectedSamples() { 985 return this.selectedSamples_; 986 }, 987 988 redraw: function() { 989 var ctr = this.counter_; 990 var ctx = this.ctx_; 991 var canvasW = this.canvas_.width; 992 var canvasH = this.canvas_.height; 993 994 ctx.clearRect(0, 0, canvasW, canvasH); 995 996 // Culling parametrs. 997 var vp = this.viewport_; 998 var pixWidth = vp.xViewVectorToWorld(1); 999 var viewLWorld = vp.xViewToWorld(0); 1000 var viewRWorld = vp.xViewToWorld(canvasW); 1001 1002 // Drop sampels that are less than skipDistancePix apart. 1003 var skipDistancePix = 1; 1004 var skipDistanceWorld = vp.xViewVectorToWorld(skipDistancePix); 1005 1006 // Begin rendering in world space. 1007 ctx.save(); 1008 vp.applyTransformToCanavs(ctx); 1009 1010 // Figure out where drawing should begin. 1011 var numSeries = ctr.numSeries; 1012 var numSamples = ctr.numSamples; 1013 var startIndex = tracing.findLowIndexInSortedArray(ctr.timestamps, 1014 function() { 1015 }, 1016 viewLWorld); 1017 1018 // Draw indices one by one until we fall off the viewRWorld. 1019 var yScale = canvasH / ctr.maxTotal; 1020 for (var seriesIndex = ctr.numSeries - 1; 1021 seriesIndex >= 0; seriesIndex--) { 1022 var colorId = ctr.seriesColors[seriesIndex]; 1023 ctx.fillStyle = pallette[colorId]; 1024 ctx.beginPath(); 1025 1026 // Set iLast and xLast such that the first sample we draw is the 1027 // startIndex sample. 1028 var iLast = startIndex - 1; 1029 var xLast = iLast >= 0 ? ctr.timestamps[iLast] - skipDistanceWorld : -1; 1030 var yLastView = canvasH; 1031 1032 // Iterate over samples from iLast onward until we either fall off the 1033 // viewRWorld or we run out of samples. To avoid drawing too much, after 1034 // drawing a sample at xLast, skip subsequent samples that are less than 1035 // skipDistanceWorld from xLast. 1036 var hasMoved = false; 1037 while (true) { 1038 var i = iLast + 1; 1039 if (i >= numSamples) { 1040 ctx.lineTo(xLast, yLastView); 1041 ctx.lineTo(xLast + 8 * pixWidth, yLastView); 1042 ctx.lineTo(xLast + 8 * pixWidth, canvasH); 1043 break; 1044 } 1045 1046 var x = ctr.timestamps[i]; 1047 1048 var y = ctr.totals[i * numSeries + seriesIndex]; 1049 var yView = canvasH - (yScale * y); 1050 1051 if (x > viewRWorld) { 1052 ctx.lineTo(x, yLastView); 1053 ctx.lineTo(x, canvasH); 1054 break; 1055 } 1056 1057 if (x - xLast < skipDistanceWorld) { 1058 iLast = i; 1059 continue; 1060 } 1061 1062 if (!hasMoved) { 1063 ctx.moveTo(viewLWorld, canvasH); 1064 hasMoved = true; 1065 } 1066 ctx.lineTo(x, yLastView); 1067 ctx.lineTo(x, yView); 1068 iLast = i; 1069 xLast = x; 1070 yLastView = yView; 1071 } 1072 ctx.closePath(); 1073 ctx.fill(); 1074 } 1075 ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 1076 for (var i in this.selectedSamples_) { 1077 if (!this.selectedSamples_[i]) 1078 continue; 1079 1080 var x = ctr.timestamps[i]; 1081 for (var seriesIndex = ctr.numSeries - 1; 1082 seriesIndex >= 0; seriesIndex--) { 1083 var y = ctr.totals[i * numSeries + seriesIndex]; 1084 var yView = canvasH - (yScale * y); 1085 ctx.fillRect(x - pixWidth, yView - 1, 3 * pixWidth, 3); 1086 } 1087 } 1088 ctx.restore(); 1089 }, 1090 1091 /** 1092 * Adds items intersecting a point to a selection. 1093 * @param {number} wX X location to search at, in worldspace. 1094 * @param {number} wY Y location to search at, in offset space. 1095 * offset space. 1096 * @param {TimelineSelection} selection Selection to which to add hits. 1097 * @return {boolean} true if a slice was found, otherwise false. 1098 */ 1099 addIntersectingItemsToSelection: function(wX, wY, selection) { 1100 var clientRect = this.getBoundingClientRect(); 1101 if (wY < clientRect.top || wY >= clientRect.bottom) 1102 return false; 1103 var ctr = this.counter_; 1104 if (wX < this.counter_.timestamps[0]) 1105 return false; 1106 var i = tracing.findLowIndexInSortedArray(ctr.timestamps, 1107 function(x) { return x; }, 1108 wX); 1109 if (i < 0 || i >= ctr.timestamps.length) 1110 return false; 1111 1112 // Sample i is going to either be exactly at wX or slightly above it, 1113 // E.g. asking for 7.5 in [7,8] gives i=1. So bump i back by 1 if needed. 1114 if (i > 0 && wX > this.counter_.timestamps[i - 1]) 1115 i--; 1116 1117 // Some preliminaries. 1118 var canvasH = this.getBoundingClientRect().height; 1119 var yScale = canvasH / ctr.maxTotal; 1120 1121 /* 1122 // Figure out which sample we hit 1123 var seriesIndexHit; 1124 for (var seriesIndex = 0; seriesIndex < ctr.numSeries; seriesIndex++) { 1125 var y = ctr.totals[i * ctr.numSeries + seriesIndex]; 1126 var yView = canvasH - (yScale * y) + clientRect.top; 1127 if (wY >= yView) { 1128 seriesIndexHit = seriesIndex; 1129 break; 1130 } 1131 } 1132 if (seriesIndexHit === undefined) 1133 return false; 1134 */ 1135 var hit = selection.addCounterSample(this, this.counter, i); 1136 this.decorateHit(hit); 1137 return true; 1138 }, 1139 1140 /** 1141 * Adds items intersecting the given range to a selection. 1142 * @param {number} loWX Lower X bound of the interval to search, in 1143 * worldspace. 1144 * @param {number} hiWX Upper X bound of the interval to search, in 1145 * worldspace. 1146 * @param {number} loY Lower Y bound of the interval to search, in 1147 * offset space. 1148 * @param {number} hiY Upper Y bound of the interval to search, in 1149 * offset space. 1150 * @param {TimelineSelection} selection Selection to which to add hits. 1151 */ 1152 addIntersectingItemsInRangeToSelection: function( 1153 loWX, hiWX, loY, hiY, selection) { 1154 1155 var clientRect = this.getBoundingClientRect(); 1156 var a = Math.max(loY, clientRect.top); 1157 var b = Math.min(hiY, clientRect.bottom); 1158 if (a > b) 1159 return; 1160 1161 var ctr = this.counter_; 1162 1163 var iLo = tracing.findLowIndexInSortedArray(ctr.timestamps, 1164 function(x) { return x; }, 1165 loWX); 1166 var iHi = tracing.findLowIndexInSortedArray(ctr.timestamps, 1167 function(x) { return x; }, 1168 hiWX); 1169 1170 // Sample i is going to either be exactly at wX or slightly above it, 1171 // E.g. asking for 7.5 in [7,8] gives i=1. So bump i back by 1 if needed. 1172 if (iLo > 0 && loWX > ctr.timestamps[iLo - 1]) 1173 iLo--; 1174 if (iHi > 0 && hiWX > ctr.timestamps[iHi - 1]) 1175 iHi--; 1176 1177 // Iterate over every sample intersecting.. 1178 for (var i = iLo; i <= iHi; i++) { 1179 if (i >= ctr.timestamps.length) 1180 continue; 1181 1182 // TODO(nduca): Pick the seriesIndexHit based on the loY - hiY values. 1183 var hit = selection.addCounterSample(this, this.counter, i); 1184 this.decorateHit(hit); 1185 } 1186 }, 1187 1188 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 1189 } 1190 1191 }; 1192 1193 return { 1194 TimelineCounterTrack: TimelineCounterTrack, 1195 TimelineSliceTrack: TimelineSliceTrack, 1196 TimelineThreadTrack: TimelineThreadTrack, 1197 TimelineViewportTrack: TimelineViewportTrack, 1198 TimelineCpuTrack: TimelineCpuTrack 1199 }; 1200 }); 1201