• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7base.requireStylesheet('tracks.slice_track');
8
9base.require('tracks.canvas_based_track');
10base.require('sorted_array_utils');
11base.require('fast_rect_renderer');
12base.require('color_scheme');
13base.require('ui');
14
15base.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 {CanvasBasedTrack}
23   */
24
25  var SliceTrack = tracing.ui.define(tracing.tracks.CanvasBasedTrack);
26
27  SliceTrack.prototype = {
28
29    __proto__: tracing.tracks.CanvasBasedTrack.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('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 {Selection} 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 {Selection} 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 {Selection} 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 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    SliceTrack: SliceTrack
403  };
404});
405