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('tracing.tracks.slice_track');
      8 
      9 base.require('base.sorted_array_utils');
     10 base.require('tracing.tracks.heading_track');
     11 base.require('tracing.fast_rect_renderer');
     12 base.require('tracing.color_scheme');
     13 base.require('ui');
     14 
     15 base.exportTo('tracing.tracks', function() {
     16 
     17   var palette = tracing.getColorPalette();
     18 
     19   /**
     20    * A track that displays an array of Slice objects.
     21    * @constructor
     22    * @extends {HeadingTrack}
     23    */
     24 
     25   var SliceTrack = ui.define(
     26       'slice-track', tracing.tracks.HeadingTrack);
     27 
     28   SliceTrack.prototype = {
     29 
     30     __proto__: tracing.tracks.HeadingTrack.prototype,
     31 
     32     /**
     33      * Should we elide text on trace labels?
     34      * Without eliding, text that is too wide isn't drawn at all.
     35      * Disable if you feel this causes a performance problem.
     36      * This is a default value that can be overridden in tracks for testing.
     37      * @const
     38      */
     39     SHOULD_ELIDE_TEXT: true,
     40 
     41     decorate: function(viewport) {
     42       tracing.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
     43       this.classList.add('slice-track');
     44       this.elidedTitleCache = new ElidedTitleCache();
     45       this.asyncStyle_ = false;
     46       this.slices_ = null;
     47     },
     48 
     49     get asyncStyle() {
     50       return this.asyncStyle_;
     51     },
     52 
     53     set asyncStyle(v) {
     54       this.asyncStyle_ = !!v;
     55     },
     56 
     57     get slices() {
     58       return this.slices_;
     59     },
     60 
     61     set slices(slices) {
     62       this.slices_ = slices || [];
     63     },
     64 
     65     get height() {
     66       return window.getComputedStyle(this).height;
     67     },
     68 
     69     set height(height) {
     70       this.style.height = height;
     71     },
     72 
     73     get hasVisibleContent() {
     74       return this.slices.length > 0;
     75     },
     76 
     77     labelWidth: function(title) {
     78       return quickMeasureText(this.context(), title) + 2;
     79     },
     80 
     81     labelWidthWorld: function(title, pixWidth) {
     82       return this.labelWidth(title) * pixWidth;
     83     },
     84 
     85     draw: function(type, viewLWorld, viewRWorld) {
     86       switch (type) {
     87         case tracing.tracks.DrawType.SLICE:
     88           this.drawSlices_(viewLWorld, viewRWorld);
     89           break;
     90       }
     91     },
     92 
     93     drawSlices_: function(viewLWorld, viewRWorld) {
     94       var ctx = this.context();
     95       var pixelRatio = window.devicePixelRatio || 1;
     96 
     97       var bounds = this.getBoundingClientRect();
     98       var height = bounds.height * pixelRatio;
     99 
    100       // Culling parameters.
    101       var vp = this.viewport;
    102       var pixWidth = vp.xViewVectorToWorld(1);
    103 
    104       // Begin rendering in world space.
    105       ctx.save();
    106       vp.applyTransformToCanvas(ctx);
    107 
    108       // Slices.
    109       if (this.asyncStyle_)
    110         ctx.globalAlpha = 0.25;
    111       var tr = new tracing.FastRectRenderer(ctx, 2 * pixWidth, 2 * pixWidth,
    112                                             palette);
    113       tr.setYandH(0, height);
    114       var slices = this.slices_;
    115       var lowSlice = base.findLowIndexInSortedArray(
    116           slices,
    117           function(slice) { return slice.start + slice.duration; },
    118           viewLWorld);
    119 
    120       for (var i = lowSlice; i < slices.length; ++i) {
    121         var slice = slices[i];
    122         var x = slice.start;
    123         if (x > viewRWorld)
    124           break;
    125 
    126         // Less than 0.001 causes short events to disappear when zoomed in.
    127         var w = Math.max(slice.duration, 0.001);
    128         var colorId = slice.selected ?
    129             slice.colorId + highlightIdBoost :
    130             slice.colorId;
    131 
    132         if (w < pixWidth)
    133           w = pixWidth;
    134         if (slice.duration > 0) {
    135           tr.fillRect(x, w, colorId);
    136         } else {
    137           // Instant: draw a triangle.  If zoomed too far, collapse
    138           // into the FastRectRenderer.
    139           if (pixWidth > 0.001) {
    140             tr.fillRect(x, pixWidth, colorId);
    141           } else {
    142             ctx.fillStyle = palette[colorId];
    143             ctx.beginPath();
    144             ctx.moveTo(x - (4 * pixWidth), height);
    145             ctx.lineTo(x, 0);
    146             ctx.lineTo(x + (4 * pixWidth), height);
    147             ctx.closePath();
    148             ctx.fill();
    149           }
    150         }
    151       }
    152       tr.flush();
    153       ctx.restore();
    154 
    155       // Labels.
    156       if (height > 8) {
    157         ctx.textAlign = 'center';
    158         ctx.textBaseline = 'top';
    159         ctx.font = (10 * pixelRatio) + 'px sans-serif';
    160         ctx.strokeStyle = 'rgb(0,0,0)';
    161         ctx.fillStyle = 'rgb(0,0,0)';
    162 
    163         // Don't render text until until it is 20px wide
    164         var quickDiscardThresshold = pixWidth * 20;
    165         var shouldElide = this.SHOULD_ELIDE_TEXT;
    166         for (var i = lowSlice; i < slices.length; ++i) {
    167           var slice = slices[i];
    168           if (slice.start > viewRWorld)
    169             break;
    170 
    171           if (slice.duration <= quickDiscardThresshold)
    172             continue;
    173 
    174           var title = slice.title +
    175               (slice.didNotFinish ? ' (Did Not Finish)' : '');
    176 
    177           var drawnTitle = title;
    178           var drawnWidth = this.labelWidth(drawnTitle);
    179           if (shouldElide &&
    180               this.labelWidthWorld(drawnTitle, pixWidth) > slice.duration) {
    181             var elidedValues = this.elidedTitleCache.get(
    182                 this, pixWidth,
    183                 drawnTitle, drawnWidth,
    184                 slice.duration);
    185             drawnTitle = elidedValues.string;
    186             drawnWidth = elidedValues.width;
    187           }
    188 
    189           if (drawnWidth * pixWidth < slice.duration) {
    190 
    191             var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration);
    192             ctx.fillText(drawnTitle, cX, 2.5 * pixelRatio, drawnWidth);
    193           }
    194         }
    195       }
    196     },
    197 
    198     addIntersectingItemsInRangeToSelectionInWorldSpace: function(
    199         loWX, hiWX, viewPixWidthWorld, selection) {
    200       function onPickHit(slice) {
    201         var hit = selection.addSlice(this, slice);
    202         this.decorateHit(hit);
    203       }
    204       base.iterateOverIntersectingIntervals(this.slices_,
    205           function(x) { return x.start; },
    206           function(x) { return x.duration; },
    207           loWX, hiWX,
    208           onPickHit.bind(this));
    209     },
    210 
    211     /**
    212      * Find the index for the given slice.
    213      * @return {index} Index of the given slice, or undefined.
    214      * @private
    215      */
    216     indexOfSlice_: function(slice) {
    217       var index = base.findLowIndexInSortedArray(this.slices_,
    218           function(x) { return x.start; },
    219           slice.start);
    220       while (index < this.slices_.length &&
    221           slice.start == this.slices_[index].start &&
    222           slice.colorId != this.slices_[index].colorId) {
    223         index++;
    224       }
    225       return index < this.slices_.length ? index : undefined;
    226     },
    227 
    228     /**
    229      * Add the item to the left or right of the provided hit, if any, to the
    230      * selection.
    231      * @param {slice} The current slice.
    232      * @param {Number} offset Number of slices away from the hit to look.
    233      * @param {Selection} selection The selection to add a hit to,
    234      * if found.
    235      * @return {boolean} Whether a hit was found.
    236      * @private
    237      */
    238     addItemNearToProvidedHitToSelection: function(hit, offset, selection) {
    239       if (!hit.slice)
    240         return false;
    241 
    242       var index = this.indexOfSlice_(hit.slice);
    243       if (index === undefined)
    244         return false;
    245 
    246       var newIndex = index + offset;
    247       if (newIndex < 0 || newIndex >= this.slices_.length)
    248         return false;
    249 
    250       var hit = selection.addSlice(this, this.slices_[newIndex]);
    251       this.decorateHit(hit);
    252       return true;
    253     },
    254 
    255     addAllObjectsMatchingFilterToSelection: function(filter, selection) {
    256       for (var i = 0; i < this.slices_.length; ++i) {
    257         if (filter.matchSlice(this.slices_[i])) {
    258           var hit = selection.addSlice(this, this.slices_[i]);
    259           this.decorateHit(hit);
    260         }
    261       }
    262     }
    263   };
    264 
    265   var highlightIdBoost = tracing.getColorPaletteHighlightIdBoost();
    266 
    267   // TODO(jrg): possibly obsoleted with the elided string cache.
    268   // Consider removing.
    269   var textWidthMap = { };
    270   function quickMeasureText(ctx, text) {
    271     var w = textWidthMap[text];
    272     if (!w) {
    273       w = ctx.measureText(text).width;
    274       textWidthMap[text] = w;
    275     }
    276     return w;
    277   }
    278 
    279   /**
    280    * Cache for elided strings.
    281    * Moved from the ElidedTitleCache protoype to a "global" for speed
    282    * (variable reference is 100x faster).
    283    *   key: String we wish to elide.
    284    *   value: Another dict whose key is width
    285    *     and value is an ElidedStringWidthPair.
    286    */
    287   var elidedTitleCacheDict = {};
    288 
    289   /**
    290    * A cache for elided strings.
    291    * @constructor
    292    */
    293   function ElidedTitleCache() {
    294   }
    295 
    296   ElidedTitleCache.prototype = {
    297     /**
    298      * Return elided text.
    299      * @param {track} A slice track or other object that defines
    300      *                functions labelWidth() and labelWidthWorld().
    301      * @param {pixWidth} Pixel width.
    302      * @param {title} Original title text.
    303      * @param {width} Drawn width in world coords.
    304      * @param {sliceDuration} Where the title must fit (in world coords).
    305      * @return {ElidedStringWidthPair} Elided string and width.
    306      */
    307     get: function(track, pixWidth, title, width, sliceDuration) {
    308       var elidedDict = elidedTitleCacheDict[title];
    309       if (!elidedDict) {
    310         elidedDict = {};
    311         elidedTitleCacheDict[title] = elidedDict;
    312       }
    313       var elidedDictForPixWidth = elidedDict[pixWidth];
    314       if (!elidedDictForPixWidth) {
    315         elidedDict[pixWidth] = {};
    316         elidedDictForPixWidth = elidedDict[pixWidth];
    317       }
    318       var stringWidthPair = elidedDictForPixWidth[sliceDuration];
    319       if (stringWidthPair === undefined) {
    320         var newtitle = title;
    321         var elided = false;
    322         while (track.labelWidthWorld(newtitle, pixWidth) > sliceDuration) {
    323           if (newtitle.length * 0.75 < 1)
    324             break;
    325           newtitle = newtitle.substring(0, newtitle.length * 0.75);
    326           elided = true;
    327         }
    328         if (elided && newtitle.length > 3)
    329           newtitle = newtitle.substring(0, newtitle.length - 3) + '...';
    330         stringWidthPair = new ElidedStringWidthPair(
    331             newtitle,
    332             track.labelWidth(newtitle));
    333         elidedDictForPixWidth[sliceDuration] = stringWidthPair;
    334       }
    335       return stringWidthPair;
    336     }
    337   };
    338 
    339   /**
    340    * A pair representing an elided string and world-coordinate width
    341    * to draw it.
    342    * @constructor
    343    */
    344   function ElidedStringWidthPair(string, width) {
    345     this.string = string;
    346     this.width = width;
    347   }
    348 
    349   return {
    350     SliceTrack: SliceTrack
    351   };
    352 });
    353