Home | History | Annotate | Download | only in tracing
      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); // &times;
    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