2 * Copyright (C) 2014 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
33 * @implements {WebInspector.FlameChartDataProvider}
34 * @param {!WebInspector.TracingTimelineModel} model
35 * @param {!WebInspector.TimelineFrameModelBase} frameModel
37 WebInspector.TimelineFlameChartDataProvider = function(model, frameModel)
39 WebInspector.FlameChartDataProvider.call(this);
42 this._frameModel = frameModel;
43 this._font = "12px " + WebInspector.fontFamily();
44 this._linkifier = new WebInspector.Linkifier();
46 this.addFilter(WebInspector.TracingTimelineUIUtils.hiddenEventsFilter());
47 this.addFilter(new WebInspector.TracingTimelineModel.ExclusiveEventNameFilter([WebInspector.TracingTimelineModel.RecordType.Program]));
50 WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs = 0.01;
51 WebInspector.TimelineFlameChartDataProvider.JSFrameCoalsceThresholdMs = 1.1;
53 WebInspector.TimelineFlameChartDataProvider.prototype = {
65 textBaseline: function()
73 textPadding: function()
79 * @param {number} entryIndex
82 entryFont: function(entryIndex)
88 * @param {number} entryIndex
91 entryTitle: function(entryIndex)
93 var event = this._entryEvents[entryIndex];
95 var name = WebInspector.TracingTimelineUIUtils.styleForTraceEvent(event.name).title;
96 // TODO(yurys): support event dividers
97 var details = WebInspector.TracingTimelineUIUtils.buildDetailsNodeForTraceEvent(event, this._linkifier);
98 if (event.name === WebInspector.TracingTimelineModel.RecordType.JSFrame && details)
99 return details.textContent;
100 return details ? WebInspector.UIString("%s (%s)", name, details.textContent) : name;
102 var title = this._entryIndexToTitle[entryIndex];
104 title = WebInspector.UIString("Unexpected entryIndex %d", entryIndex);
105 console.error(title);
111 * @param {number} startTime
112 * @param {number} endTime
113 * @return {?Array.<number>}
115 dividerOffsets: function(startTime, endTime)
122 * @param {number} index
125 markerColor: function(index)
127 var event = this._markerEvents[index];
128 return WebInspector.TracingTimelineUIUtils.markerEventColor(event.name);
133 * @param {number} index
136 markerTitle: function(index)
138 var event = this._markerEvents[index];
139 return WebInspector.TracingTimelineUIUtils.eventTitle(event, this._model);
144 this._timelineData = null;
145 /** @type {!Array.<!WebInspector.TracingModel.Event>} */
146 this._entryEvents = [];
147 this._entryIndexToTitle = {};
148 this._markerEvents = [];
149 this._entryIndexToFrame = {};
153 * @return {!WebInspector.FlameChart.TimelineData}
155 timelineData: function()
157 if (this._timelineData)
158 return this._timelineData;
160 this._timelineData = new WebInspector.FlameChart.TimelineData([], [], []);
162 this._minimumBoundary = this._model.minimumRecordTime();
163 this._timeSpan = this._model.isEmpty() ? 1000 : this._model.maximumRecordTime() - this._minimumBoundary;
164 this._currentLevel = 0;
165 this._appendFrameBars(this._frameModel.frames());
166 this._appendThreadTimelineData(WebInspector.UIString("Main Thread"), this._model.mainThreadEvents());
167 var threads = this._model.virtualThreads();
168 for (var i = 0; i < threads.length; i++) {
169 var thread = threads[i];
170 this._appendThreadTimelineData(thread.name, thread.events);
172 return this._timelineData;
176 * @param {string} headerName
177 * @param {!Array.<!WebInspector.TracingModel.Event>} traceEvents
179 _appendThreadTimelineData: function(headerName, traceEvents)
181 var maxStackDepth = 0;
183 var headerAppended = false;
184 var events = traceEvents;
185 if (WebInspector.experimentsSettings.timelineJSCPUProfile.isEnabled()) {
186 var jsFrameEvents = this._generateJSFrameEvents(traceEvents);
187 events = jsFrameEvents.mergeOrdered(traceEvents, WebInspector.TracingModel.Event.orderedCompareStartTime);
189 for (var i = 0; i < events.length; ++i) {
191 // FIXME: clean up once phase name is unified between Blink and Chromium.
192 if (!e.endTime && e.phase !== WebInspector.TracingModel.Phase.Instant && e.phase !== "I")
194 if (WebInspector.TracingTimelineUIUtils.isMarkerEvent(e)) {
195 this._markerEvents.push(e);
196 this._timelineData.markerTimestamps.push(e.startTime);
198 if (!this._isVisible(e))
200 while (openEvents.length && openEvents.peekLast().endTime <= e.startTime)
202 if (!headerAppended) {
203 this._appendHeaderRecord(headerName, this._currentLevel++);
204 headerAppended = true;
206 this._appendEvent(e, this._currentLevel + openEvents.length);
207 maxStackDepth = Math.max(maxStackDepth, openEvents.length + 1);
211 this._currentLevel += maxStackDepth;
213 ++this._currentLevel;
217 * @param {!Array.<!WebInspector.TimelineFrame>} frames
219 _appendFrameBars: function(frames)
221 this._frameBarsLevel = this._currentLevel++;
222 for (var i = 0; i < frames.length; ++i)
223 this._appendFrame(frames[i]);
227 * @param {!Array.<!WebInspector.TracingModel.Event>} events
228 * @return {!Array.<!WebInspector.TracingModel.Event>}
230 _generateJSFrameEvents: function(events)
232 function equalFrames(frame1, frame2)
234 return frame1.scriptId === frame2.scriptId && frame1.functionName === frame2.functionName;
236 function eventEndTime(e)
238 return e.endTime || e.startTime + WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs;
240 function isJSInvocationEvent(e)
243 case WebInspector.TracingTimelineModel.RecordType.FunctionCall:
244 case WebInspector.TracingTimelineModel.RecordType.EvaluateScript:
249 var jsFrameEvents = [];
250 var stackTraceOpenEvents = [];
251 var currentJSInvocationEndTime = 0;
252 var coalesceThresholdMs = WebInspector.TimelineFlameChartDataProvider.JSFrameCoalsceThresholdMs;
253 for (var i = 0; i < events.length; ++i) {
255 if (e.startTime >= currentJSInvocationEndTime) {
256 stackTraceOpenEvents.length = 0;
257 currentJSInvocationEndTime = 0;
259 if (isJSInvocationEvent(e))
260 currentJSInvocationEndTime = e.endTime;
261 if (!currentJSInvocationEndTime)
265 while (stackTraceOpenEvents.length && eventEndTime(stackTraceOpenEvents.peekLast()) + coalesceThresholdMs <= e.startTime)
266 stackTraceOpenEvents.pop();
267 var numFrames = e.stackTrace.length;
268 for (var j = 0; j < numFrames && j < stackTraceOpenEvents.length; ++j) {
269 var frame = e.stackTrace[numFrames - 1 - j];
270 if (!equalFrames(frame, stackTraceOpenEvents[j].args["data"]))
272 stackTraceOpenEvents[j].endTime = Math.max(stackTraceOpenEvents[j].endTime, eventEndTime(e));
273 stackTraceOpenEvents[j].duration = stackTraceOpenEvents[j].endTime - stackTraceOpenEvents[j].startTime;
275 stackTraceOpenEvents.length = j;
276 var timestampUs = e.startTime * 1000;
277 var durationUs = (e.duration || WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs) * 1000;
278 for (; j < numFrames; ++j) {
279 var frame = e.stackTrace[numFrames - 1 - j];
280 var payload = /** @type {!WebInspector.TracingModel.EventPayload} */ ({
282 cat: WebInspector.TracingModel.DevToolsMetadataEventCategory,
283 name: WebInspector.TracingTimelineModel.RecordType.JSFrame,
290 var jsFrameEvent = new WebInspector.TracingModel.Event(payload, 0, e.thread);
291 stackTraceOpenEvents.push(jsFrameEvent);
292 jsFrameEvents.push(jsFrameEvent);
295 return jsFrameEvents;
299 * @param {!WebInspector.TracingTimelineModel.Filter} filter
301 addFilter: function(filter)
303 this._filters.push(filter);
307 * @param {!WebInspector.TracingModel.Event} event
310 _isVisible: function(event)
312 return this._filters.every(function (filter) { return filter.accept(event); });
318 minimumBoundary: function()
320 return this._minimumBoundary;
326 totalTime: function()
328 return this._timeSpan;
334 maxStackDepth: function()
336 return this._currentLevel;
340 * @param {number} entryIndex
341 * @return {?Array.<!{title: string, text: string}>}
343 prepareHighlightedEntryInfo: function(entryIndex)
349 * @param {number} entryIndex
352 canJumpToEntry: function(entryIndex)
358 * @param {number} entryIndex
361 entryColor: function(entryIndex)
363 var event = this._entryEvents[entryIndex];
365 return this._entryIndexToFrame[entryIndex] ? "white" : "#555";
366 if (event.name === WebInspector.TracingTimelineModel.RecordType.JSFrame)
367 return WebInspector.TimelineFlameChartDataProvider.jsFrameColorGenerator().colorForID(event.args["data"]["functionName"]);
368 var style = WebInspector.TracingTimelineUIUtils.styleForTraceEvent(event.name);
369 return style.category.fillColorStop1;
373 * @param {number} entryIndex
374 * @param {!CanvasRenderingContext2D} context
375 * @param {?string} text
376 * @param {number} barX
377 * @param {number} barY
378 * @param {number} barWidth
379 * @param {number} barHeight
380 * @param {function(number):number} offsetToPosition
383 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, offsetToPosition)
385 var frame = this._entryIndexToFrame[entryIndex];
389 context.translate(0.5, 0.5);
392 context.moveTo(barX, barY);
393 context.lineTo(barX, context.canvas.height);
394 context.strokeStyle = "rgba(100, 100, 100, 0.4)";
395 context.setLineDash([5]);
397 context.setLineDash([]);
400 var padding = 4 * window.devicePixelRatio;
402 barWidth -= 2 * padding;
404 barHeight -= 2 * padding;
407 var radiusY = cornerRadis;
408 var radiusX = Math.min(cornerRadis, barWidth / 2);
411 context.moveTo(barX + radiusX, barY);
412 context.lineTo(barX + barWidth - radiusX, barY);
413 context.quadraticCurveTo(barX + barWidth, barY, barX + barWidth, barY + radiusY);
414 context.lineTo(barX + barWidth, barY + barHeight - radiusY);
415 context.quadraticCurveTo(barX + barWidth, barY + barHeight, barX + barWidth - radiusX, barY + barHeight);
416 context.lineTo(barX + radiusX, barY + barHeight);
417 context.quadraticCurveTo(barX, barY + barHeight, barX, barY + barHeight - radiusY);
418 context.lineTo(barX, barY + radiusY);
419 context.quadraticCurveTo(barX, barY, barX + radiusX, barY);
422 context.fillStyle = "rgba(200, 200, 200, 0.8)";
424 context.strokeStyle = "rgba(150, 150, 150, 0.8)";
427 var frameDurationText = Number.millisToString(frame.duration, true);
428 var textWidth = context.measureText(frameDurationText).width;
429 if (barWidth > textWidth) {
430 context.fillStyle = "#555";
431 context.fillText(frameDurationText, barX + ((barWidth - textWidth) >> 1), barY + barHeight - 2);
439 // Paint text using white color on dark background.
442 context.fillStyle = "white";
443 context.shadowColor = "rgba(0, 0, 0, 0.1)";
444 context.shadowOffsetX = 1;
445 context.shadowOffsetY = 1;
446 context.font = this._font;
447 context.fillText(text, barX + this.textPadding(), barY + barHeight - this.textBaseline());
451 var event = this._entryEvents[entryIndex];
452 if (event && event.warning) {
455 context.rect(barX, barY, barWidth, this.barHeight());
459 context.fillStyle = "red";
460 context.moveTo(barX + barWidth - 15, barY + 1);
461 context.lineTo(barX + barWidth - 1, barY + 1);
462 context.lineTo(barX + barWidth - 1, barY + 15);
472 * @param {number} entryIndex
475 forceDecoration: function(entryIndex)
477 var event = this._entryEvents[entryIndex];
479 return !!this._entryIndexToFrame[entryIndex];
480 return !!event.warning;
484 * @param {number} entryIndex
485 * @return {?{startTime: number, endTime: number}}
487 highlightTimeRange: function(entryIndex)
489 var event = this._entryEvents[entryIndex] || this._entryIndexToFrame[entryIndex];
493 startTime: event.startTime,
494 endTime: event.endTime || event.startTime + WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs
501 paddingLeft: function()
507 * @param {number} entryIndex
510 textColor: function(entryIndex)
516 * @param {string} title
517 * @param {number} level
519 _appendHeaderRecord: function(title, level)
521 var index = this._entryEvents.length;
522 this._entryIndexToTitle[index] = title;
523 this._entryEvents.push(null);
524 this._timelineData.entryLevels[index] = level;
525 this._timelineData.entryTotalTimes[index] = this._timeSpan;
526 this._timelineData.entryStartTimes[index] = this._minimumBoundary;
530 * @param {!WebInspector.TracingModel.Event} event
531 * @param {number} level
533 _appendEvent: function(event, level)
535 var index = this._entryEvents.length;
536 this._entryEvents.push(event);
537 this._timelineData.entryLevels[index] = level;
538 this._timelineData.entryTotalTimes[index] = event.duration || WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs;
539 this._timelineData.entryStartTimes[index] = event.startTime;
543 * @param {!WebInspector.TimelineFrame} frame
545 _appendFrame: function(frame)
547 var index = this._entryEvents.length;
548 this._entryEvents.push(null);
549 this._entryIndexToFrame[index] = frame;
550 this._entryIndexToTitle[index] = Number.millisToString(frame.duration, true);
551 this._timelineData.entryLevels[index] = this._frameBarsLevel;
552 this._timelineData.entryTotalTimes[index] = frame.duration;
553 this._timelineData.entryStartTimes[index] = frame.startTime;
557 * @param {number} entryIndex
558 * @return {?WebInspector.TimelineSelection}
560 createSelection: function(entryIndex)
562 var event = this._entryEvents[entryIndex];
564 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromTraceEvent(event), entryIndex);
565 return this._lastSelection.timelineSelection;
567 var frame = this._entryIndexToFrame[entryIndex];
569 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromFrame(frame), entryIndex);
570 return this._lastSelection.timelineSelection;
576 * @param {?WebInspector.TimelineSelection} selection
579 entryIndexForSelection: function(selection)
584 if (this._lastSelection && this._lastSelection.timelineSelection.object() === selection.object())
585 return this._lastSelection.entryIndex;
586 switch (selection.type()) {
587 case WebInspector.TimelineSelection.Type.TraceEvent:
588 var event = /** @type{!WebInspector.TracingModel.Event} */ (selection.object());
589 var entryEvents = this._entryEvents;
590 for (var entryIndex = 0; entryIndex < entryEvents.length; ++entryIndex) {
591 if (entryEvents[entryIndex] === event) {
592 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromTraceEvent(event), entryIndex);
597 case WebInspector.TimelineSelection.Type.Frame:
598 var frame = /** @type {!WebInspector.TimelineFrame} */ (selection.object());
599 for (var frameIndex in this._entryIndexToFrame) {
600 if (this._entryIndexToFrame[frameIndex] === frame) {
601 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromFrame(frame), Number(frameIndex));
602 return Number(frameIndex);
612 * @return {!WebInspector.FlameChart.ColorGenerator}
614 WebInspector.TimelineFlameChartDataProvider.jsFrameColorGenerator = function()
616 if (!WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator) {
617 var hueSpace = { min: 30, max: 55, count: 5 };
618 var satSpace = { min: 70, max: 100, count: 6 };
619 var colorGenerator = new WebInspector.FlameChart.ColorGenerator(hueSpace, satSpace, 50);
620 colorGenerator.setColorForID("(idle)", "hsl(0, 0%, 60%)");
621 colorGenerator.setColorForID("(program)", "hsl(0, 0%, 60%)");
622 colorGenerator.setColorForID("(garbage collector)", "hsl(0, 0%, 60%)");
623 WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator = colorGenerator;
625 return WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator;
630 * @extends {WebInspector.VBox}
631 * @implements {WebInspector.TimelineModeView}
632 * @implements {WebInspector.FlameChartDelegate}
633 * @param {!WebInspector.TimelineModeViewDelegate} delegate
634 * @param {!WebInspector.TracingTimelineModel} tracingModel
635 * @param {!WebInspector.TimelineFrameModelBase} frameModel
637 WebInspector.TimelineFlameChart = function(delegate, tracingModel, frameModel)
639 WebInspector.VBox.call(this);
640 this.element.classList.add("timeline-flamechart");
641 this.registerRequiredCSS("flameChart.css");
642 this._delegate = delegate;
643 this._model = tracingModel;
644 this._dataProvider = new WebInspector.TimelineFlameChartDataProvider(tracingModel, frameModel)
645 this._mainView = new WebInspector.FlameChart(this._dataProvider, this, true);
646 this._mainView.show(this.element);
647 this._model.addEventListener(WebInspector.TimelineModel.Events.RecordingStarted, this._onRecordingStarted, this);
648 this._mainView.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
651 WebInspector.TimelineFlameChart.prototype = {
654 this._model.removeEventListener(WebInspector.TimelineModel.Events.RecordingStarted, this._onRecordingStarted, this);
655 this._mainView.removeEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
659 * @param {number} windowStartTime
660 * @param {number} windowEndTime
662 requestWindowTimes: function(windowStartTime, windowEndTime)
664 this._delegate.requestWindowTimes(windowStartTime, windowEndTime);
668 * @param {?RegExp} textFilter
670 refreshRecords: function(textFilter)
672 this._dataProvider.reset();
673 this._mainView.scheduleUpdate();
678 this._mainView.scheduleUpdate();
682 * @return {!WebInspector.View}
691 this._automaticallySizeWindow = true;
692 this._dataProvider.reset();
693 this._mainView.reset();
694 this._mainView.setWindowTimes(0, Infinity);
697 _onRecordingStarted: function()
699 this._automaticallySizeWindow = true;
700 this._mainView.reset();
704 * @param {!WebInspector.TimelineModel.Record} record
706 addRecord: function(record)
708 this._dataProvider.reset();
709 if (this._automaticallySizeWindow) {
710 var minimumRecordTime = this._model.minimumRecordTime();
711 if (record.startTime() > (minimumRecordTime + 1000)) {
712 this._automaticallySizeWindow = false;
713 this._delegate.requestWindowTimes(minimumRecordTime, minimumRecordTime + 1000);
715 this._mainView.scheduleUpdate();
717 if (!this._pendingUpdateTimer)
718 this._pendingUpdateTimer = window.setTimeout(this._updateOnAddRecord.bind(this), 300);
722 _updateOnAddRecord: function()
724 delete this._pendingUpdateTimer;
725 this._mainView.scheduleUpdate();
729 * @param {number} startTime
730 * @param {number} endTime
732 setWindowTimes: function(startTime, endTime)
734 this._mainView.setWindowTimes(startTime, endTime);
735 this._delegate.select(null);
739 * @param {number} width
741 setSidebarSize: function(width)
746 * @param {?WebInspector.TimelineModel.Record} record
747 * @param {string=} regex
748 * @param {boolean=} selectRecord
750 highlightSearchResult: function(record, regex, selectRecord)
755 * @param {?WebInspector.TimelineSelection} selection
757 setSelection: function(selection)
759 var index = this._dataProvider.entryIndexForSelection(selection);
760 this._mainView.setSelectedEntry(index);
764 * @param {!WebInspector.Event} event
766 _onEntrySelected: function(event)
768 var entryIndex = /** @type{number} */ (event.data);
769 var timelineSelection = this._dataProvider.createSelection(entryIndex);
770 if (timelineSelection)
771 this._delegate.select(timelineSelection);
774 __proto__: WebInspector.VBox.prototype
779 * @param {!WebInspector.TimelineSelection} selection
780 * @param {number} entryIndex
782 WebInspector.TimelineFlameChart.Selection = function(selection, entryIndex)
784 this.timelineSelection = selection;
785 this.entryIndex = entryIndex;