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 var tname = this.thread_.name || this.thread_.tid; | |
136 this.tracks_[0].heading = this.thread_.parent.pid + ': ' + | |
137 tname + ':'; | |
138 this.tracks_[0].tooltip = 'pid: ' + this.thread_.parent.pid + | |
139 ', tid: ' + this.thread_.tid + | |
140 (this.thread_.name ? ', name: ' + this.thread_.name : ''); | |
141 } | |
142 } | |
143 }, | |
144 | |
145 /** | |
146 * Picks a slice, if any, at a given location. | |
147 * @param {number} wX X location to search at, in worldspace. | |
148 * @param {number} wY Y location to search at, in offset space. | |
149 * offset space. | |
150 * @param {function():*} onHitCallback Callback to call with the slice, | |
151 * if one is found. | |
152 * @return {boolean} true if a slice was found, otherwise false. | |
153 */ | |
154 pick: function(wX, wY, onHitCallback) { | |
155 for (var i = 0; i < this.tracks_.length; i++) { | |
156 var trackClientRect = this.tracks_[i].getBoundingClientRect(); | |
157 if (wY >= trackClientRect.top && wY < trackClientRect.bottom) | |
158 return this.tracks_[i].pick(wX, onHitCallback); | |
159 } | |
160 return false; | |
161 }, | |
162 | |
163 /** | |
164 * Finds slices intersecting the given interval. | |
165 * @param {number} loWX Lower X bound of the interval to search, in | |
166 * worldspace. | |
167 * @param {number} hiWX Upper X bound of the interval to search, in | |
168 * worldspace. | |
169 * @param {number} loY Lower Y bound of the interval to search, in | |
170 * offset space. | |
171 * @param {number} hiY Upper Y bound of the interval to search, in | |
172 * offset space. | |
173 * @param {function():*} onHitCallback Function to call for each slice | |
174 * intersecting the interval. | |
175 */ | |
176 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { | |
177 for (var i = 0; i < this.tracks_.length; i++) { | |
178 var trackClientRect = this.tracks_[i].getBoundingClientRect(); | |
179 var a = Math.max(loY, trackClientRect.top); | |
180 var b = Math.min(hiY, trackClientRect.bottom); | |
181 if (a <= b) | |
182 this.tracks_[i].pickRange(loWX, hiWX, loY, hiY, onHitCallback); | |
183 } | |
184 } | |
185 }; | |
186 | |
187 /** | |
188 * Creates a new timeline track div element | |
189 * @constructor | |
190 * @extends {HTMLDivElement} | |
191 */ | |
192 TimelineSliceTrack = cr.ui.define('div'); | |
193 | |
194 TimelineSliceTrack.prototype = { | |
195 __proto__: HTMLDivElement.prototype, | |
196 | |
197 decorate: function() { | |
198 this.className = 'timeline-slice-track'; | |
199 this.slices_ = null; | |
200 | |
201 this.headingDiv_ = document.createElement('div'); | |
202 this.headingDiv_.className = 'timeline-slice-track-title'; | |
203 this.appendChild(this.headingDiv_); | |
204 | |
205 this.canvasContainer_ = document.createElement('div'); | |
206 this.canvasContainer_.className = 'timeline-slice-track-canvas-container'; | |
207 this.appendChild(this.canvasContainer_); | |
208 this.canvas_ = document.createElement('canvas'); | |
209 this.canvas_.className = 'timeline-slice-track-canvas'; | |
210 this.canvasContainer_.appendChild(this.canvas_); | |
211 | |
212 this.ctx_ = this.canvas_.getContext('2d'); | |
213 }, | |
214 | |
215 set heading(text) { | |
216 this.headingDiv_.textContent = text; | |
217 }, | |
218 | |
219 set tooltip(text) { | |
220 this.headingDiv_.title = text; | |
221 }, | |
222 | |
223 set slices(slices) { | |
224 this.slices_ = slices; | |
225 this.invalidate(); | |
226 }, | |
227 | |
228 set viewport(v) { | |
229 this.viewport_ = v; | |
230 this.invalidate(); | |
231 }, | |
232 | |
233 invalidate: function() { | |
234 if (this.parentNode) | |
235 this.parentNode.invalidate(); | |
236 }, | |
237 | |
238 get firstCanvas() { | |
239 return this.canvas_; | |
240 }, | |
241 | |
242 onResize: function() { | |
243 this.canvas_.width = this.canvasContainer_.clientWidth; | |
244 this.canvas_.height = this.canvasContainer_.clientHeight; | |
245 this.invalidate(); | |
246 }, | |
247 | |
248 redraw: function() { | |
249 if (!this.viewport_) | |
250 return; | |
251 var ctx = this.ctx_; | |
252 var canvasW = this.canvas_.width; | |
253 var canvasH = this.canvas_.height; | |
254 | |
255 ctx.clearRect(0, 0, canvasW, canvasH); | |
256 | |
257 // culling... | |
258 var vp = this.viewport_; | |
259 var pixWidth = vp.xViewVectorToWorld(1); | |
260 var viewLWorld = vp.xViewToWorld(0); | |
261 var viewRWorld = vp.xViewToWorld(canvasW); | |
262 | |
263 // Draw grid without a transform because the scale | |
264 // affects line width. | |
265 if (vp.gridEnabled) { | |
266 var x = vp.gridTimebase; | |
267 ctx.beginPath(); | |
268 while (x < viewRWorld) { | |
269 if (x >= viewLWorld) { | |
270 // Do conversion to viewspace here rather than on | |
271 // x to avoid precision issues. | |
272 var vx = vp.xWorldToView(x); | |
273 ctx.moveTo(vx, 0); | |
274 ctx.lineTo(vx, canvasH); | |
275 } | |
276 x += vp.gridStep; | |
277 } | |
278 ctx.strokeStyle = 'rgba(255,0,0,0.25)'; | |
279 ctx.stroke(); | |
280 } | |
281 | |
282 // begin rendering in world space | |
283 ctx.save(); | |
284 vp.applyTransformToCanavs(ctx); | |
285 | |
286 // tracks | |
287 var tr = new gpu.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth, | |
288 2 * pixWidth, viewRWorld, pallette); | |
289 tr.setYandH(0, canvasH); | |
290 var slices = this.slices_; | |
291 for (var i = 0; i < slices.length; ++i) { | |
292 var slice = slices[i]; | |
293 var x = slice.start; | |
294 // Less than 0.001 causes short events to disappear when zoomed in. | |
295 var w = Math.max(slice.duration, 0.001); | |
296 var colorId; | |
297 colorId = slice.selected ? | |
298 slice.colorId + selectedIdBoost : | |
299 slice.colorId; | |
300 | |
301 if (w < pixWidth) | |
302 w = pixWidth; | |
303 if (slice.duration > 0) { | |
304 tr.fillRect(x, w, colorId); | |
305 } else { | |
306 // Instant: draw a triangle. If zoomed too far, collapse | |
307 // into the FastRectRenderer. | |
308 if (pixWidth > 0.001) { | |
309 tr.fillRect(x, pixWidth, colorId); | |
310 } else { | |
311 ctx.fillStyle = pallette[colorId]; | |
312 ctx.beginPath(); | |
313 ctx.moveTo(x - (4 * pixWidth), canvasH); | |
314 ctx.lineTo(x, 0); | |
315 ctx.lineTo(x + (4 * pixWidth), canvasH); | |
316 ctx.closePath(); | |
317 ctx.fill(); | |
318 } | |
319 } | |
320 } | |
321 tr.flush(); | |
322 ctx.restore(); | |
323 | |
324 // labels | |
325 ctx.textAlign = 'center'; | |
326 ctx.textBaseline = 'top'; | |
327 ctx.font = '10px sans-serif'; | |
328 ctx.strokeStyle = 'rgb(0,0,0)'; | |
329 ctx.fillStyle = 'rgb(0,0,0)'; | |
330 var quickDiscardThresshold = pixWidth * 20; // dont render until 20px wide | |
331 for (var i = 0; i < slices.length; ++i) { | |
332 var slice = slices[i]; | |
333 if (slice.duration > quickDiscardThresshold) { | |
334 var title = slice.title; | |
335 if (slice.didNotFinish) { | |
336 title += ' (Did Not Finish)'; | |
337 } | |
338 function labelWidth() { | |
339 return quickMeasureText(ctx, title) + 2; | |
340 } | |
341 function labelWidthWorld() { | |
342 return pixWidth * labelWidth(); | |
343 } | |
344 var elided = false; | |
345 while (labelWidthWorld() > slice.duration) { | |
346 title = title.substring(0, title.length * 0.75); | |
347 elided = true; | |
348 } | |
349 if (elided && title.length > 3) | |
350 title = title.substring(0, title.length - 3) + '...'; | |
351 var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration); | |
352 ctx.fillText(title, cX, 2.5, labelWidthWorld()); | |
353 } | |
354 } | |
355 }, | |
356 | |
357 /** | |
358 * Picks a slice, if any, at a given location. | |
359 * @param {number} wX X location to search at, in worldspace. | |
360 * @param {number} wY Y location to search at, in offset space. | |
361 * offset space. | |
362 * @param {function():*} onHitCallback Callback to call with the slice, | |
363 * if one is found. | |
364 * @return {boolean} true if a slice was found, otherwise false. | |
365 */ | |
366 pick: function(wX, wY, onHitCallback) { | |
367 var clientRect = this.getBoundingClientRect(); | |
368 if (wY < clientRect.top || wY >= clientRect.bottom) | |
369 return false; | |
370 var x = gpu.findLowIndexInSortedIntervals(this.slices_, | |
371 function(x) { return x.start; }, | |
372 function(x) { return x.duration; }, | |
373 wX); | |
374 if (x >= 0 && x < this.slices_.length) { | |
375 onHitCallback('slice', this, this.slices_[x]); | |
376 return true; | |
377 } | |
378 return false; | |
379 }, | |
380 | |
381 /** | |
382 * Finds slices intersecting the given interval. | |
383 * @param {number} loWX Lower X bound of the interval to search, in | |
384 * worldspace. | |
385 * @param {number} hiWX Upper X bound of the interval to search, in | |
386 * worldspace. | |
387 * @param {number} loY Lower Y bound of the interval to search, in | |
388 * offset space. | |
389 * @param {number} hiY Upper Y bound of the interval to search, in | |
390 * offset space. | |
391 * @param {function():*} onHitCallback Function to call for each slice | |
392 * intersecting the interval. | |
393 */ | |
394 pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) { | |
395 var clientRect = this.getBoundingClientRect(); | |
396 var a = Math.max(loY, clientRect.top); | |
397 var b = Math.min(hiY, clientRect.bottom); | |
398 if (a > b) | |
399 return; | |
400 | |
401 var that = this; | |
402 function onPickHit(slice) { | |
403 onHitCallback('slice', that, slice); | |
404 } | |
405 gpu.iterateOverIntersectingIntervals(this.slices_, | |
406 function(x) { return x.start; }, | |
407 function(x) { return x.duration; }, | |
408 loWX, hiWX, | |
409 onPickHit); | |
410 }, | |
411 | |
412 /** | |
413 * Find the index for the given slice. | |
414 * @return {index} Index of the given slice, or undefined. | |
415 * @private | |
416 */ | |
417 indexOfSlice_: function(slice) { | |
418 var index = gpu.findLowIndexInSortedArray(this.slices_, | |
419 function(x) { return x.start; }, | |
420 slice.start); | |
421 while (index < this.slices_.length && | |
422 slice.start == this.slices_[index].start && | |
423 slice.colorId != this.slices_[index].colorId) { | |
424 index++; | |
425 } | |
426 return index < this.slices_.length ? index : undefined; | |
427 }, | |
428 | |
429 /** | |
430 * Return the next slice, if any, after the given slice. | |
431 * @param {slice} The previous slice. | |
432 * @return {slice} The next slice, or undefined. | |
433 * @private | |
434 */ | |
435 pickNext: function(slice) { | |
436 var index = this.indexOfSlice_(slice); | |
437 if (index != undefined) { | |
438 if (index < this.slices_.length - 1) | |
439 index++; | |
440 else | |
441 index = undefined; | |
442 } | |
443 return index != undefined ? this.slices_[index] : undefined; | |
444 }, | |
445 | |
446 /** | |
447 * Return the previous slice, if any, before the given slice. | |
448 * @param {slice} A slice. | |
449 * @return {slice} The previous slice, or undefined. | |
450 */ | |
451 pickPrevious: function(slice) { | |
452 var index = this.indexOfSlice_(slice); | |
453 if (index == 0) | |
454 return undefined; | |
455 else if ((index != undefined) && (index > 0)) | |
456 index--; | |
457 return index != undefined ? this.slices_[index] : undefined; | |
458 }, | |
459 | |
460 }; | |
461 | |
462 return { | |
463 TimelineSliceTrack: TimelineSliceTrack, | |
464 TimelineThreadTrack: TimelineThreadTrack | |
465 }; | |
466 }); | |
OLD | NEW |