UNPKG

strong-arc

Version:

A visual suite for the StrongLoop API Platform

778 lines (709 loc) 26.5 kB
/* * Copyright (C) 2014 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @constructor * @implements {WebInspector.FlameChartDataProvider} * @param {!WebInspector.TracingTimelineModel} model * @param {!WebInspector.TimelineFrameModelBase} frameModel */ WebInspector.TimelineFlameChartDataProvider = function(model, frameModel) { WebInspector.FlameChartDataProvider.call(this); this.reset(); this._model = model; this._frameModel = frameModel; this._font = "12px " + WebInspector.fontFamily(); this._linkifier = new WebInspector.Linkifier(); this._filters = []; this.addFilter(WebInspector.TracingTimelineUIUtils.hiddenEventsFilter()); this.addFilter(new WebInspector.TracingTimelineModel.ExclusiveEventNameFilter([WebInspector.TracingTimelineModel.RecordType.Program])); } WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs = 0.01; WebInspector.TimelineFlameChartDataProvider.JSFrameCoalsceThresholdMs = 1.1; WebInspector.TimelineFlameChartDataProvider.prototype = { /** * @return {number} */ barHeight: function() { return 20; }, /** * @return {number} */ textBaseline: function() { return 6; }, /** * @return {number} */ textPadding: function() { return 5; }, /** * @param {number} entryIndex * @return {string} */ entryFont: function(entryIndex) { return this._font; }, /** * @param {number} entryIndex * @return {?string} */ entryTitle: function(entryIndex) { var event = this._entryEvents[entryIndex]; if (event) { var name = WebInspector.TracingTimelineUIUtils.styleForTraceEvent(event.name).title; // TODO(yurys): support event dividers var details = WebInspector.TracingTimelineUIUtils.buildDetailsNodeForTraceEvent(event, this._linkifier); if (event.name === WebInspector.TracingTimelineModel.RecordType.JSFrame && details) return details.textContent; return details ? WebInspector.UIString("%s (%s)", name, details.textContent) : name; } var title = this._entryIndexToTitle[entryIndex]; if (!title) { title = WebInspector.UIString("Unexpected entryIndex %d", entryIndex); console.error(title); } return title; }, /** * @param {number} startTime * @param {number} endTime * @return {?Array.<number>} */ dividerOffsets: function(startTime, endTime) { return null; }, /** * @override * @param {number} index * @return {string} */ markerColor: function(index) { var event = this._markerEvents[index]; return WebInspector.TracingTimelineUIUtils.markerEventColor(event); }, /** * @override * @param {number} index * @return {string} */ markerTitle: function(index) { var event = this._markerEvents[index]; return WebInspector.TracingTimelineUIUtils.eventTitle(event, this._model); }, reset: function() { this._timelineData = null; /** @type {!Array.<!WebInspector.TracingModel.Event>} */ this._entryEvents = []; this._entryIndexToTitle = {}; this._markerEvents = []; this._entryIndexToFrame = {}; }, /** * @return {!WebInspector.FlameChart.TimelineData} */ timelineData: function() { if (this._timelineData) return this._timelineData; this._timelineData = new WebInspector.FlameChart.TimelineData([], [], []); this._minimumBoundary = this._model.minimumRecordTime(); this._timeSpan = this._model.isEmpty() ? 1000 : this._model.maximumRecordTime() - this._minimumBoundary; this._currentLevel = 0; this._appendFrameBars(this._frameModel.frames()); this._appendThreadTimelineData(WebInspector.UIString("Main Thread"), this._model.mainThreadEvents()); var threads = this._model.virtualThreads(); for (var i = 0; i < threads.length; i++) { var thread = threads[i]; this._appendThreadTimelineData(thread.name, thread.events); } return this._timelineData; }, /** * @param {string} headerName * @param {!Array.<!WebInspector.TracingModel.Event>} traceEvents */ _appendThreadTimelineData: function(headerName, traceEvents) { var maxStackDepth = 0; var openEvents = []; var headerAppended = false; var events = traceEvents; if (WebInspector.experimentsSettings.timelineJSCPUProfile.isEnabled()) { var jsFrameEvents = this._generateJSFrameEvents(traceEvents); events = jsFrameEvents.mergeOrdered(traceEvents, WebInspector.TracingModel.Event.orderedCompareStartTime); } for (var i = 0; i < events.length; ++i) { var e = events[i]; if (WebInspector.TracingTimelineUIUtils.isMarkerEvent(e)) { this._markerEvents.push(e); this._timelineData.markerTimestamps.push(e.startTime); } if (!e.endTime && e.phase !== WebInspector.TracingModel.Phase.Instant) continue; if (!this._isVisible(e)) continue; while (openEvents.length && openEvents.peekLast().endTime <= e.startTime) openEvents.pop(); if (!headerAppended) { this._appendHeaderRecord(headerName, this._currentLevel++); headerAppended = true; } this._appendEvent(e, this._currentLevel + openEvents.length); maxStackDepth = Math.max(maxStackDepth, openEvents.length + 1); if (e.endTime) openEvents.push(e); } this._currentLevel += maxStackDepth; if (headerAppended) ++this._currentLevel; }, /** * @param {!Array.<!WebInspector.TimelineFrame>} frames */ _appendFrameBars: function(frames) { this._frameBarsLevel = this._currentLevel++; for (var i = 0; i < frames.length; ++i) this._appendFrame(frames[i]); }, /** * @param {!Array.<!WebInspector.TracingModel.Event>} events * @return {!Array.<!WebInspector.TracingModel.Event>} */ _generateJSFrameEvents: function(events) { function equalFrames(frame1, frame2) { return frame1.scriptId === frame2.scriptId && frame1.functionName === frame2.functionName; } function eventEndTime(e) { return e.endTime || e.startTime + WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs; } function isJSInvocationEvent(e) { switch (e.name) { case WebInspector.TracingTimelineModel.RecordType.FunctionCall: case WebInspector.TracingTimelineModel.RecordType.EvaluateScript: return true; } return false; } var jsFrameEvents = []; var stackTraceOpenEvents = []; var currentJSInvocationEndTime = 0; var coalesceThresholdMs = WebInspector.TimelineFlameChartDataProvider.JSFrameCoalsceThresholdMs; for (var i = 0; i < events.length; ++i) { var e = events[i]; if (e.startTime >= currentJSInvocationEndTime) { stackTraceOpenEvents.length = 0; currentJSInvocationEndTime = 0; } if (isJSInvocationEvent(e)) currentJSInvocationEndTime = e.endTime; if (!currentJSInvocationEndTime) continue; if (!e.stackTrace) continue; while (stackTraceOpenEvents.length && eventEndTime(stackTraceOpenEvents.peekLast()) + coalesceThresholdMs <= e.startTime) stackTraceOpenEvents.pop(); var numFrames = e.stackTrace.length; for (var j = 0; j < numFrames && j < stackTraceOpenEvents.length; ++j) { var frame = e.stackTrace[numFrames - 1 - j]; if (!equalFrames(frame, stackTraceOpenEvents[j].args["data"])) break; stackTraceOpenEvents[j].endTime = Math.max(stackTraceOpenEvents[j].endTime, eventEndTime(e)); stackTraceOpenEvents[j].duration = stackTraceOpenEvents[j].endTime - stackTraceOpenEvents[j].startTime; } stackTraceOpenEvents.length = j; var duration = (e.duration || WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs); for (; j < numFrames; ++j) { var frame = e.stackTrace[numFrames - 1 - j]; var jsFrameEvent = new WebInspector.TracingModel.Event(WebInspector.TracingModel.DevToolsMetadataEventCategory, WebInspector.TracingTimelineModel.RecordType.JSFrame, e.phase, e.startTime, e.thread); jsFrameEvent.addArgs({data: frame}); jsFrameEvent.setEndTime(e.startTime + duration); stackTraceOpenEvents.push(jsFrameEvent); jsFrameEvents.push(jsFrameEvent); } } return jsFrameEvents; }, /** * @param {!WebInspector.TracingTimelineModel.Filter} filter */ addFilter: function(filter) { this._filters.push(filter); }, /** * @param {!WebInspector.TracingModel.Event} event * @return {boolean} */ _isVisible: function(event) { return this._filters.every(function (filter) { return filter.accept(event); }); }, /** * @return {number} */ minimumBoundary: function() { return this._minimumBoundary; }, /** * @return {number} */ totalTime: function() { return this._timeSpan; }, /** * @return {number} */ maxStackDepth: function() { return this._currentLevel; }, /** * @param {number} entryIndex * @return {?Array.<!{title: string, text: string}>} */ prepareHighlightedEntryInfo: function(entryIndex) { return null; }, /** * @param {number} entryIndex * @return {boolean} */ canJumpToEntry: function(entryIndex) { return false; }, /** * @param {number} entryIndex * @return {string} */ entryColor: function(entryIndex) { var event = this._entryEvents[entryIndex]; if (!event) return this._entryIndexToFrame[entryIndex] ? "white" : "#555"; if (event.name === WebInspector.TracingTimelineModel.RecordType.JSFrame) return WebInspector.TimelineFlameChartDataProvider.jsFrameColorGenerator().colorForID(event.args["data"]["functionName"]); var style = WebInspector.TracingTimelineUIUtils.styleForTraceEvent(event.name); return style.category.fillColorStop1; }, /** * @param {number} entryIndex * @param {!CanvasRenderingContext2D} context * @param {?string} text * @param {number} barX * @param {number} barY * @param {number} barWidth * @param {number} barHeight * @param {function(number):number} offsetToPosition * @return {boolean} */ decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, offsetToPosition) { var frame = this._entryIndexToFrame[entryIndex]; if (frame) { context.save(); context.translate(0.5, 0.5); context.beginPath(); context.moveTo(barX, barY); context.lineTo(barX, context.canvas.height); context.strokeStyle = "rgba(100, 100, 100, 0.4)"; context.setLineDash([5]); context.stroke(); context.setLineDash([]); var padding = 4 * window.devicePixelRatio; barX += padding; barWidth -= 2 * padding; barY += padding; barHeight -= 2 * padding; var cornerRadis = 3; var radiusY = cornerRadis; var radiusX = Math.min(cornerRadis, barWidth / 2); context.beginPath(); context.moveTo(barX + radiusX, barY); context.lineTo(barX + barWidth - radiusX, barY); context.quadraticCurveTo(barX + barWidth, barY, barX + barWidth, barY + radiusY); context.lineTo(barX + barWidth, barY + barHeight - radiusY); context.quadraticCurveTo(barX + barWidth, barY + barHeight, barX + barWidth - radiusX, barY + barHeight); context.lineTo(barX + radiusX, barY + barHeight); context.quadraticCurveTo(barX, barY + barHeight, barX, barY + barHeight - radiusY); context.lineTo(barX, barY + radiusY); context.quadraticCurveTo(barX, barY, barX + radiusX, barY); context.closePath(); context.fillStyle = "rgba(200, 200, 200, 0.8)"; context.fill(); context.strokeStyle = "rgba(150, 150, 150, 0.8)"; context.stroke(); var frameDurationText = Number.millisToString(frame.duration, true); var textWidth = context.measureText(frameDurationText).width; if (barWidth > textWidth) { context.fillStyle = "#555"; context.fillText(frameDurationText, barX + ((barWidth - textWidth) >> 1), barY + barHeight - 2); } context.restore(); return true; } if (barWidth < 5) return false; // Paint text using white color on dark background. if (text) { context.save(); context.fillStyle = "white"; context.shadowColor = "rgba(0, 0, 0, 0.1)"; context.shadowOffsetX = 1; context.shadowOffsetY = 1; context.font = this._font; context.fillText(text, barX + this.textPadding(), barY + barHeight - this.textBaseline()); context.restore(); } var event = this._entryEvents[entryIndex]; if (event && event.warning) { context.save(); context.rect(barX, barY, barWidth, this.barHeight()); context.clip(); context.beginPath(); context.fillStyle = "red"; context.moveTo(barX + barWidth - 15, barY + 1); context.lineTo(barX + barWidth - 1, barY + 1); context.lineTo(barX + barWidth - 1, barY + 15); context.fill(); context.restore(); } return true; }, /** * @param {number} entryIndex * @return {boolean} */ forceDecoration: function(entryIndex) { var event = this._entryEvents[entryIndex]; if (!event) return !!this._entryIndexToFrame[entryIndex]; return !!event.warning; }, /** * @param {number} entryIndex * @return {?{startTime: number, endTime: number}} */ highlightTimeRange: function(entryIndex) { var event = this._entryEvents[entryIndex] || this._entryIndexToFrame[entryIndex]; if (!event) return null; return { startTime: event.startTime, endTime: event.endTime || event.startTime + WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs } }, /** * @return {number} */ paddingLeft: function() { return 0; }, /** * @param {number} entryIndex * @return {string} */ textColor: function(entryIndex) { return "white"; }, /** * @param {string} title * @param {number} level */ _appendHeaderRecord: function(title, level) { var index = this._entryEvents.length; this._entryIndexToTitle[index] = title; this._entryEvents.push(null); this._timelineData.entryLevels[index] = level; this._timelineData.entryTotalTimes[index] = this._timeSpan; this._timelineData.entryStartTimes[index] = this._minimumBoundary; }, /** * @param {!WebInspector.TracingModel.Event} event * @param {number} level */ _appendEvent: function(event, level) { var index = this._entryEvents.length; this._entryEvents.push(event); this._timelineData.entryLevels[index] = level; this._timelineData.entryTotalTimes[index] = event.duration || WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs; this._timelineData.entryStartTimes[index] = event.startTime; }, /** * @param {!WebInspector.TimelineFrame} frame */ _appendFrame: function(frame) { var index = this._entryEvents.length; this._entryEvents.push(null); this._entryIndexToFrame[index] = frame; this._entryIndexToTitle[index] = Number.millisToString(frame.duration, true); this._timelineData.entryLevels[index] = this._frameBarsLevel; this._timelineData.entryTotalTimes[index] = frame.duration; this._timelineData.entryStartTimes[index] = frame.startTime; }, /** * @param {number} entryIndex * @return {?WebInspector.TimelineSelection} */ createSelection: function(entryIndex) { var event = this._entryEvents[entryIndex]; if (event) { this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromTraceEvent(event), entryIndex); return this._lastSelection.timelineSelection; } var frame = this._entryIndexToFrame[entryIndex]; if (frame) { this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromFrame(frame), entryIndex); return this._lastSelection.timelineSelection; } return null; }, /** * @param {?WebInspector.TimelineSelection} selection * @return {number} */ entryIndexForSelection: function(selection) { if (!selection) return -1; if (this._lastSelection && this._lastSelection.timelineSelection.object() === selection.object()) return this._lastSelection.entryIndex; switch (selection.type()) { case WebInspector.TimelineSelection.Type.TraceEvent: var event = /** @type{!WebInspector.TracingModel.Event} */ (selection.object()); var entryEvents = this._entryEvents; for (var entryIndex = 0; entryIndex < entryEvents.length; ++entryIndex) { if (entryEvents[entryIndex] === event) { this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromTraceEvent(event), entryIndex); return entryIndex; } } break; case WebInspector.TimelineSelection.Type.Frame: var frame = /** @type {!WebInspector.TimelineFrame} */ (selection.object()); for (var frameIndex in this._entryIndexToFrame) { if (this._entryIndexToFrame[frameIndex] === frame) { this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromFrame(frame), Number(frameIndex)); return Number(frameIndex); } } break; } return -1; } } /** * @return {!WebInspector.FlameChart.ColorGenerator} */ WebInspector.TimelineFlameChartDataProvider.jsFrameColorGenerator = function() { if (!WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator) { var hueSpace = { min: 30, max: 55, count: 5 }; var satSpace = { min: 70, max: 100, count: 6 }; var colorGenerator = new WebInspector.FlameChart.ColorGenerator(hueSpace, satSpace, 50); colorGenerator.setColorForID("(idle)", "hsl(0, 0%, 60%)"); colorGenerator.setColorForID("(program)", "hsl(0, 0%, 60%)"); colorGenerator.setColorForID("(garbage collector)", "hsl(0, 0%, 60%)"); WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator = colorGenerator; } return WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator; } /** * @constructor * @extends {WebInspector.VBox} * @implements {WebInspector.TimelineModeView} * @implements {WebInspector.FlameChartDelegate} * @param {!WebInspector.TimelineModeViewDelegate} delegate * @param {!WebInspector.TracingTimelineModel} tracingModel * @param {!WebInspector.TimelineFrameModelBase} frameModel */ WebInspector.TimelineFlameChart = function(delegate, tracingModel, frameModel) { WebInspector.VBox.call(this); this.element.classList.add("timeline-flamechart"); this.registerRequiredCSS("flameChart.css"); this._delegate = delegate; this._model = tracingModel; this._dataProvider = new WebInspector.TimelineFlameChartDataProvider(tracingModel, frameModel) this._mainView = new WebInspector.FlameChart(this._dataProvider, this, true); this._mainView.show(this.element); this._model.addEventListener(WebInspector.TimelineModel.Events.RecordingStarted, this._onRecordingStarted, this); this._mainView.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this); } WebInspector.TimelineFlameChart.prototype = { dispose: function() { this._model.removeEventListener(WebInspector.TimelineModel.Events.RecordingStarted, this._onRecordingStarted, this); this._mainView.removeEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this); }, /** * @param {number} windowStartTime * @param {number} windowEndTime */ requestWindowTimes: function(windowStartTime, windowEndTime) { this._delegate.requestWindowTimes(windowStartTime, windowEndTime); }, /** * @param {?RegExp} textFilter */ refreshRecords: function(textFilter) { this._dataProvider.reset(); this._mainView.scheduleUpdate(); }, wasShown: function() { this._mainView.scheduleUpdate(); }, /** * @return {!WebInspector.View} */ view: function() { return this; }, reset: function() { this._automaticallySizeWindow = true; this._dataProvider.reset(); this._mainView.reset(); this._mainView.setWindowTimes(0, Infinity); }, _onRecordingStarted: function() { this._automaticallySizeWindow = true; this._mainView.reset(); }, /** * @param {!WebInspector.TimelineModel.Record} record */ addRecord: function(record) { this._dataProvider.reset(); if (this._automaticallySizeWindow) { var minimumRecordTime = this._model.minimumRecordTime(); if (record.startTime() > (minimumRecordTime + 1000)) { this._automaticallySizeWindow = false; this._delegate.requestWindowTimes(minimumRecordTime, minimumRecordTime + 1000); } this._mainView.scheduleUpdate(); } else { if (!this._pendingUpdateTimer) this._pendingUpdateTimer = window.setTimeout(this._updateOnAddRecord.bind(this), 300); } }, _updateOnAddRecord: function() { delete this._pendingUpdateTimer; this._mainView.scheduleUpdate(); }, /** * @param {number} startTime * @param {number} endTime */ setWindowTimes: function(startTime, endTime) { this._mainView.setWindowTimes(startTime, endTime); this._delegate.select(null); }, /** * @param {number} width */ setSidebarSize: function(width) { }, /** * @param {?WebInspector.TimelineModel.Record} record * @param {string=} regex * @param {boolean=} selectRecord */ highlightSearchResult: function(record, regex, selectRecord) { }, /** * @param {?WebInspector.TimelineSelection} selection */ setSelection: function(selection) { var index = this._dataProvider.entryIndexForSelection(selection); this._mainView.setSelectedEntry(index); }, /** * @param {!WebInspector.Event} event */ _onEntrySelected: function(event) { var entryIndex = /** @type{number} */ (event.data); var timelineSelection = this._dataProvider.createSelection(entryIndex); if (timelineSelection) this._delegate.select(timelineSelection); }, __proto__: WebInspector.VBox.prototype } /** * @constructor * @param {!WebInspector.TimelineSelection} selection * @param {number} entryIndex */ WebInspector.TimelineFlameChart.Selection = function(selection, entryIndex) { this.timelineSelection = selection; this.entryIndex = entryIndex; }