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('tracing.tracks.slice_track');
8
9base.require('base.sorted_array_utils');
10base.require('tracing.tracks.heading_track');
11base.require('tracing.fast_rect_renderer');
12base.require('tracing.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 {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