blob: df2facfbcb797fc8d1841dc1ff1f4f77728b7653 [file] [log] [blame]
// 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.
export class TimelineJSProfileProcessor {
/**
* @param {!SDK.CPUProfileDataModel} jsProfileModel
* @param {!SDK.TracingModel.Thread} thread
* @return {!Array<!SDK.TracingModel.Event>}
*/
static generateTracingEventsFromCpuProfile(jsProfileModel, thread) {
const idleNode = jsProfileModel.idleNode;
const programNode = jsProfileModel.programNode;
const gcNode = jsProfileModel.gcNode;
const samples = jsProfileModel.samples;
const timestamps = jsProfileModel.timestamps;
const jsEvents = [];
/** @type {!Map<!Object, !Array<!Protocol.Runtime.CallFrame>>} */
const nodeToStackMap = new Map();
nodeToStackMap.set(programNode, []);
for (let i = 0; i < samples.length; ++i) {
let node = jsProfileModel.nodeByIndex(i);
if (!node) {
console.error(`Node with unknown id ${samples[i]} at index ${i}`);
continue;
}
if (node === gcNode || node === idleNode) {
continue;
}
let callFrames = nodeToStackMap.get(node);
if (!callFrames) {
callFrames = /** @type {!Array<!Protocol.Runtime.CallFrame>} */ (new Array(node.depth + 1));
nodeToStackMap.set(node, callFrames);
for (let j = 0; node.parent; node = node.parent) {
callFrames[j++] = /** @type {!Protocol.Runtime.CallFrame} */ (node);
}
}
const jsSampleEvent = new SDK.TracingModel.Event(
SDK.TracingModel.DevToolsTimelineEventCategory, TimelineModel.TimelineModel.RecordType.JSSample,
SDK.TracingModel.Phase.Instant, timestamps[i], thread);
jsSampleEvent.args['data'] = {stackTrace: callFrames};
jsEvents.push(jsSampleEvent);
}
return jsEvents;
}
/**
* @param {!Array<!SDK.TracingModel.Event>} events
* @return {!Array<!SDK.TracingModel.Event>}
*/
static generateJSFrameEvents(events) {
/**
* @param {!Protocol.Runtime.CallFrame} frame1
* @param {!Protocol.Runtime.CallFrame} frame2
* @return {boolean}
*/
function equalFrames(frame1, frame2) {
return frame1.scriptId === frame2.scriptId &&
frame1.functionName === frame2.functionName &&
frame1.lineNumber === frame2.lineNumber;
}
/**
* @param {!SDK.TracingModel.Event} e
* @return {boolean}
*/
function isJSInvocationEvent(e) {
switch (e.name) {
case TimelineModel.TimelineModel.RecordType.RunMicrotasks:
case TimelineModel.TimelineModel.RecordType.FunctionCall:
case TimelineModel.TimelineModel.RecordType.EvaluateScript:
case TimelineModel.TimelineModel.RecordType.EvaluateModule:
case TimelineModel.TimelineModel.RecordType.EventDispatch:
case TimelineModel.TimelineModel.RecordType.V8Execute:
return true;
}
return false;
}
const jsFrameEvents = [];
const jsFramesStack = [];
const lockedJsStackDepth = [];
let ordinal = 0;
const showAllEvents = Root.Runtime.experiments.isEnabled('timelineShowAllEvents');
const showRuntimeCallStats = Root.Runtime.experiments.isEnabled('timelineV8RuntimeCallStats');
const showNativeFunctions = Common.moduleSetting('showNativeFunctionsInJSProfile').get();
/**
* @param {!SDK.TracingModel.Event} e
*/
function onStartEvent(e) {
e.ordinal = ++ordinal;
extractStackTrace(e);
// For the duration of the event we cannot go beyond the stack associated with it.
lockedJsStackDepth.push(jsFramesStack.length);
}
/**
* @param {!SDK.TracingModel.Event} e
* @param {?SDK.TracingModel.Event} parent
*/
function onInstantEvent(e, parent) {
e.ordinal = ++ordinal;
if (parent && isJSInvocationEvent(parent)) {
extractStackTrace(e);
}
}
/**
* @param {!SDK.TracingModel.Event} e
*/
function onEndEvent(e) {
truncateJSStack(lockedJsStackDepth.pop(), e.endTime);
}
/**
* @param {number} depth
* @param {number} time
*/
function truncateJSStack(depth, time) {
if (lockedJsStackDepth.length) {
const lockedDepth = lockedJsStackDepth.peekLast();
if (depth < lockedDepth) {
console.error(`Child stack is shallower (${depth}) than the parent stack (${lockedDepth}) at ${time}`);
depth = lockedDepth;
}
}
if (jsFramesStack.length < depth) {
console.error(`Trying to truncate higher than the current stack size at ${time}`);
depth = jsFramesStack.length;
}
for (let k = 0; k < jsFramesStack.length; ++k) {
jsFramesStack[k].setEndTime(time);
}
jsFramesStack.length = depth;
}
/**
* @param {string} name
* @return {boolean}
*/
function showNativeName(name) {
return showRuntimeCallStats && !!TimelineJSProfileProcessor.nativeGroup(name);
}
/**
* @param {!Array<!Protocol.Runtime.CallFrame>} stack
*/
function filterStackFrames(stack) {
if (showAllEvents) {
return;
}
let previousNativeFrameName = null;
let j = 0;
for (let i = 0; i < stack.length; ++i) {
const frame = stack[i];
const url = frame.url;
const isNativeFrame = url && url.startsWith('native ');
if (!showNativeFunctions && isNativeFrame) {
continue;
}
const isNativeRuntimeFrame = TimelineJSProfileProcessor.isNativeRuntimeFrame(frame);
if (isNativeRuntimeFrame && !showNativeName(frame.functionName)) {
continue;
}
const nativeFrameName =
isNativeRuntimeFrame ? TimelineJSProfileProcessor.nativeGroup(frame.functionName) : null;
if (previousNativeFrameName && previousNativeFrameName === nativeFrameName) {
continue;
}
previousNativeFrameName = nativeFrameName;
stack[j++] = frame;
}
stack.length = j;
}
/**
* @param {!SDK.TracingModel.Event} e
*/
function extractStackTrace(e) {
const recordTypes = TimelineModel.TimelineModel.RecordType;
/** @type {!Array<!Protocol.Runtime.CallFrame>} */
const callFrames = e.name === recordTypes.JSSample ? e.args['data']['stackTrace'].slice().reverse() :
jsFramesStack.map(frameEvent => frameEvent.args['data']);
filterStackFrames(callFrames);
const endTime = e.endTime || e.startTime;
const minFrames = Math.min(callFrames.length, jsFramesStack.length);
let i;
for (i = lockedJsStackDepth.peekLast() || 0; i < minFrames; ++i) {
const newFrame = callFrames[i];
const oldFrame = jsFramesStack[i].args['data'];
if (!equalFrames(newFrame, oldFrame)) {
break;
}
jsFramesStack[i].setEndTime(Math.max(jsFramesStack[i].endTime, endTime));
}
truncateJSStack(i, e.startTime);
for (; i < callFrames.length; ++i) {
const frame = callFrames[i];
const jsFrameEvent = new SDK.TracingModel.Event(
SDK.TracingModel.DevToolsTimelineEventCategory, recordTypes.JSFrame, SDK.TracingModel.Phase.Complete,
e.startTime, e.thread);
jsFrameEvent.ordinal = e.ordinal;
jsFrameEvent.addArgs({data: frame});
jsFrameEvent.setEndTime(endTime);
jsFramesStack.push(jsFrameEvent);
jsFrameEvents.push(jsFrameEvent);
}
}
const firstTopLevelEvent = events.find(SDK.TracingModel.isTopLevelEvent);
const startTime = firstTopLevelEvent ? firstTopLevelEvent.startTime : 0;
TimelineModel.TimelineModel.forEachEvent(events, onStartEvent, onEndEvent, onInstantEvent, startTime);
return jsFrameEvents;
}
/**
* @param {!Protocol.Runtime.CallFrame} frame
* @return {boolean}
*/
static isNativeRuntimeFrame(frame) {
return frame.url === 'native V8Runtime';
}
/**
* @param {string} nativeName
* @return {?TimelineJSProfileProcessor.NativeGroups}
*/
static nativeGroup(nativeName) {
if (nativeName.startsWith('Parse')) {
return TimelineJSProfileProcessor.NativeGroups.Parse;
}
if (nativeName.startsWith('Compile') || nativeName.startsWith('Recompile')) {
return TimelineJSProfileProcessor.NativeGroups.Compile;
}
return null;
}
/**
* @param {*} profile
* @param {number} tid
* @param {boolean} injectPageEvent
* @param {?string=} name
* @return {!Array<!SDK.TracingManager.EventPayload>}
*/
static buildTraceProfileFromCpuProfile(profile, tid, injectPageEvent, name) {
const events = [];
if (injectPageEvent) {
appendEvent('TracingStartedInPage', {data: {'sessionId': '1'}}, 0, 0, 'M');
}
if (!name) {
name = ls`Thread ${tid}`;
}
appendEvent(SDK.TracingModel.MetadataEvent.ThreadName, {name}, 0, 0, 'M', '__metadata');
if (!profile) {
return events;
}
const idToNode = new Map();
const nodes = profile['nodes'];
for (let i = 0; i < nodes.length; ++i) {
idToNode.set(nodes[i].id, nodes[i]);
}
let programEvent = null;
let functionEvent = null;
let nextTime = profile.startTime;
let currentTime;
const samples = profile['samples'];
const timeDeltas = profile['timeDeltas'];
for (let i = 0; i < samples.length; ++i) {
currentTime = nextTime;
nextTime += timeDeltas[i];
const node = idToNode.get(samples[i]);
const name = node.callFrame.functionName;
if (name === '(idle)') {
closeEvents();
continue;
}
if (!programEvent) {
programEvent = appendEvent('MessageLoop::RunTask', {}, currentTime, 0, 'X', 'toplevel');
}
if (name === '(program)') {
if (functionEvent) {
functionEvent.dur = currentTime - functionEvent.ts;
functionEvent = null;
}
} else {
// A JS function.
if (!functionEvent) {
functionEvent = appendEvent('FunctionCall', {data: {'sessionId': '1'}}, currentTime);
}
}
}
closeEvents();
appendEvent('CpuProfile', {data: {'cpuProfile': profile}}, profile.endTime, 0, 'I');
return events;
function closeEvents() {
if (programEvent) {
programEvent.dur = currentTime - programEvent.ts;
}
if (functionEvent) {
functionEvent.dur = currentTime - functionEvent.ts;
}
programEvent = null;
functionEvent = null;
}
/**
* @param {string} name
* @param {*} args
* @param {number} ts
* @param {number=} dur
* @param {string=} ph
* @param {string=} cat
* @return {!SDK.TracingManager.EventPayload}
*/
function appendEvent(name, args, ts, dur, ph, cat) {
const event = /** @type {!SDK.TracingManager.EventPayload} */ (
{cat: cat || 'disabled-by-default-devtools.timeline', name, ph: ph || 'X', pid: 1, tid, ts, args});
if (dur) {
event.dur = dur;
}
events.push(event);
return event;
}
}
}
/** @enum {string} */
TimelineJSProfileProcessor.NativeGroups = {
'Compile': 'Compile',
'Parse': 'Parse'
};
/* Legacy exported object */
self.TimelineModel = self.TimelineModel || {};
/* Legacy exported object */
TimelineModel = TimelineModel || {};
/** @constructor */
TimelineModel.TimelineJSProfileProcessor = TimelineJSProfileProcessor;