@quick-game/cli
Version:
Command line interface for rapid qg development
175 lines • 7.95 kB
JavaScript
// Copyright 2022 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.
import * as Platform from '../../../core/platform/platform.js';
import * as Types from '../types/types.js';
import * as CPUProfile from '../../cpu_profile/cpu_profile.js';
import * as Helpers from '../helpers/helpers.js';
const events = new Map();
const profilesInProcess = new Map();
// The profile head, containing the event's metadata like its start
// time, comes in a "Profile" event. The sample data comes in
// "ProfileChunk" events. We match these ProfileChunks with their head
// using process and profile ids. However, in order to integrate sample
// data with trace data, we need the thread id that owns each profile.
// This thread id is extracted from the head event.
// For this reason, we have a preprocessed data structure, where events
// are matched by profile id, which we then finish processing to export
// events matched by thread id.
const preprocessedData = new Map();
let handlerState = 1 /* HandlerState.UNINITIALIZED */;
export function buildProfileCalls() {
for (const [processId, profiles] of preprocessedData) {
for (const [profileId, preProcessedData] of profiles) {
const threadId = preProcessedData.threadId;
if (!preProcessedData.rawProfile.nodes.length || !threadId) {
continue;
}
const trackingStack = [];
const profileModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(preProcessedData.rawProfile);
const finalizedData = { rawProfile: preProcessedData.rawProfile, parsedProfile: profileModel, profileCalls: [] };
profileModel.forEachFrame(openFrameCallback, closeFrameCallback);
Helpers.Trace.sortTraceEventsInPlace(finalizedData.profileCalls);
const dataByThread = Platform.MapUtilities.getWithDefault(profilesInProcess, processId, () => new Map());
dataByThread.set(threadId, finalizedData);
function openFrameCallback(_depth, node, timeStampMs) {
const ts = Helpers.Timing.millisecondsToMicroseconds(Types.Timing.MilliSeconds(timeStampMs));
trackingStack.push({ callFrame: node.callFrame, ts, pid: processId, children: [], tid: threadId });
}
function closeFrameCallback(depth, node, _timeStamp, durMs, selfTimeMs) {
const partialProfileCall = trackingStack.pop();
if (!partialProfileCall) {
return;
}
const { callFrame, ts, pid, children, tid } = partialProfileCall;
if (callFrame === undefined || ts === undefined || pid === undefined || profileId === undefined ||
children === undefined || tid === undefined) {
return;
}
const dur = Helpers.Timing.millisecondsToMicroseconds(Types.Timing.MilliSeconds(durMs));
const selfTime = Helpers.Timing.millisecondsToMicroseconds(Types.Timing.MilliSeconds(selfTimeMs));
const completeProfileCall = {
callFrame,
ts,
pid,
dur,
selfTime,
children,
ph: "X" /* Types.TraceEvents.Phase.COMPLETE */,
cat: '',
name: 'ProfileCall',
tid,
nodeId: node.id,
};
const parent = trackingStack.at(-1);
const calls = finalizedData.profileCalls;
calls.push(completeProfileCall);
if (!parent) {
return;
}
parent.children = parent.children || [];
parent.children.push(completeProfileCall);
if (parent.selfTime) {
parent.selfTime = Types.Timing.MicroSeconds(parent.selfTime - dur);
}
}
}
}
}
export function reset() {
events.clear();
preprocessedData.clear();
profilesInProcess.clear();
handlerState = 1 /* HandlerState.UNINITIALIZED */;
}
export function initialize() {
if (handlerState !== 1 /* HandlerState.UNINITIALIZED */) {
throw new Error('Samples Handler was not reset');
}
handlerState = 2 /* HandlerState.INITIALIZED */;
}
export function handleEvent(event) {
if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
throw new Error('Samples Handler is not initialized');
}
if (Types.TraceEvents.isTraceEventProfile(event)) {
// Do not use event.args.data.startTime as it is in CLOCK_MONOTONIC domain,
// but use profileEvent.ts which has been translated to Perfetto's clock
// domain. Also convert from ms to us.
// Note: events are collected on a different thread than what's sampled.
// The correct process and thread ids are specified by the profile.
const profileData = getOrCreatePreProcessedData(event.pid, event.id);
profileData.rawProfile.startTime = event.ts;
profileData.threadId = event.tid;
return;
}
if (Types.TraceEvents.isTraceEventProfileChunk(event)) {
const profileData = getOrCreatePreProcessedData(event.pid, event.id);
const cdpProfile = profileData.rawProfile;
const nodesAndSamples = event.args?.data?.cpuProfile || { samples: [] };
const samples = nodesAndSamples?.samples || [];
const nodes = [];
for (const n of nodesAndSamples?.nodes || []) {
const lineNumber = n.callFrame.lineNumber || -1;
const columnNumber = n.callFrame.columnNumber || -1;
const scriptId = String(n.callFrame.scriptId);
const url = n.callFrame.url || '';
const node = {
...n,
callFrame: {
...n.callFrame,
url,
lineNumber,
columnNumber,
scriptId,
},
};
nodes.push(node);
}
const timeDeltas = event.args.data?.timeDeltas || [];
const lines = event.args.data?.lines || Array(samples.length).fill(0);
cdpProfile.nodes.push(...nodes);
cdpProfile.samples?.push(...samples);
cdpProfile.timeDeltas?.push(...timeDeltas);
cdpProfile.lines?.push(...lines);
if (cdpProfile.samples && cdpProfile.timeDeltas && cdpProfile.samples.length !== cdpProfile.timeDeltas.length) {
console.error('Failed to parse CPU profile.');
return;
}
if (!cdpProfile.endTime && cdpProfile.timeDeltas) {
const timeDeltas = cdpProfile.timeDeltas;
cdpProfile.endTime = timeDeltas.reduce((x, y) => x + y, cdpProfile.startTime);
}
return;
}
}
export async function finalize() {
if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
throw new Error('Samples Handler is not initialized');
}
buildProfileCalls();
handlerState = 3 /* HandlerState.FINALIZED */;
}
export function data() {
if (handlerState !== 3 /* HandlerState.FINALIZED */) {
throw new Error('Samples Handler is not finalized');
}
return {
profilesInProcess: new Map(profilesInProcess),
};
}
function getOrCreatePreProcessedData(processId, profileId) {
const profileById = Platform.MapUtilities.getWithDefault(preprocessedData, processId, () => new Map());
return Platform.MapUtilities.getWithDefault(profileById, profileId, () => ({
rawProfile: {
startTime: 0,
endTime: 0,
nodes: [],
samples: [],
timeDeltas: [],
lines: [],
},
profileId,
}));
}
//# sourceMappingURL=SamplesHandler.js.map