OLD | NEW |
| (Empty) |
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 */ | |
11 cr.define('gpu', function() { | |
12 | |
13 const palletteBase = [ | |
14 {r: 138, g: 113, b: 152}, | |
15 {r: 175, g: 112, b: 133}, | |
16 {r: 127, g: 135, b: 225}, | |
17 {r: 93, g: 81, b: 137}, | |
18 {r: 116, g: 143, b: 119}, | |
19 {r: 178, g: 214, b: 122}, | |
20 {r: 87, g: 109, b: 147}, | |
21 {r: 119, g: 155, b: 95}, | |
22 {r: 114, g: 180, b: 160}, | |
23 {r: 132, g: 85, b: 103}, | |
24 {r: 157, g: 210, b: 150}, | |
25 {r: 148, g: 94, b: 86}, | |
26 {r: 164, g: 108, b: 138}, | |
27 {r: 139, g: 191, b: 150}, | |
28 {r: 110, g: 99, b: 145}, | |
29 {r: 80, g: 129, b: 109}, | |
30 {r: 125, g: 140, b: 149}, | |
31 {r: 93, g: 124, b: 132}, | |
32 {r: 140, g: 85, b: 140}, | |
33 {r: 104, g: 163, b: 162}, | |
34 {r: 132, g: 141, b: 178}, | |
35 {r: 131, g: 105, b: 147}, | |
36 {r: 135, g: 183, b: 98}, | |
37 {r: 152, g: 134, b: 177}, | |
38 {r: 141, g: 188, b: 141}, | |
39 {r: 133, g: 160, b: 210}, | |
40 {r: 126, g: 186, b: 148}, | |
41 {r: 112, g: 198, b: 205}, | |
42 {r: 180, g: 122, b: 195}, | |
43 {r: 203, g: 144, b: 152}]; | |
44 | |
45 function brighten(c) { | |
46 return {r: Math.min(255, c.r + Math.floor(c.r * 0.45)), | |
47 g: Math.min(255, c.g + Math.floor(c.g * 0.45)), | |
48 b: Math.min(255, c.b + Math.floor(c.b * 0.45))}; | |
49 } | |
50 function colorToString(c) { | |
51 return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')'; | |
52 } | |
53 | |
54 const selectedIdBoost = palletteBase.length; | |
55 | |
56 const pallette = palletteBase.concat(palletteBase.map(brighten)). | |
57 map(colorToString); | |
58 | |
59 var textWidthMap = { }; | |
60 function quickMeasureText(ctx, text) { | |
61 var w = textWidthMap[text]; | |
62 if (!w) { | |
63 w = ctx.measureText(text).width; | |
64 textWidthMap[text] = w; | |
65 } | |
66 return w; | |
67 } | |
68 | |
69 function addTrack(thisTrack, slices) { | |
70 var track = new TimelineSliceTrack(); | |
71 | |
72 track.heading = ''; | |
73 track.slices = slices; | |
74 track.viewport = thisTrack.viewport_; | |
75 | |
76 thisTrack.tracks_.push(track); | |
77 thisTrack.appendChild(track); | |
78 } | |
79 | |
80 /** | |
81 * Generic base class for timeline tracks | |
82 */ | |
83 TimelineThreadTrack = cr.ui.define('div'); | |
84 TimelineThreadTrack.prototype = { | |
85 __proto__: HTMLDivElement.prototype, | |
86 | |
87 decorate: function() { | |
88 this.className = 'timeline-thread-track'; | |
89 }, | |
90 | |
91 set thread(thread) { | |
92 this.thread_ = thread; | |
93 this.updateChildTracks_(); | |
94 }, | |
95 | |
96 set viewport(v) { | |
97 this.viewport_ = v; | |
98 for (var i = 0; i < this.tracks_.length; i++) | |
99 this.tracks_[i].viewport = v; | |
100 this.invalidate(); | |
101 }, | |
102 | |
103 invalidate: function() { | |
104 if (this.parentNode) | |
105 this.parentNode.invalidate(); | |
106 }, | |
107 | |
108 onResize: function() { | |
109 for (var i = 0; i < this.tracks_.length; i++) | |
110 this.tracks_[i].onResize(); | |
111 }, | |
112 | |
113 get firstCanvas() { | |
114 if (this.tracks_.length) | |
115 return this.tracks_[0].firstCanvas; | |
116 return undefined; | |
117 }, | |
118 | |
119 redraw: function() { | |
120 for (var i = 0; i < this.tracks_.length; i++) | |
121 this.tracks_[i].redraw(); | |
122 }, | |
123 | |
124 updateChildTracks_: function() { | |
125 this.textContent = ''; | |
126 this.tracks_ = []; | |
127 if (this.thread_) { | |
128 for (var srI = 0; srI < this.thread_.nonNestedSubRows.length; ++srI) { | |
129 addTrack(this, this.thread_.nonNestedSubRows[srI]); | |
130 } | |
131 for (var srI = 0; srI < this.thread_.subRows.length; ++srI) { | |
132 addTrack(this, this.thread_.subRows[srI]); | |
133 } | |
134 if (this.tracks_.length > 0) { | |
135 this.tracks_[0].heading = this.thread_.parent.pid + ': ' + | |
136 this.thread_.tid + ': '; | |
137 } | |
138 } | |
139 }, | |
140 | |
141 /** | |
142 * Picks a slice, if any, at a given location. | |
143 * @param {number} wX X location to search at, in worldspace. | |
144 * @param {number} wY Y location to search at, in offset space. | |
145 * offset space. | |
146 * @param {function():*} onHitCallback Callback to call with the slice, | |
147 * if one is found. | |
148 * @return {boolean} true if a slice was found, otherwise false. | |
149 */ | |
150 pick: function(wX, wY, onHitCallback) { | |
151 for (var i = 0; i < this.tracks_.length; i++) { | |
152 var trackClientRect = this.tracks_[i].getBoundingClientRect(); | |
153 if (wY >= trackClientRect.top && wY < trackClientRect.bottom) | |
154 return this.tracks_[i].pick(wX, onHitCallback); | |
155 } | |
156 return false; | |
157 }, | |
158 | |
159 /** | |
160 * Finds slices intersecting the given interval. | |
161 * @param {number} loWX Lower X bound of the interval to search, in | |
162 * worldspace. | |
163 * @param {number} hiWX Upper X bound of the interval to search, in | |
164 * worldspace. | |
165 * @param {number} loY Lower Y bound of the interval to search, in | |
166 * offset space. | |
167 * @param {number} hiY Upper Y bound of the interval to search, in | |
168 * offset space. | |
169 * @param {function():*} onHitCallback Function to call for each slice | |
170 * intersecting the interval. | |
171 */ | |
172 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { | |
173 for (var i = 0; i < this.tracks_.length; i++) { | |
174 var trackClientRect = this.tracks_[i].getBoundingClientRect(); | |
175 var a = Math.max(loY, trackClientRect.top); | |
176 var b = Math.min(hiY, trackClientRect.bottom); | |
177 if (a <= b) | |
178 this.tracks_[i].pickRange(loWX, hiWX, loY, hiY, onHitCallback); | |
179 } | |
180 } | |
181 }; | |
182 | |
183 /** | |
184 * Creates a new timeline track div element | |
185 * @constructor | |
186 * @extends {HTMLDivElement} | |
187 */ | |
188 TimelineSliceTrack = cr.ui.define('div'); | |
189 | |
190 TimelineSliceTrack.prototype = { | |
191 __proto__: HTMLDivElement.prototype, | |
192 | |
193 decorate: function() { | |
194 this.className = 'timeline-slice-track'; | |
195 this.slices_ = null; | |
196 | |
197 this.titleDiv_ = document.createElement('div'); | |
198 this.titleDiv_.className = 'timeline-slice-track-title'; | |
199 this.appendChild(this.titleDiv_); | |
200 | |
201 this.canvasContainer_ = document.createElement('div'); | |
202 this.canvasContainer_.className = 'timeline-slice-track-canvas-container'; | |
203 this.appendChild(this.canvasContainer_); | |
204 this.canvas_ = document.createElement('canvas'); | |
205 this.canvas_.className = 'timeline-slice-track-canvas'; | |
206 this.canvasContainer_.appendChild(this.canvas_); | |
207 | |
208 this.ctx_ = this.canvas_.getContext('2d'); | |
209 }, | |
210 | |
211 set heading(text) { | |
212 this.titleDiv_.textContent = text; | |
213 }, | |
214 | |
215 set slices(slices) { | |
216 this.slices_ = slices; | |
217 this.invalidate(); | |
218 }, | |
219 | |
220 set viewport(v) { | |
221 this.viewport_ = v; | |
222 this.invalidate(); | |
223 }, | |
224 | |
225 invalidate: function() { | |
226 if (this.parentNode) | |
227 this.parentNode.invalidate(); | |
228 }, | |
229 | |
230 get firstCanvas() { | |
231 return this.canvas_; | |
232 }, | |
233 | |
234 onResize: function() { | |
235 this.canvas_.width = this.canvasContainer_.clientWidth; | |
236 this.canvas_.height = this.canvasContainer_.clientHeight; | |
237 this.invalidate(); | |
238 }, | |
239 | |
240 redraw: function() { | |
241 if (!this.viewport_) | |
242 return; | |
243 var ctx = this.ctx_; | |
244 var canvasW = this.canvas_.width; | |
245 var canvasH = this.canvas_.height; | |
246 | |
247 ctx.clearRect(0, 0, canvasW, canvasH); | |
248 | |
249 // culling... | |
250 var vp = this.viewport_; | |
251 var pixWidth = vp.xViewVectorToWorld(1); | |
252 var viewLWorld = vp.xViewToWorld(0); | |
253 var viewRWorld = vp.xViewToWorld(canvasW); | |
254 | |
255 // Draw grid without a transform because the scale | |
256 // affects line width. | |
257 if (vp.gridEnabled) { | |
258 var x = vp.gridTimebase; | |
259 ctx.beginPath(); | |
260 while (x < viewRWorld) { | |
261 if (x >= viewLWorld) { | |
262 // Do conversion to viewspace here rather than on | |
263 // x to avoid precision issues. | |
264 var vx = vp.xWorldToView(x); | |
265 ctx.moveTo(vx, 0); | |
266 ctx.lineTo(vx, canvasH); | |
267 } | |
268 x += vp.gridStep; | |
269 } | |
270 ctx.strokeStyle = 'rgba(255,0,0,0.25)'; | |
271 ctx.stroke(); | |
272 } | |
273 | |
274 // begin rendering in world space | |
275 ctx.save(); | |
276 vp.applyTransformToCanavs(ctx); | |
277 | |
278 // tracks | |
279 var tr = new gpu.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth, | |
280 2 * pixWidth, viewRWorld, pallette); | |
281 tr.setYandH(0, canvasH); | |
282 var slices = this.slices_; | |
283 for (var i = 0; i < slices.length; ++i) { | |
284 var slice = slices[i]; | |
285 var x = slice.start; | |
286 // Less than 0.001 causes short events to disappear when zoomed in. | |
287 var w = Math.max(slice.duration, 0.001); | |
288 var colorId; | |
289 colorId = slice.selected ? | |
290 slice.colorId + selectedIdBoost : | |
291 slice.colorId; | |
292 | |
293 if (w < pixWidth) | |
294 w = pixWidth; | |
295 if (slice.duration > 0) { | |
296 tr.fillRect(x, w, colorId); | |
297 } else { | |
298 // Instant: draw a triangle. If zoomed too far, collapse | |
299 // into the FastRectRenderer. | |
300 if (pixWidth > 0.001) { | |
301 tr.fillRect(x, pixWidth, colorId); | |
302 } else { | |
303 ctx.fillStyle = pallette[colorId]; | |
304 ctx.beginPath(); | |
305 ctx.moveTo(x - (4 * pixWidth), canvasH); | |
306 ctx.lineTo(x, 0); | |
307 ctx.lineTo(x + (4 * pixWidth), canvasH); | |
308 ctx.closePath(); | |
309 ctx.fill(); | |
310 } | |
311 } | |
312 } | |
313 tr.flush(); | |
314 ctx.restore(); | |
315 | |
316 // labels | |
317 ctx.textAlign = 'center'; | |
318 ctx.textBaseline = 'top'; | |
319 ctx.font = '10px sans-serif'; | |
320 ctx.strokeStyle = 'rgb(0,0,0)'; | |
321 ctx.fillStyle = 'rgb(0,0,0)'; | |
322 var quickDiscardThresshold = pixWidth * 20; // dont render until 20px wide | |
323 for (var i = 0; i < slices.length; ++i) { | |
324 var slice = slices[i]; | |
325 if (slice.duration > quickDiscardThresshold) { | |
326 var title = slice.title; | |
327 if (slice.didNotFinish) { | |
328 title += " (Did Not Finish)"; | |
329 } | |
330 function labelWidth() { | |
331 return quickMeasureText(ctx, title) + 2; | |
332 } | |
333 function labelWidthWorld() { | |
334 return pixWidth * labelWidth(); | |
335 } | |
336 var elided = false; | |
337 while (labelWidthWorld() > slice.duration) { | |
338 title = title.substring(0, title.length * 0.75); | |
339 elided = true; | |
340 } | |
341 if (elided && title.length > 3) | |
342 title = title.substring(0, title.length - 3) + '...'; | |
343 var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); | |
344 ctx.fillText(title, cX, 2.5, labelWidthWorld()); | |
345 } | |
346 } | |
347 }, | |
348 | |
349 /** | |
350 * Picks a slice, if any, at a given location. | |
351 * @param {number} wX X location to search at, in worldspace. | |
352 * @param {number} wY Y location to search at, in offset space. | |
353 * offset space. | |
354 * @param {function():*} onHitCallback Callback to call with the slice, | |
355 * if one is found. | |
356 * @return {boolean} true if a slice was found, otherwise false. | |
357 */ | |
358 pick: function(wX, wY, onHitCallback) { | |
359 var clientRect = this.getBoundingClientRect(); | |
360 if (wY < clientRect.top || wY >= clientRect.bottom) | |
361 return false; | |
362 var x = gpu.findLowIndexInSortedIntervals(this.slices_, | |
363 function(x) { return x.start; }, | |
364 function(x) { return x.duration; }, | |
365 wX); | |
366 if (x >= 0 && x < this.slices_.length) { | |
367 onHitCallback('slice', this, this.slices_[x]); | |
368 return true; | |
369 } | |
370 return false; | |
371 }, | |
372 | |
373 /** | |
374 * Finds slices intersecting the given interval. | |
375 * @param {number} loWX Lower X bound of the interval to search, in | |
376 * worldspace. | |
377 * @param {number} hiWX Upper X bound of the interval to search, in | |
378 * worldspace. | |
379 * @param {number} loY Lower Y bound of the interval to search, in | |
380 * offset space. | |
381 * @param {number} hiY Upper Y bound of the interval to search, in | |
382 * offset space. | |
383 * @param {function():*} onHitCallback Function to call for each slice | |
384 * intersecting the interval. | |
385 */ | |
386 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { | |
387 var clientRect = this.getBoundingClientRect(); | |
388 var a = Math.max(loY, clientRect.top); | |
389 var b = Math.min(hiY, clientRect.bottom); | |
390 if (a > b) | |
391 return; | |
392 | |
393 var that = this; | |
394 function onPickHit(slice) { | |
395 onHitCallback('slice', that, slice); | |
396 } | |
397 gpu.iterateOverIntersectingIntervals(this.slices_, | |
398 function(x) { return x.start; }, | |
399 function(x) { return x.duration; }, | |
400 loWX, hiWX, | |
401 onPickHit); | |
402 }, | |
403 | |
404 /** | |
405 * Find the index for the given slice. | |
406 * @return {index} Index of the given slice, or undefined. | |
407 * @private | |
408 */ | |
409 indexOfSlice_: function(slice) { | |
410 var index = gpu.findLowIndexInSortedArray(this.slices_, | |
411 function(x) { return x.start; }, | |
412 slice.start); | |
413 while (index < this.slices_.length && | |
414 slice.start == this.slices_[index].start && | |
415 slice.colorId != this.slices_[index].colorId) { | |
416 index++; | |
417 } | |
418 return index < this.slices_.length ? index : undefined; | |
419 }, | |
420 | |
421 /** | |
422 * Return the next slice, if any, after the given slice. | |
423 * @param {slice} The previous slice. | |
424 * @return {slice} The next slice, or undefined. | |
425 * @private | |
426 */ | |
427 pickNext: function(slice) { | |
428 var index = this.indexOfSlice_(slice); | |
429 if (index != undefined) { | |
430 if (index < this.slices_.length - 1) | |
431 index++; | |
432 else | |
433 index = undefined; | |
434 } | |
435 return index != undefined ? this.slices_[index] : undefined; | |
436 }, | |
437 | |
438 /** | |
439 * Return the previous slice, if any, before the given slice. | |
440 * @param {slice} A slice. | |
441 * @return {slice} The previous slice, or undefined. | |
442 */ | |
443 pickPrevious: function(slice) { | |
444 var index = this.indexOfSlice_(slice); | |
445 if (index == 0) | |
446 return undefined; | |
447 else if ((index != undefined) && (index > 0)) | |
448 index--; | |
449 return index != undefined ? this.slices_[index] : undefined; | |
450 }, | |
451 | |
452 }; | |
453 | |
454 return { | |
455 TimelineSliceTrack: TimelineSliceTrack, | |
456 TimelineThreadTrack: TimelineThreadTrack | |
457 }; | |
458 }); | |
OLD | NEW |