1// Copyright (c) 2011 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 */
11cr.define('gpu', function() {
12
13  const palletteBase = [
14    {r: 0x45, g: 0x85, b: 0xaa},
15    {r: 0xdc, g: 0x73, b: 0xa8},
16    {r: 0x77, g: 0xb6, b: 0x94},
17    {r: 0x23, g: 0xae, b: 0x6e},
18    {r: 0x76, g: 0x5d, b: 0x9e},
19    {r: 0x48, g: 0xd8, b: 0xfb},
20    {r: 0xa9, g: 0xd7, b: 0x93},
21    {r: 0x7c, g: 0x2d, b: 0x52},
22    {r: 0x69, g: 0xc2, b: 0x75},
23    {r: 0x76, g: 0xcf, b: 0xee},
24    {r: 0x3d, g: 0x85, b: 0xd1},
25    {r: 0x71, g: 0x0b, b: 0x54}];
26
27  function brighten(c) {
28    return {r: Math.min(255, c.r + Math.floor(c.r * 0.45)),
29      g: Math.min(255, c.g + Math.floor(c.g * 0.45)),
30      b: Math.min(255, c.b + Math.floor(c.b * 0.45))};
31  }
32  function colorToString(c) {
33    return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')';
34  }
35
36  const selectedIdBoost = palletteBase.length;
37
38  const pallette = palletteBase.concat(palletteBase.map(brighten)).
39      map(colorToString);
40
41  var textWidthMap = { };
42  function quickMeasureText(ctx, text) {
43    var w = textWidthMap[text];
44    if (!w) {
45      w = ctx.measureText(text).width;
46      textWidthMap[text] = w;
47    }
48    return w;
49  }
50
51  /**
52   * Generic base class for timeline tracks
53   */
54  TimelineThreadTrack = cr.ui.define('div');
55  TimelineThreadTrack.prototype = {
56    __proto__: HTMLDivElement.prototype,
57
58    decorate: function() {
59      this.className = 'timeline-thread-track';
60    },
61
62    set thread(thread) {
63      this.thread_ = thread;
64      this.updateChildTracks_();
65    },
66
67    set viewport(v) {
68      this.viewport_ = v;
69      for (var i = 0; i < this.tracks_.length; i++)
70        this.tracks_[i].viewport = v;
71      this.invalidate();
72    },
73
74    invalidate: function() {
75      if (this.parentNode)
76        this.parentNode.invalidate();
77    },
78
79    onResize: function() {
80      for (var i = 0; i < this.tracks_.length; i++)
81        this.tracks_[i].onResize();
82    },
83
84    get firstCanvas() {
85      if (this.tracks_.length)
86        return this.tracks_[0].firstCanvas;
87      return undefined;
88    },
89
90    redraw: function() {
91      for (var i = 0; i < this.tracks_.length; i++)
92        this.tracks_[i].redraw();
93    },
94
95    updateChildTracks_: function() {
96      this.textContent = '';
97      this.tracks_ = [];
98      if (this.thread_) {
99        for (var srI = 0; srI < this.thread_.subRows.length; ++srI) {
100          var track = new TimelineSliceTrack();
101
102          if (srI == 0)
103            track.heading = this.thread_.parent.pid + ': ' +
104                this.thread_.tid + ': ';
105          else
106            track.heading = '';
107          track.slices = this.thread_.subRows[srI];
108          track.viewport = this.viewport_;
109
110          this.tracks_.push(track);
111          this.appendChild(track);
112        }
113      }
114    },
115
116    /**
117     * Picks a slice, if any, at a given location.
118     * @param {number} wX X location to search at, in worldspace.
119     * @param {number} wY Y location to search at, in offset space.
120     *     offset space.
121     * @param {function():*} onHitCallback Callback to call with the slice,
122     *     if one is found.
123     * @return {boolean} true if a slice was found, otherwise false.
124     */
125    pick: function(wX, wY, onHitCallback) {
126      for (var i = 0; i < this.tracks_.length; i++) {
127        var track = this.tracks_[i];
128        if (wY >= track.offsetTop && wY < track.offsetTop + track.offsetHeight)
129          return track.pick(wX, onHitCallback);
130      }
131      return false;
132    },
133
134    /**
135     * Finds slices intersecting the given interval.
136     * @param {number} loWX Lower X bound of the interval to search, in
137     *     worldspace.
138     * @param {number} hiWX Upper X bound of the interval to search, in
139     *     worldspace.
140     * @param {number} loY Lower Y bound of the interval to search, in
141     *     offset space.
142     * @param {number} hiY Upper Y bound of the interval to search, in
143     *     offset space.
144     * @param {function():*} onHitCallback Function to call for each slice
145     *     intersecting the interval.
146     */
147    pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) {
148      for (var i = 0; i < this.tracks_.length; i++) {
149        var a = Math.max(loY, this.tracks_[i].offsetTop);
150        var b = Math.min(hiY, this.tracks_[i].offsetTop +
151                         this.tracks_[i].offsetHeight);
152        if (a <= b)
153          this.tracks_[i].pickRange(loWX, hiWX, loY, hiY, onHitCallback);
154      }
155    }
156  };
157
158  /**
159   * Creates a new timeline track div element
160   * @constructor
161   * @extends {HTMLDivElement}
162   */
163  TimelineSliceTrack = cr.ui.define('div');
164
165  TimelineSliceTrack.prototype = {
166    __proto__: HTMLDivElement.prototype,
167
168    decorate: function() {
169      this.className = 'timeline-slice-track';
170      this.slices_ = null;
171
172      this.titleDiv_ = document.createElement('div');
173      this.titleDiv_.className = 'timeline-slice-track-title';
174      this.appendChild(this.titleDiv_);
175
176      this.canvasContainer_ = document.createElement('div');
177      this.canvasContainer_.className = 'timeline-slice-track-canvas-container';
178      this.appendChild(this.canvasContainer_);
179      this.canvas_ = document.createElement('canvas');
180      this.canvas_.className = 'timeline-slice-track-canvas';
181      this.canvasContainer_.appendChild(this.canvas_);
182
183      this.ctx_ = this.canvas_.getContext('2d');
184    },
185
186    set heading(text) {
187      this.titleDiv_.textContent = text;
188    },
189
190    set slices(slices) {
191      this.slices_ = slices;
192      this.invalidate();
193    },
194
195    set viewport(v) {
196      this.viewport_ = v;
197      this.invalidate();
198    },
199
200    invalidate: function() {
201      if (this.parentNode)
202        this.parentNode.invalidate();
203    },
204
205    get firstCanvas() {
206      return this.canvas_;
207    },
208
209    onResize: function() {
210      this.canvas_.width = this.canvasContainer_.clientWidth;
211      this.canvas_.height = this.canvasContainer_.clientHeight;
212      this.invalidate();
213    },
214
215    redraw: function() {
216      if (!this.viewport_)
217        return;
218      var ctx = this.ctx_;
219      var canvasW = this.canvas_.width;
220      var canvasH = this.canvas_.height;
221
222      ctx.clearRect(0, 0, canvasW, canvasH);
223
224      // culling...
225      var vp = this.viewport_;
226      var pixWidth = vp.xViewVectorToWorld(1);
227      var viewLWorld = vp.xViewToWorld(0);
228      var viewRWorld = vp.xViewToWorld(this.width);
229
230      // begin rendering in world space
231      ctx.save();
232      vp.applyTransformToCanavs(ctx);
233
234      // tracks
235      var tr = new gpu.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth,
236                                        2 * pixWidth, viewRWorld, pallette);
237      tr.setYandH(0, canvasH);
238      var slices = this.slices_;
239      for (var i = 0; i < slices.length; ++i) {
240        var slice = slices[i];
241        var x = slice.start;
242        var w = slice.duration;
243        var colorId;
244        colorId = slice.selected ?
245            slice.colorId + selectedIdBoost :
246            slice.colorId;
247
248        if (w < pixWidth)
249          w = pixWidth;
250        tr.fillRect(x, w, colorId);
251      }
252      tr.flush();
253      ctx.restore();
254
255      // labels
256      ctx.textAlign = 'center';
257      ctx.textBaseline = 'top';
258      ctx.font = '10px sans-serif';
259      ctx.strokeStyle = 'rgb(0,0,0)';
260      ctx.fillStyle = 'rgb(0,0,0)';
261      var quickDiscardThresshold = pixWidth * 20; // dont render until 20px wide
262      for (var i = 0; i < slices.length; ++i) {
263        var slice = slices[i];
264        if (slice.duration > quickDiscardThresshold) {
265          var labelWidth = quickMeasureText(ctx, slice.title) + 2;
266          var labelWidthWorld = pixWidth * labelWidth;
267          if (labelWidthWorld < slice.duration) {
268            var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration);
269            ctx.fillText(slice.title, cX, 2.5);
270          }
271        }
272      }
273    },
274
275    /**
276     * Picks a slice, if any, at a given location.
277     * @param {number} wX X location to search at, in worldspace.
278     * @param {number} wY Y location to search at, in offset space.
279     *     offset space.
280     * @param {function():*} onHitCallback Callback to call with the slice,
281     *     if one is found.
282     * @return {boolean} true if a slice was found, otherwise false.
283     */
284    pick: function(wX, wY, onHitCallback) {
285      if (wY < this.offsetTop || wY >= this.offsetTop + this.offsetHeight)
286        return false;
287      var x = gpu.findLowIndexInSortedIntervals(this.slices_,
288          function(x) { return x.start; },
289          function(x) { return x.duration; },
290          wX);
291      if (x >= 0 && x < this.slices_.length) {
292        onHitCallback('slice', this, this.slices_[x]);
293        return true;
294      }
295      return false;
296    },
297
298    /**
299     * Finds slices intersecting the given interval.
300     * @param {number} loWX Lower X bound of the interval to search, in
301     *     worldspace.
302     * @param {number} hiWX Upper X bound of the interval to search, in
303     *     worldspace.
304     * @param {number} loY Lower Y bound of the interval to search, in
305     *     offset space.
306     * @param {number} hiY Upper Y bound of the interval to search, in
307     *     offset space.
308     * @param {function():*} onHitCallback Function to call for each slice
309     *     intersecting the interval.
310     */
311    pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) {
312      var a = Math.max(loY, this.offsetTop);
313      var b = Math.min(hiY, this.offsetTop + this.offsetHeight);
314      if (a > b)
315        return;
316
317      function onPickHit(slice) {
318        onHitCallback('slice', this, slice);
319      }
320      gpu.iterateOverIntersectingIntervals(this.slices_,
321          function(x) { return x.start; },
322          function(x) { return x.duration; },
323          loWX, hiWX,
324          onPickHit);
325    }
326
327  };
328
329  return {
330    TimelineSliceTrack: TimelineSliceTrack,
331    TimelineThreadTrack: TimelineThreadTrack
332  };
333});
334