Index: Source/devtools/front_end/CPUProfileModel.js |
diff --git a/Source/devtools/front_end/CPUProfileModel.js b/Source/devtools/front_end/CPUProfileModel.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4f6f785756a56ff942e25636dc7d9f8b9b29846d |
--- /dev/null |
+++ b/Source/devtools/front_end/CPUProfileModel.js |
@@ -0,0 +1,445 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+ |
+/** |
+ * @constructor |
+ * @param {!ProfilerAgent.CPUProfile} profile |
+ */ |
+WebInspector.CPUProfileDataModel = function(profile) |
+{ |
+ this.profileHead = profile.head; |
+ this.samples = profile.samples; |
+ this._calculateTimes(profile); |
+ this._assignParentsInProfile(); |
+ if (this.samples) |
+ this._buildIdToNodeMap(); |
+} |
+ |
+WebInspector.CPUProfileDataModel.prototype = { |
+ /** |
+ * @param {!ProfilerAgent.CPUProfile} profile |
+ */ |
+ _calculateTimes: function(profile) |
+ { |
+ function totalHitCount(node) { |
+ var result = node.hitCount; |
+ for (var i = 0; i < node.children.length; i++) |
+ result += totalHitCount(node.children[i]); |
+ return result; |
+ } |
+ profile.totalHitCount = totalHitCount(profile.head); |
+ |
+ var durationMs = 1000 * (profile.endTime - profile.startTime); |
+ var samplingInterval = durationMs / profile.totalHitCount; |
+ this.samplingIntervalMs = samplingInterval; |
+ |
+ function calculateTimesForNode(node) { |
+ node.selfTime = node.hitCount * samplingInterval; |
+ var totalHitCount = node.hitCount; |
+ for (var i = 0; i < node.children.length; i++) |
+ totalHitCount += calculateTimesForNode(node.children[i]); |
+ node.totalTime = totalHitCount * samplingInterval; |
+ return totalHitCount; |
+ } |
+ calculateTimesForNode(profile.head); |
+ }, |
+ |
+ _assignParentsInProfile: function() |
+ { |
+ var head = this.profileHead; |
+ head.parent = null; |
+ head.head = null; |
+ var nodesToTraverse = [ head ]; |
+ while (nodesToTraverse.length) { |
+ var parent = nodesToTraverse.pop(); |
+ var children = parent.children; |
+ var length = children.length; |
+ for (var i = 0; i < length; ++i) { |
+ var child = children[i]; |
+ child.head = head; |
+ child.parent = parent; |
+ if (child.children.length) |
+ nodesToTraverse.push(child); |
+ } |
+ } |
+ }, |
+ |
+ _buildIdToNodeMap: function() |
+ { |
+ /** @type {!Object.<number, !ProfilerAgent.CPUProfileNode>} */ |
+ this._idToNode = {}; |
+ var idToNode = this._idToNode; |
+ var stack = [this.profileHead]; |
+ while (stack.length) { |
+ var node = stack.pop(); |
+ idToNode[node.id] = node; |
+ for (var i = 0; i < node.children.length; i++) |
+ stack.push(node.children[i]); |
+ } |
+ |
+ var topLevelNodes = this.profileHead.children; |
+ for (var i = 0; i < topLevelNodes.length; i++) { |
+ var node = topLevelNodes[i]; |
+ if (node.functionName === "(garbage collector)") { |
+ this._gcNode = node; |
+ break; |
+ } |
+ } |
+ } |
+} |
+ |
+ |
+/** |
+ * @constructor |
+ * @implements {WebInspector.FlameChartDataProvider} |
+ * @param {!WebInspector.CPUProfileDataModel} cpuProfile |
+ * @param {!WebInspector.Target} target |
+ */ |
+WebInspector.CPUFlameChartDataProvider = function(cpuProfile, target) |
+{ |
+ WebInspector.FlameChartDataProvider.call(this); |
+ this._cpuProfile = cpuProfile; |
+ this._target = target; |
+ this._colorGenerator = WebInspector.CPUProfileView.colorGenerator(); |
+} |
+ |
+WebInspector.CPUFlameChartDataProvider.prototype = { |
+ /** |
+ * @return {number} |
+ */ |
+ barHeight: function() |
+ { |
+ return 15; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ textBaseline: function() |
+ { |
+ return 4; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ textPadding: function() |
+ { |
+ return 2; |
+ }, |
+ |
+ /** |
+ * @param {number} startTime |
+ * @param {number} endTime |
+ * @return {?Array.<number>} |
+ */ |
+ dividerOffsets: function(startTime, endTime) |
+ { |
+ return null; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ zeroTime: function() |
+ { |
+ return 0; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ totalTime: function() |
+ { |
+ return this._cpuProfile.profileHead.totalTime; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ maxStackDepth: function() |
+ { |
+ return this._maxStackDepth; |
+ }, |
+ |
+ /** |
+ * @return {?WebInspector.FlameChart.TimelineData} |
+ */ |
+ timelineData: function() |
+ { |
+ return this._timelineData || this._calculateTimelineData(); |
+ }, |
+ |
+ /** |
+ * @return {?WebInspector.FlameChart.TimelineData} |
+ */ |
+ _calculateTimelineData: function() |
+ { |
+ if (!this._cpuProfile.profileHead) |
+ return null; |
+ |
+ var samples = this._cpuProfile.samples; |
+ var idToNode = this._cpuProfile._idToNode; |
+ var gcNode = this._cpuProfile._gcNode; |
+ var samplesCount = samples.length; |
+ var samplingInterval = this._cpuProfile.samplingIntervalMs; |
+ |
+ var index = 0; |
+ |
+ var openIntervals = []; |
+ var stackTrace = []; |
+ var maxDepth = 5; // minimum stack depth for the case when we see no activity. |
+ var depth = 0; |
+ |
+ /** |
+ * @constructor |
+ * @param {number} depth |
+ * @param {number} duration |
+ * @param {number} startTime |
+ * @param {!Object} node |
+ */ |
+ function ChartEntry(depth, duration, startTime, node) |
+ { |
+ this.depth = depth; |
+ this.duration = duration; |
+ this.startTime = startTime; |
+ this.node = node; |
+ this.selfTime = 0; |
+ } |
+ var entries = /** @type {!Array.<!ChartEntry>} */ ([]); |
+ |
+ for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) { |
+ var node = idToNode[samples[sampleIndex]]; |
+ stackTrace.length = 0; |
+ while (node) { |
+ stackTrace.push(node); |
+ node = node.parent; |
+ } |
+ stackTrace.pop(); // Remove (root) node |
+ |
+ maxDepth = Math.max(maxDepth, depth); |
+ depth = 0; |
+ node = stackTrace.pop(); |
+ var intervalIndex; |
+ |
+ // GC samples have no stack, so we just put GC node on top of the last recoreded sample. |
+ if (node === gcNode) { |
+ while (depth < openIntervals.length) { |
+ intervalIndex = openIntervals[depth].index; |
+ entries[intervalIndex].duration += samplingInterval; |
+ ++depth; |
+ } |
+ // If previous stack is also GC then just continue. |
+ if (openIntervals.length > 0 && openIntervals.peekLast().node === node) { |
+ entries[intervalIndex].selfTime += samplingInterval; |
+ continue; |
+ } |
+ } |
+ |
+ while (node && depth < openIntervals.length && node === openIntervals[depth].node) { |
+ intervalIndex = openIntervals[depth].index; |
+ entries[intervalIndex].duration += samplingInterval; |
+ node = stackTrace.pop(); |
+ ++depth; |
+ } |
+ if (depth < openIntervals.length) |
+ openIntervals.length = depth; |
+ if (!node) { |
+ entries[intervalIndex].selfTime += samplingInterval; |
+ continue; |
+ } |
+ |
+ var colorGenerator = this._colorGenerator; |
+ var color = ""; |
+ while (node) { |
+ entries.push(new ChartEntry(depth, samplingInterval, sampleIndex * samplingInterval, node)); |
+ openIntervals.push({node: node, index: index}); |
+ ++index; |
+ |
+ node = stackTrace.pop(); |
+ ++depth; |
+ } |
+ entries[entries.length - 1].selfTime += samplingInterval; |
+ } |
+ |
+ /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ |
+ var entryNodes = new Array(entries.length); |
+ var entryLevels = new Uint8Array(entries.length); |
+ var entryTotalTimes = new Float32Array(entries.length); |
+ var entrySelfTimes = new Float32Array(entries.length); |
+ var entryOffsets = new Float32Array(entries.length); |
+ |
+ for (var i = 0; i < entries.length; ++i) { |
+ var entry = entries[i]; |
+ entryNodes[i] = entry.node; |
+ entryLevels[i] = entry.depth; |
+ entryTotalTimes[i] = entry.duration; |
+ entryOffsets[i] = entry.startTime; |
+ entrySelfTimes[i] = entry.selfTime; |
+ } |
+ |
+ this._maxStackDepth = Math.max(maxDepth, depth); |
+ |
+ this._timelineData = { |
+ entryLevels: entryLevels, |
+ entryTotalTimes: entryTotalTimes, |
+ entryOffsets: entryOffsets, |
+ }; |
+ |
+ /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ |
+ this._entryNodes = entryNodes; |
+ this._entrySelfTimes = entrySelfTimes; |
+ |
+ return /** @type {!WebInspector.FlameChart.TimelineData} */ (this._timelineData); |
+ }, |
+ |
+ /** |
+ * @param {number} ms |
+ * @return {string} |
+ */ |
+ _millisecondsToString: function(ms) |
+ { |
+ if (ms === 0) |
+ return "0"; |
+ if (ms < 1000) |
+ return WebInspector.UIString("%.1f\u2009ms", ms); |
+ return Number.secondsToString(ms / 1000, true); |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {?Array.<!{title: string, text: string}>} |
+ */ |
+ prepareHighlightedEntryInfo: function(entryIndex) |
+ { |
+ var timelineData = this._timelineData; |
+ var node = this._entryNodes[entryIndex]; |
+ if (!node) |
+ return null; |
+ |
+ var entryInfo = []; |
+ function pushEntryInfoRow(title, text) |
+ { |
+ var row = {}; |
+ row.title = title; |
+ row.text = text; |
+ entryInfo.push(row); |
+ } |
+ |
+ pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName); |
+ var selfTime = this._millisecondsToString(this._entrySelfTimes[entryIndex]); |
+ var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]); |
+ pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime); |
+ pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime); |
+ var target = this._target; |
+ var text = WebInspector.Linkifier.liveLocationText(target, node.scriptId, node.lineNumber, node.columnNumber); |
+ pushEntryInfoRow(WebInspector.UIString("URL"), text); |
+ pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true)); |
+ pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true)); |
+ if (node.deoptReason && node.deoptReason !== "no reason") |
+ pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason); |
+ |
+ return entryInfo; |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {boolean} |
+ */ |
+ canJumpToEntry: function(entryIndex) |
+ { |
+ return this._entryNodes[entryIndex].scriptId !== "0"; |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {?string} |
+ */ |
+ entryTitle: function(entryIndex) |
+ { |
+ var node = this._entryNodes[entryIndex]; |
+ return node.functionName; |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {?string} |
+ */ |
+ entryFont: function(entryIndex) |
+ { |
+ if (!this._font) { |
+ this._font = (this.barHeight() - 4) + "px " + WebInspector.fontFamily(); |
+ this._boldFont = "bold " + this._font; |
+ } |
+ var node = this._entryNodes[entryIndex]; |
+ var reason = node.deoptReason; |
+ return (reason && reason !== "no reason") ? this._boldFont : this._font; |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {!string} |
+ */ |
+ entryColor: function(entryIndex) |
+ { |
+ var node = this._entryNodes[entryIndex]; |
+ return this._colorGenerator.colorForID(node.functionName + ":" + node.url + ":" + node.lineNumber); |
+ }, |
+ |
+ /** |
+ * @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) |
+ { |
+ return false; |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {boolean} |
+ */ |
+ forceDecoration: function(entryIndex) |
+ { |
+ return false; |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {!{startTimeOffset: number, endTimeOffset: number}} |
+ */ |
+ highlightTimeRange: function(entryIndex) |
+ { |
+ var startTimeOffset = this._timelineData.entryOffsets[entryIndex]; |
+ return { |
+ startTimeOffset: startTimeOffset, |
+ endTimeOffset: startTimeOffset + this._timelineData.entryTotalTimes[entryIndex] |
+ }; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ paddingLeft: function() |
+ { |
+ return 15; |
+ }, |
+ |
+ /** |
+ * @param {number} entryIndex |
+ * @return {!string} |
+ */ |
+ textColor: function(entryIndex) |
+ { |
+ return "#333"; |
+ } |
+} |