@quick-game/cli
Version:
Command line interface for rapid qg development
206 lines • 8.79 kB
JavaScript
// Copyright 2017 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 Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as SourceMapScopes from '../../models/source_map_scopes/source_map_scopes.js';
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
import { TimelineUIUtils } from './TimelineUIUtils.js';
const resolveNamesTimeout = 500;
export class PerformanceModel extends Common.ObjectWrapper.ObjectWrapper {
mainTargetInternal;
tracingModelInternal;
filtersInternal;
timelineModelInternal;
frameModelInternal;
windowInternal;
willResolveNames = false;
recordStartTimeInternal;
constructor() {
super();
this.mainTargetInternal = null;
this.tracingModelInternal = null;
this.filtersInternal = [];
this.timelineModelInternal = new TimelineModel.TimelineModel.TimelineModelImpl();
this.frameModelInternal = new TimelineModel.TimelineFrameModel.TimelineFrameModel(event => TimelineUIUtils.eventStyle(event).category.name);
this.windowInternal = { left: 0, right: Infinity };
this.recordStartTimeInternal = undefined;
}
setMainTarget(target) {
this.mainTargetInternal = target;
}
mainTarget() {
return this.mainTargetInternal;
}
setRecordStartTime(time) {
this.recordStartTimeInternal = time;
}
recordStartTime() {
return this.recordStartTimeInternal;
}
setFilters(filters) {
this.filtersInternal = filters;
}
filters() {
return this.filtersInternal;
}
isVisible(event) {
return this.filtersInternal.every(f => f.accept(event));
}
async setTracingModel(model, isFreshRecording = false) {
this.tracingModelInternal = model;
this.timelineModelInternal.setEvents(model, isFreshRecording);
await this.addSourceMapListeners();
const mainTracks = this.timelineModelInternal.tracks().filter(track => track.type === TimelineModel.TimelineModel.TrackType.MainThread && track.forMainFrame &&
track.events.length);
const threadData = mainTracks.map(track => {
const event = track.events[0];
return { thread: event.thread, time: event.startTime };
});
this.frameModelInternal.addTraceEvents(this.mainTargetInternal, this.timelineModelInternal.inspectedTargetEvents(), threadData);
this.autoWindowTimes();
}
async addSourceMapListeners() {
const debuggerModelsToListen = new Set();
for (const profile of this.timelineModel().cpuProfiles()) {
for (const node of profile.cpuProfileData.nodes() || []) {
if (!node) {
continue;
}
const debuggerModelToListen = this.#maybeGetDebuggerModelForNode(node, profile.target);
if (!debuggerModelToListen) {
continue;
}
debuggerModelsToListen.add(debuggerModelToListen);
}
}
for (const debuggerModel of debuggerModelsToListen) {
debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, this.#onAttachedSourceMap, this);
}
await this.#resolveNamesFromCPUProfile();
}
// If a node corresponds to a script that has not been parsed or a script
// that has a source map, we should listen to SourceMapAttached events to
// attempt a function name resolving.
#maybeGetDebuggerModelForNode(node, target) {
const debuggerModel = target?.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
return null;
}
const script = debuggerModel.scriptForId(String(node.callFrame.scriptId));
const shouldListenToSourceMap = !script || script.sourceMapURL;
if (shouldListenToSourceMap) {
return debuggerModel;
}
return null;
}
async #resolveNamesFromCPUProfile() {
for (const profile of this.timelineModel().cpuProfiles()) {
const target = profile.target;
for (const node of profile.cpuProfileData.nodes() || []) {
const resolvedFunctionName = await SourceMapScopes.NamesResolver.resolveProfileFrameFunctionName(node.callFrame, target);
node.setFunctionName(resolvedFunctionName);
}
}
}
async #onAttachedSourceMap() {
if (!this.willResolveNames) {
this.willResolveNames = true;
// Resolving names triggers a repaint of the flame chart. Instead of attempting to resolve
// names every time a source map is attached, wait for some time once the first source map is
// attached. This way we allow for other source maps to be parsed before attempting a name
// resolving using the available source maps. Otherwise the UI is blocked when the number
// of source maps is particularly large.
setTimeout(this.resolveNamesAndUpdate.bind(this), resolveNamesTimeout);
}
}
async resolveNamesAndUpdate() {
this.willResolveNames = false;
await this.#resolveNamesFromCPUProfile();
this.dispatchEventToListeners(Events.NamesResolved);
}
tracingModel() {
if (!this.tracingModelInternal) {
throw 'call setTracingModel before accessing PerformanceModel';
}
return this.tracingModelInternal;
}
timelineModel() {
return this.timelineModelInternal;
}
frames() {
return this.frameModelInternal.getFrames();
}
frameModel() {
return this.frameModelInternal;
}
setWindow(window, animate) {
this.windowInternal = window;
this.dispatchEventToListeners(Events.WindowChanged, { window, animate });
}
window() {
return this.windowInternal;
}
minimumRecordTime() {
return this.timelineModelInternal.minimumRecordTime();
}
maximumRecordTime() {
return this.timelineModelInternal.maximumRecordTime();
}
autoWindowTimes() {
const timelineModel = this.timelineModelInternal;
let tasks = [];
for (const track of timelineModel.tracks()) {
// Deliberately pick up last main frame's track.
if (track.type === TimelineModel.TimelineModel.TrackType.MainThread && track.forMainFrame) {
tasks = track.tasks;
}
}
if (!tasks.length) {
this.setWindow({ left: timelineModel.minimumRecordTime(), right: timelineModel.maximumRecordTime() });
return;
}
function findLowUtilizationRegion(startIndex, stopIndex) {
const threshold = 0.1;
let cutIndex = startIndex;
let cutTime = (tasks[cutIndex].startTime + tasks[cutIndex].endTime) / 2;
let usedTime = 0;
const step = Math.sign(stopIndex - startIndex);
for (let i = startIndex; i !== stopIndex; i += step) {
const task = tasks[i];
const taskTime = (task.startTime + task.endTime) / 2;
const interval = Math.abs(cutTime - taskTime);
if (usedTime < threshold * interval) {
cutIndex = i;
cutTime = taskTime;
usedTime = 0;
}
usedTime += task.duration;
}
return cutIndex;
}
const rightIndex = findLowUtilizationRegion(tasks.length - 1, 0);
const leftIndex = findLowUtilizationRegion(0, rightIndex);
let leftTime = tasks[leftIndex].startTime;
let rightTime = tasks[rightIndex].endTime;
const span = rightTime - leftTime;
const totalSpan = timelineModel.maximumRecordTime() - timelineModel.minimumRecordTime();
if (span < totalSpan * 0.1) {
leftTime = timelineModel.minimumRecordTime();
rightTime = timelineModel.maximumRecordTime();
}
else {
leftTime = Math.max(leftTime - 0.05 * span, timelineModel.minimumRecordTime());
rightTime = Math.min(rightTime + 0.05 * span, timelineModel.maximumRecordTime());
}
this.setWindow({ left: leftTime, right: rightTime });
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export var Events;
(function (Events) {
Events["WindowChanged"] = "WindowChanged";
Events["NamesResolved"] = "NamesResolved";
})(Events || (Events = {}));
//# sourceMappingURL=PerformanceModel.js.map