chrome-devtools-frontend
Version:
Chrome DevTools UI
260 lines (229 loc) • 10.6 kB
text/typescript
// Copyright 2020 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 '../common/common.js';
import type * as Protocol from '../../generated/protocol.js';
import {type Resource} from './Resource.js';
import {Events as ResourceTreeModelEvents, ResourceTreeModel, type ResourceTreeFrame} from './ResourceTreeModel.js';
import {type Target} from './Target.js';
import {TargetManager, type SDKModelObserver} from './TargetManager.js';
let frameManagerInstance: FrameManager|null = null;
/**
* The FrameManager is a central storage for all #frames. It collects #frames from all
* ResourceTreeModel-instances (one per target), so that #frames can be found by id
* without needing to know their target.
*/
export class FrameManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
SDKModelObserver<ResourceTreeModel> {
readonly #eventListeners = new WeakMap<ResourceTreeModel, Common.EventTarget.EventDescriptor[]>();
// Maps frameIds to #frames and a count of how many ResourceTreeModels contain this frame.
// (OOPIFs are usually first attached to a new target and then detached from their old target,
// therefore being contained in 2 models for a short period of time.)
#frames = new Map<string, {
frame: ResourceTreeFrame,
count: number,
}>();
readonly #framesForTarget = new Map<Protocol.Target.TargetID|'main', Set<Protocol.Page.FrameId>>();
#outermostFrame: ResourceTreeFrame|null = null;
#transferringFramesDataCache = new Map<string, {
creationStackTrace?: Protocol.Runtime.StackTrace,
creationStackTraceTarget?: Target,
}>();
#awaitedFrames: Map<string, {notInTarget?: Target, resolve: (frame: ResourceTreeFrame) => void}[]> = new Map();
constructor() {
super();
TargetManager.instance().observeModels(ResourceTreeModel, this);
}
static instance({forceNew}: {
forceNew: boolean,
} = {forceNew: false}): FrameManager {
if (!frameManagerInstance || forceNew) {
frameManagerInstance = new FrameManager();
}
return frameManagerInstance;
}
modelAdded(resourceTreeModel: ResourceTreeModel): void {
const addListener = resourceTreeModel.addEventListener(ResourceTreeModelEvents.FrameAdded, this.frameAdded, this);
const detachListener =
resourceTreeModel.addEventListener(ResourceTreeModelEvents.FrameDetached, this.frameDetached, this);
const navigatedListener =
resourceTreeModel.addEventListener(ResourceTreeModelEvents.FrameNavigated, this.frameNavigated, this);
const resourceAddedListener =
resourceTreeModel.addEventListener(ResourceTreeModelEvents.ResourceAdded, this.resourceAdded, this);
this.#eventListeners.set(
resourceTreeModel, [addListener, detachListener, navigatedListener, resourceAddedListener]);
this.#framesForTarget.set(resourceTreeModel.target().id(), new Set());
}
modelRemoved(resourceTreeModel: ResourceTreeModel): void {
const listeners = this.#eventListeners.get(resourceTreeModel);
if (listeners) {
Common.EventTarget.removeEventListeners(listeners);
}
// Iterate over this model's #frames and decrease their count or remove them.
// (The ResourceTreeModel does not send FrameDetached events when a model
// is removed.)
const frameSet = this.#framesForTarget.get(resourceTreeModel.target().id());
if (frameSet) {
for (const frameId of frameSet) {
this.decreaseOrRemoveFrame(frameId);
}
}
this.#framesForTarget.delete(resourceTreeModel.target().id());
}
private frameAdded(event: Common.EventTarget.EventTargetEvent<ResourceTreeFrame>): void {
const frame = event.data;
const frameData = this.#frames.get(frame.id);
// If the frame is already in the map, increase its count, otherwise add it to the map.
if (frameData) {
// In order to not lose the following attributes of a frame during
// an OOPIF transfer we need to copy them to the new frame
frame.setCreationStackTrace(frameData.frame.getCreationStackTraceData());
this.#frames.set(frame.id, {frame, count: frameData.count + 1});
} else {
// If the transferring frame's detached event is received before its frame added
// event in the new target, the frame's cached attributes are reassigned.
const cachedFrameAttributes = this.#transferringFramesDataCache.get(frame.id);
if (cachedFrameAttributes?.creationStackTrace && cachedFrameAttributes?.creationStackTraceTarget) {
frame.setCreationStackTrace({
creationStackTrace: cachedFrameAttributes.creationStackTrace,
creationStackTraceTarget: cachedFrameAttributes.creationStackTraceTarget,
});
}
this.#frames.set(frame.id, {frame, count: 1});
this.#transferringFramesDataCache.delete(frame.id);
}
this.resetOutermostFrame();
// Add the frameId to the the targetId's set of frameIds.
const frameSet = this.#framesForTarget.get(frame.resourceTreeModel().target().id());
if (frameSet) {
frameSet.add(frame.id);
}
this.dispatchEventToListeners(Events.FrameAddedToTarget, {frame});
this.resolveAwaitedFrame(frame);
}
private frameDetached(event: Common.EventTarget.EventTargetEvent<{frame: ResourceTreeFrame, isSwap: boolean}>): void {
const {frame, isSwap} = event.data;
// Decrease the frame's count or remove it entirely from the map.
this.decreaseOrRemoveFrame(frame.id);
// If the transferring frame's detached event is received before its frame
// added event in the new target, we persist some attributes of the frame here
// so that later on the frame added event in the new target they can be reassigned.
if (isSwap && !this.#frames.get(frame.id)) {
const traceData = frame.getCreationStackTraceData();
const cachedFrameAttributes = {
...(traceData.creationStackTrace && {creationStackTrace: traceData.creationStackTrace}),
...(traceData.creationStackTrace && {creationStackTraceTarget: traceData.creationStackTraceTarget}),
};
this.#transferringFramesDataCache.set(frame.id, cachedFrameAttributes);
}
// Remove the frameId from the target's set of frameIds.
const frameSet = this.#framesForTarget.get(frame.resourceTreeModel().target().id());
if (frameSet) {
frameSet.delete(frame.id);
}
}
private frameNavigated(event: Common.EventTarget.EventTargetEvent<ResourceTreeFrame>): void {
const frame = event.data;
this.dispatchEventToListeners(Events.FrameNavigated, {frame});
if (frame.isOutermostFrame()) {
this.dispatchEventToListeners(Events.OutermostFrameNavigated, {frame});
}
}
private resourceAdded(event: Common.EventTarget.EventTargetEvent<Resource>): void {
this.dispatchEventToListeners(Events.ResourceAdded, {resource: event.data});
}
private decreaseOrRemoveFrame(frameId: Protocol.Page.FrameId): void {
const frameData = this.#frames.get(frameId);
if (frameData) {
if (frameData.count === 1) {
this.#frames.delete(frameId);
this.resetOutermostFrame();
this.dispatchEventToListeners(Events.FrameRemoved, {frameId});
} else {
frameData.count--;
}
}
}
/**
* Looks for the outermost frame in `#frames` and sets `#outermostFrame` accordingly.
*
* Important: This method needs to be called everytime `#frames` is updated.
*/
private resetOutermostFrame(): void {
const outermostFrames = this.getAllFrames().filter(frame => frame.isOutermostFrame());
this.#outermostFrame = outermostFrames.length > 0 ? outermostFrames[0] : null;
}
/**
* Returns the ResourceTreeFrame with a given frameId.
* When a frame is being detached a new ResourceTreeFrame but with the same
* frameId is created. Consequently getFrame() will return a different
* ResourceTreeFrame after detachment. Callers of getFrame() should therefore
* immediately use the function return value and not store it for later use.
*/
getFrame(frameId: Protocol.Page.FrameId): ResourceTreeFrame|null {
const frameData = this.#frames.get(frameId);
if (frameData) {
return frameData.frame;
}
return null;
}
getAllFrames(): ResourceTreeFrame[] {
return Array.from(this.#frames.values(), frameData => frameData.frame);
}
getOutermostFrame(): ResourceTreeFrame|null {
return this.#outermostFrame;
}
async getOrWaitForFrame(frameId: Protocol.Page.FrameId, notInTarget?: Target): Promise<ResourceTreeFrame> {
const frame = this.getFrame(frameId);
if (frame && (!notInTarget || notInTarget !== frame.resourceTreeModel().target())) {
return frame;
}
return new Promise<ResourceTreeFrame>(resolve => {
const waiting = this.#awaitedFrames.get(frameId);
if (waiting) {
waiting.push({notInTarget, resolve});
} else {
this.#awaitedFrames.set(frameId, [{notInTarget, resolve}]);
}
});
}
private resolveAwaitedFrame(frame: ResourceTreeFrame): void {
const waiting = this.#awaitedFrames.get(frame.id);
if (!waiting) {
return;
}
const newWaiting = waiting.filter(({notInTarget, resolve}) => {
if (!notInTarget || notInTarget !== frame.resourceTreeModel().target()) {
resolve(frame);
return false;
}
return true;
});
if (newWaiting.length > 0) {
this.#awaitedFrames.set(frame.id, newWaiting);
} else {
this.#awaitedFrames.delete(frame.id);
}
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
// The FrameAddedToTarget event is sent whenever a frame is added to a target.
// This means that for OOPIFs it is sent twice: once when it's added to a
// parent target and a second time when it's added to its own target.
FrameAddedToTarget = 'FrameAddedToTarget',
FrameNavigated = 'FrameNavigated',
// The FrameRemoved event is only sent when a frame has been detached from
// all targets.
FrameRemoved = 'FrameRemoved',
ResourceAdded = 'ResourceAdded',
OutermostFrameNavigated = 'OutermostFrameNavigated',
}
export type EventTypes = {
[Events.FrameAddedToTarget]: {frame: ResourceTreeFrame},
[Events.FrameNavigated]: {frame: ResourceTreeFrame},
[Events.FrameRemoved]: {frameId: Protocol.Page.FrameId},
[Events.ResourceAdded]: {resource: Resource},
[Events.OutermostFrameNavigated]: {frame: ResourceTreeFrame},
};