Home | History | Annotate | Download | only in tracks
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 base.requireStylesheet('tracks.timeline_slice_track');
      8 
      9 base.require('tracks.timeline_canvas_based_track');
     10 base.require('sorted_array_utils');
     11 base.require('fast_rect_renderer');
     12 base.require('timeline_color_scheme');
     13 base.require('ui');
     14 
     15 base.exportTo('tracks', function() {
     16 
     17   var palette = tracing.getColorPalette();
     18 
     19   /**
     20    * A track that displays an array of TimelineSlice objects.
     21    * @constructor
     22    * @extends {CanvasBasedTrack}
     23    */
     24 
     25   var TimelineSliceTrack = base.ui.define(tracks.TimelineCanvasBasedTrack);
     26 
     27   TimelineSliceTrack.prototype = {
     28 
     29     __proto__: tracks.TimelineCanvasBasedTrack.prototype,
     30 
     31     /**
     32      * Should we elide text on trace labels?
     33      * Without eliding, text that is too wide isn't drawn at all.
     34      * Disable if you feel this causes a performance problem.
     35      * This is a default value that can be overridden in tracks for testing.
     36      * @const
     37      */
     38     SHOULD_ELIDE_TEXT: true,
     39 
     40     decorate: function() {
     41       this.classList.add('timeline-slice-track');
     42       this.elidedTitleCache = new ElidedTitleCache();
     43       this.asyncStyle_ = false;
     44     },
     45 
     46     /**
     47      * Called by all the addToSelection functions on the created selection
     48      * hit objects. Override this function on parent classes to add
     49      * context-specific information to the hit.
     50      */
     51     decorateHit: function(hit) {
     52     },
     53 
     54     get asyncStyle() {
     55       return this.asyncStyle_;
     56     },
     57 
     58     set asyncStyle(v) {
     59       this.asyncStyle_ = !!v;
     60       this.invalidate();
     61     },
     62 
     63     get slices() {
     64       return this.slices_;
     65     },
     66 
     67     set slices(slices) {
     68       this.slices_ = slices || [];
     69       if (!slices)
     70         this.visible = false;
     71       this.invalidate();
     72     },
     73 
     74     get height() {
     75       return window.getComputedStyle(this).height;
     76     },
     77 
     78     set height(height) {
     79       this.style.height = height;
     80       this.invalidate();
     81     },
     82 
     83     labelWidth: function(title) {
     84       return quickMeasureText(this.ctx_, title) + 2;
     85     },
     86 
     87     labelWidthWorld: function(title, pixWidth) {
     88       return this.labelWidth(title) * pixWidth;
     89     },
     90 
     91     redraw: function() {
     92       var ctx = this.ctx_;
     93       var canvasW = this.canvas_.width;
     94       var canvasH = this.canvas_.height;
     95 
     96       ctx.clearRect(0, 0, canvasW, canvasH);
     97 
     98       // Culling parameters.
     99       var vp = this.viewport_;
    100       var pixWidth = vp.xViewVectorToWorld(1);
    101       var viewLWorld = vp.xViewToWorld(0);
    102       var viewRWorld = vp.xViewToWorld(canvasW);
    103 
    104       // Give the viewport a chance to draw onto this canvas.
    105       vp.drawUnderContent(ctx, viewLWorld, viewRWorld, canvasH);
    106 
    107       // Begin rendering in world space.
    108       ctx.save();
    109       vp.applyTransformToCanvas(ctx);
    110 
    111       // Slices.
    112       if (this.asyncStyle_)
    113         ctx.globalAlpha = 0.25;
    114       var tr = new tracing.FastRectRenderer(ctx, 2 * pixWidth, 2 * pixWidth,
    115                                             palette);
    116       tr.setYandH(0, canvasH);
    117       var slices = this.slices_;
    118       var lowSlice = tracing.findLowIndexInSortedArray(slices,
    119                                                        function(slice) {
    120                                                          return slice.start +
    121                                                                 slice.duration;
    122                                                        },
    123                                                        viewLWorld);
    124       for (var i = lowSlice; i < slices.length; ++i) {
    125         var slice = slices[i];
    126         var x = slice.start;
    127         if (x > viewRWorld) {
    128           break;
    129         }
    130         // Less than 0.001 causes short events to disappear when zoomed in.
    131         var w = Math.max(slice.duration, 0.001);
    132         var colorId = slice.selected ?
    133             slice.colorId + highlightIdBoost :
    134             slice.colorId;
    135 
    136         if (w < pixWidth)
    137           w = pixWidth;
    138         if (slice.duration > 0) {
    139           tr.fillRect(x, w, colorId);
    140         } else {
    141           // Instant: draw a triangle.  If zoomed too far, collapse
    142           // into the FastRectRenderer.
    143           if (pixWidth > 0.001) {
    144             tr.fillRect(x, pixWidth, colorId);
    145           } else {
    146             ctx.fillStyle = palette[colorId];
    147             ctx.beginPath();
    148             ctx.moveTo(x - (4 * pixWidth), canvasH);
    149             ctx.lineTo(x, 0);
    150             ctx.lineTo(x + (4 * pixWidth), canvasH);
    151             ctx.closePath();
    152             ctx.fill();
    153           }
    154         }
    155       }
    156       tr.flush();
    157       ctx.restore();
    158 
    159       // Labels.
    160       var pixelRatio = window.devicePixelRatio || 1;
    161       if (canvasH > 8) {
    162         ctx.textAlign = 'center';
    163         ctx.textBaseline = 'top';
    164         ctx.font = (10 * pixelRatio) + 'px sans-serif';
    165         ctx.strokeStyle = 'rgb(0,0,0)';
    166         ctx.fillStyle = 'rgb(0,0,0)';
    167         // Don't render text until until it is 20px wide
    168         var quickDiscardThresshold = pixWidth * 20;
    169         var shouldElide = this.SHOULD_ELIDE_TEXT;
    170         for (var i = lowSlice; i < slices.length; ++i) {
    171           var slice = slices[i];
    172           if (slice.start > viewRWorld) {
    173             break;
    174           }
    175           if (slice.duration > quickDiscardThresshold) {
    176             var title = slice.title;
    177             if (slice.didNotFinish) {
    178               title += ' (Did Not Finish)';
    179             }
    180             var drawnTitle = title;
    181             var drawnWidth = this.labelWidth(drawnTitle);
    182             if (shouldElide &&
    183                 this.labelWidthWorld(drawnTitle, pixWidth) > slice.duration) {
    184               var elidedValues = this.elidedTitleCache.get(
    185                   this, pixWidth,
    186                   drawnTitle, drawnWidth,
    187                   slice.duration);
    188               drawnTitle = elidedValues.string;
    189               drawnWidth = elidedValues.width;
    190             }
    191             if (drawnWidth * pixWidth < slice.duration) {
    192               var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration);
    193               ctx.fillText(drawnTitle, cX, 2.5 * pixelRatio, drawnWidth);
    194             }
    195           }
    196         }
    197       }
    198 
    199       // Give the viewport a chance to draw over this canvas.
    200       vp.drawOverContent(ctx, viewLWorld, viewRWorld, canvasH);
    201     },
    202 
    203     /**
    204      * Finds slices intersecting the given interval.
    205      * @param {number} vX X location to search at, in viewspace.
    206      * @param {number} vY Y location to search at, in viewspace.
    207      * @param {TimelineSelection} selection Selection to which to add hits.
    208      * @return {boolean} true if a slice was found, otherwise false.
    209      */
    210     addIntersectingItemsToSelection: function(vX, vY, selection) {
    211       var clientRect = this.getBoundingClientRect();
    212       if (vY < clientRect.top || vY >= clientRect.bottom)
    213         return false;
    214       var pixelRatio = window.devicePixelRatio || 1;
    215       var wX = this.viewport_.xViewVectorToWorld(vX * devicePixelRatio);
    216       var x = tracing.findLowIndexInSortedIntervals(this.slices_,
    217           function(x) { return x.start; },
    218           function(x) { return x.duration; },
    219           wX);
    220       if (x >= 0 && x < this.slices_.length) {
    221         var hit = selection.addSlice(this, this.slices_[x]);
    222         this.decorateHit(hit);
    223         return true;
    224       }
    225       return false;
    226     },
    227 
    228     /**
    229      * Adds items intersecting the given range to a selection.
    230      * @param {number} loVX Lower X bound of the interval to search, in
    231      *     viewspace.
    232      * @param {number} hiVX Upper X bound of the interval to search, in
    233      *     viewspace.
    234      * @param {number} loVY Lower Y bound of the interval to search, in
    235      *     viewspace.
    236      * @param {number} hiVY Upper Y bound of the interval to search, in
    237      *     viewspace.
    238      * @param {TimelineSelection} selection Selection to which to add hits.
    239      */
    240     addIntersectingItemsInRangeToSelection: function(
    241         loVX, hiVX, loVY, hiVY, selection) {
    242 
    243       var pixelRatio = window.devicePixelRatio || 1;
    244       var loWX = this.viewport_.xViewToWorld(loVX * pixelRatio);
    245       var hiWX = this.viewport_.xViewToWorld(hiVX * pixelRatio);
    246 
    247       var clientRect = this.getBoundingClientRect();
    248       var a = Math.max(loVY, clientRect.top);
    249       var b = Math.min(hiVY, clientRect.bottom);
    250       if (a > b)
    251         return;
    252 
    253       var that = this;
    254       function onPickHit(slice) {
    255         var hit = selection.addSlice(that, slice);
    256         that.decorateHit(hit);
    257       }
    258       tracing.iterateOverIntersectingIntervals(this.slices_,
    259           function(x) { return x.start; },
    260           function(x) { return x.duration; },
    261           loWX, hiWX,
    262           onPickHit);
    263     },
    264 
    265     /**
    266      * Find the index for the given slice.
    267      * @return {index} Index of the given slice, or undefined.
    268      * @private
    269      */
    270     indexOfSlice_: function(slice) {
    271       var index = tracing.findLowIndexInSortedArray(this.slices_,
    272           function(x) { return x.start; },
    273           slice.start);
    274       while (index < this.slices_.length &&
    275           slice.start == this.slices_[index].start &&
    276           slice.colorId != this.slices_[index].colorId) {
    277         index++;
    278       }
    279       return index < this.slices_.length ? index : undefined;
    280     },
    281 
    282     /**
    283      * Add the item to the left or right of the provided hit, if any, to the
    284      * selection.
    285      * @param {slice} The current slice.
    286      * @param {Number} offset Number of slices away from the hit to look.
    287      * @param {TimelineSelection} selection The selection to add a hit to,
    288      * if found.
    289      * @return {boolean} Whether a hit was found.
    290      * @private
    291      */
    292     addItemNearToProvidedHitToSelection: function(hit, offset, selection) {
    293       if (!hit.slice)
    294         return false;
    295 
    296       var index = this.indexOfSlice_(hit.slice);
    297       if (index === undefined)
    298         return false;
    299 
    300       var newIndex = index + offset;
    301       if (newIndex < 0 || newIndex >= this.slices_.length)
    302         return false;
    303 
    304       var hit = selection.addSlice(this, this.slices_[newIndex]);
    305       this.decorateHit(hit);
    306       return true;
    307     },
    308 
    309     addAllObjectsMatchingFilterToSelection: function(filter, selection) {
    310       for (var i = 0; i < this.slices_.length; ++i) {
    311         if (filter.matchSlice(this.slices_[i])) {
    312           var hit = selection.addSlice(this, this.slices_[i]);
    313           this.decorateHit(hit);
    314         }
    315       }
    316     }
    317   };
    318 
    319   var highlightIdBoost = tracing.getColorPaletteHighlightIdBoost();
    320 
    321   // TODO(jrg): possibly obsoleted with the elided string cache.
    322   // Consider removing.
    323   var textWidthMap = { };
    324   function quickMeasureText(ctx, text) {
    325     var w = textWidthMap[text];
    326     if (!w) {
    327       w = ctx.measureText(text).width;
    328       textWidthMap[text] = w;
    329     }
    330     return w;
    331   }
    332 
    333   /**
    334    * Cache for elided strings.
    335    * Moved from the ElidedTitleCache protoype to a "global" for speed
    336    * (variable reference is 100x faster).
    337    *   key: String we wish to elide.
    338    *   value: Another dict whose key is width
    339    *     and value is an ElidedStringWidthPair.
    340    */
    341   var elidedTitleCacheDict = {};
    342 
    343   /**
    344    * A cache for elided strings.
    345    * @constructor
    346    */
    347   function ElidedTitleCache() {
    348   }
    349 
    350   ElidedTitleCache.prototype = {
    351     /**
    352      * Return elided text.
    353      * @param {track} A timeline slice track or other object that defines
    354      *                functions labelWidth() and labelWidthWorld().
    355      * @param {pixWidth} Pixel width.
    356      * @param {title} Original title text.
    357      * @param {width} Drawn width in world coords.
    358      * @param {sliceDuration} Where the title must fit (in world coords).
    359      * @return {ElidedStringWidthPair} Elided string and width.
    360      */
    361     get: function(track, pixWidth, title, width, sliceDuration) {
    362       var elidedDict = elidedTitleCacheDict[title];
    363       if (!elidedDict) {
    364         elidedDict = {};
    365         elidedTitleCacheDict[title] = elidedDict;
    366       }
    367       var elidedDictForPixWidth = elidedDict[pixWidth];
    368       if (!elidedDictForPixWidth) {
    369         elidedDict[pixWidth] = {};
    370         elidedDictForPixWidth = elidedDict[pixWidth];
    371       }
    372       var stringWidthPair = elidedDictForPixWidth[sliceDuration];
    373       if (stringWidthPair === undefined) {
    374         var newtitle = title;
    375         var elided = false;
    376         while (track.labelWidthWorld(newtitle, pixWidth) > sliceDuration) {
    377           newtitle = newtitle.substring(0, newtitle.length * 0.75);
    378           elided = true;
    379         }
    380         if (elided && newtitle.length > 3)
    381           newtitle = newtitle.substring(0, newtitle.length - 3) + '...';
    382         stringWidthPair = new ElidedStringWidthPair(
    383             newtitle,
    384             track.labelWidth(newtitle));
    385         elidedDictForPixWidth[sliceDuration] = stringWidthPair;
    386       }
    387       return stringWidthPair;
    388     }
    389   };
    390 
    391   /**
    392    * A pair representing an elided string and world-coordinate width
    393    * to draw it.
    394    * @constructor
    395    */
    396   function ElidedStringWidthPair(string, width) {
    397     this.string = string;
    398     this.width = width;
    399   }
    400 
    401   return {
    402     TimelineSliceTrack: TimelineSliceTrack
    403   };
    404 });
    405