OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright (C) 2013 Google Inc. All rights reserved. | |
3 * | |
4 * Redistribution and use in source and binary forms, with or without | |
5 * modification, are permitted provided that the following conditions are | |
6 * met: | |
7 * | |
8 * * Redistributions of source code must retain the above copyright | |
9 * notice, this list of conditions and the following disclaimer. | |
10 * * Redistributions in binary form must reproduce the above | |
11 * copyright notice, this list of conditions and the following disclaimer | |
12 * in the documentation and/or other materials provided with the | |
13 * distribution. | |
14 * * Neither the name of Google Inc. nor the names of its | |
15 * contributors may be used to endorse or promote products derived from | |
16 * this software without specific prior written permission. | |
17 * | |
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
29 */ | |
30 | |
31 /** | |
32 * @constructor | |
33 * @extends {WebInspector.TimelineOverviewBase} | |
34 * @param {!WebInspector.TimelineModel} model | |
35 * @param {!WebInspector.TimelineFrameModelBase} frameModel | |
36 */ | |
37 WebInspector.TimelineFrameOverview = function(model, frameModel) | |
38 { | |
39 WebInspector.TimelineOverviewBase.call(this); | |
40 this.element.id = "timeline-overview-frames"; | |
41 this._model = model; | |
42 this._frameModel = frameModel; | |
43 this.reset(); | |
44 | |
45 this._outerPadding = 4 * window.devicePixelRatio; | |
46 this._maxInnerBarWidth = 10 * window.devicePixelRatio; | |
47 this._topPadding = 6 * window.devicePixelRatio; | |
48 | |
49 // The below two are really computed by update() -- but let's have something
so that windowTimes() is happy. | |
50 this._actualPadding = 5 * window.devicePixelRatio; | |
51 this._actualOuterBarWidth = this._maxInnerBarWidth + this._actualPadding; | |
52 | |
53 this._fillStyles = {}; | |
54 var categories = WebInspector.TimelineUIUtils.categories(); | |
55 for (var category in categories) | |
56 this._fillStyles[category] = WebInspector.TimelineUIUtils.createFillStyl
eForCategory(this._context, this._maxInnerBarWidth, 0, categories[category]); | |
57 | |
58 this._frameTopShadeGradient = this._context.createLinearGradient(0, 0, 0, th
is._topPadding); | |
59 this._frameTopShadeGradient.addColorStop(0, "rgba(255, 255, 255, 0.9)"); | |
60 this._frameTopShadeGradient.addColorStop(1, "rgba(255, 255, 255, 0.2)"); | |
61 | |
62 this.element.addEventListener("mousemove", this._onMouseMove.bind(this), fal
se); | |
63 this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false
); | |
64 } | |
65 | |
66 WebInspector.TimelineFrameOverview.Events = { | |
67 SelectionChanged: "SelectionChanged" | |
68 } | |
69 | |
70 WebInspector.TimelineFrameOverview.prototype = { | |
71 /** | |
72 * @override | |
73 */ | |
74 reset: function() | |
75 { | |
76 /** @type {!Array<!{startTime:number, endTime:number}>} */ | |
77 this._barTimes = []; | |
78 /** @type {!Array<!WebInspector.TimelineFrame>} */ | |
79 this._visibleFrames = []; | |
80 this._selectedBarIndex = null; | |
81 this._activeBarIndex = null; | |
82 }, | |
83 | |
84 /** | |
85 * @override | |
86 */ | |
87 update: function() | |
88 { | |
89 this.resetCanvas(); | |
90 this._barTimes = []; | |
91 | |
92 var minBarWidth = 4 * window.devicePixelRatio; | |
93 var frames = this._frameModel.frames(); | |
94 var framesPerBar = Math.max(1, frames.length * minBarWidth / this._canva
s.width); | |
95 this._visibleFrames = this._aggregateFrames(frames, framesPerBar); | |
96 this._scale = (this._canvas.height - this._topPadding) / this._computeTa
rgetFrameLength(this._visibleFrames); | |
97 var maxPadding = 5 * window.devicePixelRatio; | |
98 this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._out
erPadding) / this._visibleFrames.length, this._maxInnerBarWidth + maxPadding); | |
99 this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3)
, maxPadding); | |
100 | |
101 this._context.save(); | |
102 for (var i = this._visibleFrames.length - 1; i >= 0; --i) | |
103 this._drawBar(i); | |
104 this._drawTopShadeGradient(); | |
105 this._drawFPSMarks(); | |
106 this._drawSelection(); | |
107 this._context.restore(); | |
108 }, | |
109 | |
110 /** | |
111 * @param {?WebInspector.TimelineSelection} selection | |
112 */ | |
113 select: function(selection) | |
114 { | |
115 var oldSelectionIndex = this._selectedBarIndex; | |
116 var frame = selection && selection.type() === WebInspector.TimelineSelec
tion.Type.Frame ? /** @type {!WebInspector.TimelineFrame} */ (selection.object()
) : null; | |
117 var index = frame ? this._visibleFrames.indexOf(frame) : -1; | |
118 this._selectedBarIndex = index >= 0 ? index : null; | |
119 if (this._selectedBarIndex === oldSelectionIndex) | |
120 return; | |
121 if (typeof oldSelectionIndex === "number") | |
122 this._redrawBar(oldSelectionIndex); | |
123 this._drawSelection(); | |
124 }, | |
125 | |
126 /** | |
127 * @override | |
128 * @param {!Event} event | |
129 * @return {boolean} | |
130 */ | |
131 onClick: function(event) | |
132 { | |
133 var barIndex = this._screenPositionToBarIndex(event.clientX); | |
134 if (barIndex < 0 || barIndex >= this._visibleFrames.length) | |
135 return false; | |
136 var selection = WebInspector.TimelineSelection.fromFrame(this._visibleFr
ames[barIndex]); | |
137 this.dispatchEventToListeners(WebInspector.TimelineFrameOverview.Events.
SelectionChanged, selection); | |
138 return true; | |
139 }, | |
140 | |
141 /** | |
142 * @param {!Event} event | |
143 */ | |
144 _onMouseMove: function(event) | |
145 { | |
146 var barIndex = this._screenPositionToBarIndex(event.clientX); | |
147 if (barIndex < 0 || barIndex >= this._visibleFrames.length) | |
148 barIndex = null; | |
149 this._setActiveBarIndex(barIndex); | |
150 }, | |
151 | |
152 /** | |
153 * @param {!Event} event | |
154 */ | |
155 _onMouseOut: function(event) | |
156 { | |
157 this._setActiveBarIndex(null); | |
158 }, | |
159 | |
160 /** | |
161 * @param {?number} index | |
162 */ | |
163 _setActiveBarIndex: function(index) | |
164 { | |
165 if (this._activeBarIndex === index) | |
166 return; | |
167 var oldActveBarIndex = this._activeBarIndex; | |
168 this._activeBarIndex = index; | |
169 if (typeof oldActveBarIndex === "number") | |
170 this._redrawBar(oldActveBarIndex); | |
171 if (typeof this._activeBarIndex === "number") | |
172 this._redrawBar(this._activeBarIndex); | |
173 }, | |
174 | |
175 /** | |
176 * @param {number} index | |
177 */ | |
178 _redrawBar: function(index) | |
179 { | |
180 this._context.save(); | |
181 this._context.beginPath(); | |
182 var left = this._barIndexToScreenPosition(index) - this._actualPadding; | |
183 var right = Math.ceil(left + this._actualOuterBarWidth); | |
184 this._context.rect(left, 0, right - left + 1, this._canvas.height); | |
185 this._context.fillStyle = "rgb(255, 255, 255)"; | |
186 this._context.clip(); | |
187 this._context.fill(); | |
188 if (index > 0) | |
189 this._drawBar(index - 1); | |
190 if (index + 1 < this._visibleFrames.length) | |
191 this._drawBar(index + 1); | |
192 this._drawBar(index); | |
193 this._drawTopShadeGradient(); | |
194 this._drawFPSMarks(); | |
195 if (typeof this._selectedBarIndex === "number") | |
196 this._drawSelection(); | |
197 this._context.restore(); | |
198 }, | |
199 | |
200 /** | |
201 * @param {!Array.<!WebInspector.TimelineFrame>} frames | |
202 * @param {number} framesPerBar | |
203 * @return {!Array.<!WebInspector.TimelineFrame>} | |
204 */ | |
205 _aggregateFrames: function(frames, framesPerBar) | |
206 { | |
207 var visibleFrames = []; | |
208 for (var barIndex = 0, currentFrame = 0; currentFrame < frames.length; +
+barIndex) { | |
209 var barStartTime = frames[currentFrame].startTime; | |
210 var longestFrame = null; | |
211 var longestDuration; | |
212 | |
213 for (var lastFrame = Math.min(Math.floor((barIndex + 1) * framesPerB
ar), frames.length); | |
214 currentFrame < lastFrame; ++currentFrame) { | |
215 var frame = frames[currentFrame]; | |
216 var duration = frame.idle ? 0 : frame.duration; // Only consider
idle frames if there are no regular frames. | |
217 if (!longestFrame || longestDuration < duration) { | |
218 longestFrame = frame; | |
219 longestDuration = duration; | |
220 } | |
221 } | |
222 var barEndTime = frames[currentFrame - 1].endTime; | |
223 if (longestFrame) { | |
224 visibleFrames.push(longestFrame); | |
225 this._barTimes.push({ startTime: barStartTime, endTime: barEndTi
me }); | |
226 } | |
227 } | |
228 return visibleFrames; | |
229 }, | |
230 | |
231 /** | |
232 * @param {!Array.<!WebInspector.TimelineFrame>} frames | |
233 * @return {number} | |
234 */ | |
235 _computeTargetFrameLength: function(frames) | |
236 { | |
237 var targetFPS = 20; | |
238 var result = 1000.0 / targetFPS; | |
239 if (!frames.length) | |
240 return result; | |
241 | |
242 var durations = frames.select("duration"); | |
243 var medianFrameLength = durations.qselect(Math.floor(durations.length /
2)); | |
244 | |
245 // Optimize appearance for 30fps, but leave some space so it's evident w
hen a frame overflows. | |
246 // However, if at least half frames won't fit at this scale, fall back t
o using autoscale. | |
247 if (result >= medianFrameLength) | |
248 return result; | |
249 | |
250 var maxFrameLength = Math.max.apply(Math, durations); | |
251 return Math.min(medianFrameLength * 2, maxFrameLength); | |
252 }, | |
253 | |
254 /** | |
255 * @param {number} n | |
256 */ | |
257 _barIndexToScreenPosition: function(n) | |
258 { | |
259 return this._outerPadding + this._actualOuterBarWidth * n; | |
260 }, | |
261 | |
262 /** | |
263 * @param {number} clientX | |
264 */ | |
265 _screenPositionToBarIndex: function(clientX) | |
266 { | |
267 var x = (clientX - this.element.totalOffsetLeft()) * window.devicePixelR
atio; | |
268 return Math.floor((x - this._outerPadding) / this._actualOuterBarWidth); | |
269 }, | |
270 | |
271 _drawTopShadeGradient: function() | |
272 { | |
273 this._context.fillStyle = this._frameTopShadeGradient; | |
274 this._context.fillRect(0, 0, this._canvas.width, this._topPadding); | |
275 }, | |
276 | |
277 _drawFPSMarks: function() | |
278 { | |
279 var fpsMarks = [30, 60]; | |
280 | |
281 this._context.save(); | |
282 this._context.beginPath(); | |
283 this._context.font = (10 * window.devicePixelRatio) + "px " + window.get
ComputedStyle(this.element, null).getPropertyValue("font-family"); | |
284 this._context.textAlign = "right"; | |
285 this._context.textBaseline = "alphabetic"; | |
286 | |
287 var labelPadding = 4 * window.devicePixelRatio; | |
288 var baselineHeight = 3 * window.devicePixelRatio; | |
289 var lineHeight = 12 * window.devicePixelRatio; | |
290 var labelTopMargin = 0; | |
291 var labelOffsetY = 0; // Labels are going to be under their grid lines. | |
292 | |
293 for (var i = 0; i < fpsMarks.length; ++i) { | |
294 var fps = fpsMarks[i]; | |
295 // Draw lines one pixel above they need to be, so 60pfs line does no
t cross most of the frames tops. | |
296 var y = this._canvas.height - Math.floor(1000.0 / fps * this._scale)
- 0.5; | |
297 var label = WebInspector.UIString("%d\u2009fps", fps); | |
298 var labelWidth = this._context.measureText(label).width + 2 * labelP
adding; | |
299 var labelX = this._canvas.width; | |
300 | |
301 if (!i && labelTopMargin < y - lineHeight) | |
302 labelOffsetY = -lineHeight; // Labels are going to be over their
grid lines. | |
303 var labelY = y + labelOffsetY; | |
304 if (labelY < labelTopMargin || labelY + lineHeight > this._canvas.he
ight) | |
305 break; // No space for the label, so no line as well. | |
306 | |
307 this._context.moveTo(0, y); | |
308 this._context.lineTo(this._canvas.width, y); | |
309 | |
310 this._context.fillStyle = "rgba(255, 255, 255, 0.5)"; | |
311 this._context.fillRect(labelX - labelWidth, labelY, labelWidth, line
Height); | |
312 this._context.fillStyle = "black"; | |
313 this._context.fillText(label, labelX - labelPadding, labelY + lineHe
ight - baselineHeight); | |
314 labelTopMargin = labelY + lineHeight; | |
315 } | |
316 this._context.strokeStyle = "rgba(60, 60, 60, 0.4)"; | |
317 this._context.stroke(); | |
318 this._context.restore(); | |
319 }, | |
320 | |
321 /** | |
322 * @param {number} index | |
323 */ | |
324 _drawBar: function(index) | |
325 { | |
326 var left = this._barIndexToScreenPosition(index); | |
327 var frame = this._visibleFrames[index]; | |
328 var categories = Object.keys(WebInspector.TimelineUIUtils.categories()); | |
329 var windowHeight = this._canvas.height; | |
330 var width = Math.floor(this._actualOuterBarWidth - this._actualPadding); | |
331 | |
332 var x = Math.floor(left) + 0.5; | |
333 | |
334 var totalCPUTime = frame.cpuTime; | |
335 var normalizedScale = this._scale; | |
336 if (totalCPUTime > frame.duration) | |
337 normalizedScale *= frame.duration / totalCPUTime; | |
338 | |
339 for (var i = 0, bottomOffset = windowHeight; i < categories.length; ++i)
{ | |
340 var category = categories[i]; | |
341 var duration = frame.timeByCategory[category]; | |
342 if (!duration) | |
343 continue; | |
344 var height = Math.round(duration * normalizedScale); | |
345 var y = Math.floor(bottomOffset - height) + 0.5; | |
346 | |
347 this._context.save(); | |
348 this._context.translate(x, 0); | |
349 this._context.scale(width / this._maxInnerBarWidth, 1); | |
350 this._context.fillStyle = this._fillStyles[category]; | |
351 this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(heig
ht)); | |
352 this._context.strokeStyle = WebInspector.TimelineUIUtils.categories(
)[category].borderColor; | |
353 this._context.beginPath(); | |
354 this._context.moveTo(0, y); | |
355 this._context.lineTo(this._maxInnerBarWidth, y); | |
356 this._context.stroke(); | |
357 this._context.restore(); | |
358 | |
359 bottomOffset -= height; | |
360 } | |
361 // Skip outline for idle frames, unless frame is selected. | |
362 if (frame.idle && index !== this._activeBarIndex) | |
363 return; | |
364 | |
365 // Draw a contour for the total frame time. | |
366 var y0 = frame.idle ? bottomOffset + 0.5 : Math.floor(windowHeight - fra
me.duration * this._scale) + 0.5; | |
367 var y1 = windowHeight + 0.5; | |
368 | |
369 this._context.strokeStyle = index === this._activeBarIndex ? "rgba(0, 0,
0, 0.6)" : "rgba(90, 90, 90, 0.2)"; | |
370 this._context.beginPath(); | |
371 this._context.moveTo(x, y1); | |
372 this._context.lineTo(x, y0); | |
373 this._context.lineTo(x + width, y0); | |
374 this._context.lineTo(x + width, y1); | |
375 this._context.stroke(); | |
376 }, | |
377 | |
378 _drawSelection: function() | |
379 { | |
380 if (typeof this._selectedBarIndex !== "number") | |
381 return; | |
382 var left = this._barIndexToScreenPosition(this._selectedBarIndex); | |
383 var width = Math.floor(this._actualOuterBarWidth - this._actualPadding); | |
384 var triangleHeight = 4 * window.devicePixelRatio; | |
385 this._context.save(); | |
386 this._context.beginPath(); | |
387 this._context.moveTo(left, 0); | |
388 this._context.lineTo(left + width, 0); | |
389 this._context.lineTo(left + width / 2, triangleHeight); | |
390 this._context.closePath(); | |
391 this._context.fillStyle = "black"; | |
392 this._context.fill(); | |
393 this._context.restore(); | |
394 }, | |
395 | |
396 /** | |
397 * @override | |
398 * @param {number} windowLeft | |
399 * @param {number} windowRight | |
400 * @return {!{startTime: number, endTime: number}} | |
401 */ | |
402 windowTimes: function(windowLeft, windowRight) | |
403 { | |
404 if (!this._barTimes.length) | |
405 return WebInspector.TimelineOverviewBase.prototype.windowTimes.call(
this, windowLeft, windowRight); | |
406 var windowSpan = this._canvas.width; | |
407 var leftOffset = windowLeft * windowSpan; | |
408 var rightOffset = windowRight * windowSpan; | |
409 var firstBar = Math.floor(Math.max(leftOffset - this._outerPadding + thi
s._actualPadding, 0) / this._actualOuterBarWidth); | |
410 var lastBar = Math.min(Math.floor(Math.max(rightOffset - this._outerPadd
ing, 0)/ this._actualOuterBarWidth), this._barTimes.length - 1); | |
411 if (firstBar >= this._barTimes.length) | |
412 return {startTime: Infinity, endTime: Infinity}; | |
413 | |
414 var snapTolerancePixels = 3; | |
415 return { | |
416 startTime: leftOffset > snapTolerancePixels ? this._barTimes[firstBa
r].startTime : this._model.minimumRecordTime(), | |
417 endTime: (rightOffset + snapTolerancePixels > windowSpan) || (lastBa
r >= this._barTimes.length) ? this._model.maximumRecordTime() : this._barTimes[l
astBar].endTime | |
418 }; | |
419 }, | |
420 | |
421 /** | |
422 * @override | |
423 * @param {number} startTime | |
424 * @param {number} endTime | |
425 * @return {!{left: number, right: number}} | |
426 */ | |
427 windowBoundaries: function(startTime, endTime) | |
428 { | |
429 if (this._barTimes.length === 0) | |
430 return {left: 0, right: 1}; | |
431 /** | |
432 * @param {number} time | |
433 * @param {!{startTime:number, endTime:number}} barTime | |
434 * @return {number} | |
435 */ | |
436 function barStartComparator(time, barTime) | |
437 { | |
438 return time - barTime.startTime; | |
439 } | |
440 /** | |
441 * @param {number} time | |
442 * @param {!{startTime:number, endTime:number}} barTime | |
443 * @return {number} | |
444 */ | |
445 function barEndComparator(time, barTime) | |
446 { | |
447 // We need a frame where time is in [barTime.startTime, barTime.endT
ime), so exclude exact matches against endTime. | |
448 if (time === barTime.endTime) | |
449 return 1; | |
450 return time - barTime.endTime; | |
451 } | |
452 return { | |
453 left: this._windowBoundaryFromTime(startTime, barEndComparator), | |
454 right: this._windowBoundaryFromTime(endTime, barStartComparator) | |
455 }; | |
456 }, | |
457 | |
458 /** | |
459 * @param {number} time | |
460 * @param {function(number, !{startTime:number, endTime:number}):number} com
parator | |
461 */ | |
462 _windowBoundaryFromTime: function(time, comparator) | |
463 { | |
464 if (time === Infinity) | |
465 return 1; | |
466 var index = this._firstBarAfter(time, comparator); | |
467 if (!index) | |
468 return 0; | |
469 return (this._barIndexToScreenPosition(index) - this._actualPadding / 2)
/ this._canvas.width; | |
470 }, | |
471 | |
472 /** | |
473 * @param {number} time | |
474 * @param {function(number, {startTime:number, endTime:number}):number} comp
arator | |
475 */ | |
476 _firstBarAfter: function(time, comparator) | |
477 { | |
478 return insertionIndexForObjectInListSortedByFunction(time, this._barTime
s, comparator); | |
479 }, | |
480 | |
481 __proto__: WebInspector.TimelineOverviewBase.prototype | |
482 } | |
OLD | NEW |