chrome-devtools-frontend
Version:
Chrome DevTools UI
792 lines (662 loc) • 27.9 kB
text/typescript
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
import * as Protocol from '../../generated/protocol.js';
import type * as Common from '../common/common.js';
import {MapWithDefault} from '../common/MapWithDefault.js';
import {assertNotNullOrUndefined} from '../platform/platform.js';
import {
Events as ResourceTreeModelEvents,
PrimaryPageChangeType,
type ResourceTreeFrame,
ResourceTreeModel,
} from './ResourceTreeModel.js';
import {SDKModel} from './SDKModel.js';
import {Capability, type Target} from './Target.js';
import {TargetManager} from './TargetManager.js';
export interface WithId<I, V> {
id: I;
value: V;
}
// Holds preloading related information.
//
// - SpeculationRule rule sets
// - Preloading attempts
// - Relationship between rule sets and preloading attempts
export class PreloadingModel extends SDKModel<EventTypes> {
private agent: ProtocolProxyApi.PreloadApi;
private loaderIds: Protocol.Network.LoaderId[] = [];
private targetJustAttached = true;
private lastPrimaryPageModel: PreloadingModel|null = null;
private documents: Map<Protocol.Network.LoaderId, DocumentPreloadingData> =
new Map<Protocol.Network.LoaderId, DocumentPreloadingData>();
constructor(target: Target) {
super(target);
target.registerPreloadDispatcher(new PreloadDispatcher(this));
this.agent = target.preloadAgent();
void this.agent.invoke_enable();
const targetInfo = target.targetInfo();
if (targetInfo !== undefined && targetInfo.subtype === 'prerender') {
this.lastPrimaryPageModel = TargetManager.instance().primaryPageTarget()?.model(PreloadingModel) || null;
}
TargetManager.instance().addModelListener(
ResourceTreeModel, ResourceTreeModelEvents.PrimaryPageChanged, this.onPrimaryPageChanged, this);
}
override dispose(): void {
super.dispose();
TargetManager.instance().removeModelListener(
ResourceTreeModel, ResourceTreeModelEvents.PrimaryPageChanged, this.onPrimaryPageChanged, this);
void this.agent.invoke_disable();
}
private ensureDocumentPreloadingData(loaderId: Protocol.Network.LoaderId): void {
if (this.documents.get(loaderId) === undefined) {
this.documents.set(loaderId, new DocumentPreloadingData());
}
}
private currentLoaderId(): Protocol.Network.LoaderId|null {
// Target is just attached and didn't received CDP events that we can infer loaderId.
if (this.targetJustAttached) {
return null;
}
if (this.loaderIds.length === 0) {
throw new Error('unreachable');
}
return this.loaderIds[this.loaderIds.length - 1];
}
private currentDocument(): DocumentPreloadingData|null {
const loaderId = this.currentLoaderId();
return loaderId === null ? null : this.documents.get(loaderId) || null;
}
// Returns a rule set of the current page.
//
// Returns reference. Don't save returned values.
// Returned value may or may not be updated as the time grows.
getRuleSetById(id: Protocol.Preload.RuleSetId): Protocol.Preload.RuleSet|null {
return this.currentDocument()?.ruleSets.getById(id) || null;
}
// Returns rule sets of the current page.
//
// Returns array of pairs of id and reference. Don't save returned references.
// Returned values may or may not be updated as the time grows.
getAllRuleSets(): Array<WithId<Protocol.Preload.RuleSetId, Protocol.Preload.RuleSet>> {
return this.currentDocument()?.ruleSets.getAll() || [];
}
getPreloadCountsByRuleSetId(): Map<Protocol.Preload.RuleSetId|null, Map<PreloadingStatus, number>> {
const countsByRuleSetId = new Map<Protocol.Preload.RuleSetId|null, Map<PreloadingStatus, number>>();
for (const {value} of this.getRepresentativePreloadingAttempts(null)) {
for (const ruleSetId of [null, ...value.ruleSetIds]) {
if (countsByRuleSetId.get(ruleSetId) === undefined) {
countsByRuleSetId.set(ruleSetId, new Map<PreloadingStatus, number>());
}
const countsByStatus = countsByRuleSetId.get(ruleSetId);
assertNotNullOrUndefined(countsByStatus);
const i = countsByStatus.get(value.status) || 0;
countsByStatus.set(value.status, i + 1);
}
}
return countsByRuleSetId;
}
// Returns a preloading attempt of the current page.
//
// Returns reference. Don't save returned values.
// Returned value may or may not be updated as the time grows.
getPreloadingAttemptById(id: PreloadingAttemptId): PreloadingAttempt|null {
const document = this.currentDocument();
if (document === null) {
return null;
}
return document.preloadingAttempts.getById(id, document.sources) || null;
}
// Returs preloading attempts of the current page that triggered by the rule set with `ruleSetId`.
// `ruleSetId === null` means "do not filter".
//
// Returns array of pairs of id and reference. Don't save returned references.
// Returned values may or may not be updated as the time grows.
getRepresentativePreloadingAttempts(ruleSetId: Protocol.Preload.RuleSetId|null):
Array<WithId<PreloadingAttemptId, PreloadingAttempt>> {
const document = this.currentDocument();
if (document === null) {
return [];
}
return document.preloadingAttempts.getAllRepresentative(ruleSetId, document.sources);
}
// Returs preloading attempts of the previousPgae.
//
// Returns array of pairs of id and reference. Don't save returned references.
// Returned values may or may not be updated as the time grows.
getRepresentativePreloadingAttemptsOfPreviousPage(): Array<WithId<PreloadingAttemptId, PreloadingAttempt>> {
if (this.loaderIds.length <= 1) {
return [];
}
const document = this.documents.get(this.loaderIds[this.loaderIds.length - 2]);
if (document === undefined) {
return [];
}
return document.preloadingAttempts.getAllRepresentative(null, document.sources);
}
// Precondition: `pipelineId` should exists.
// Postcondition: The return value is not empty.
private getPipelineById(pipelineId: Protocol.Preload.PreloadPipelineId):
Map<Protocol.Preload.SpeculationAction, PreloadingAttempt>|null {
const document = this.currentDocument();
if (document === null) {
return null;
}
return document.preloadingAttempts.getPipeline(pipelineId, document.sources);
}
// Returns attemtps that are sit in the same preload pipeline.
getPipeline(attempt: PreloadingAttempt): PreloadPipeline {
let pipelineNullable = null;
if (attempt.pipelineId !== null) {
pipelineNullable = this.getPipelineById(attempt.pipelineId);
}
if (pipelineNullable === null) {
const pipeline = new Map();
pipeline.set(attempt.action, attempt);
return new PreloadPipeline(pipeline);
}
return new PreloadPipeline(pipelineNullable);
}
private onPrimaryPageChanged(
event: Common.EventTarget.EventTargetEvent<{frame: ResourceTreeFrame, type: PrimaryPageChangeType}>): void {
const {frame, type} = event.data;
// Model of prerendered page's target will hands over. Do nothing for the initiator page.
if (this.lastPrimaryPageModel === null && type === PrimaryPageChangeType.ACTIVATION) {
return;
}
if (this.lastPrimaryPageModel !== null && type !== PrimaryPageChangeType.ACTIVATION) {
return;
}
if (this.lastPrimaryPageModel !== null && type === PrimaryPageChangeType.ACTIVATION) {
// Hand over from the model of the last primary page.
this.loaderIds = this.lastPrimaryPageModel.loaderIds;
for (const [loaderId, prev] of this.lastPrimaryPageModel.documents.entries()) {
this.ensureDocumentPreloadingData(loaderId);
this.documents.get(loaderId)?.mergePrevious(prev);
}
}
this.lastPrimaryPageModel = null;
// Note that at this timing ResourceTreeFrame.loaderId is ensured to
// be non empty and Protocol.Network.LoaderId because it is filled
// by ResourceTreeFrame.navigate.
const currentLoaderId = frame.loaderId;
// Holds histories for two pages at most.
this.loaderIds.push(currentLoaderId);
this.loaderIds = this.loaderIds.slice(-2);
this.ensureDocumentPreloadingData(currentLoaderId);
for (const loaderId of this.documents.keys()) {
if (!this.loaderIds.includes(loaderId)) {
this.documents.delete(loaderId);
}
}
this.dispatchEventToListeners(Events.MODEL_UPDATED);
}
onRuleSetUpdated(event: Protocol.Preload.RuleSetUpdatedEvent): void {
const ruleSet = event.ruleSet;
const loaderId = ruleSet.loaderId;
// Infer current loaderId if DevTools is opned at the current page.
if (this.currentLoaderId() === null) {
this.loaderIds = [loaderId];
this.targetJustAttached = false;
}
this.ensureDocumentPreloadingData(loaderId);
this.documents.get(loaderId)?.ruleSets.upsert(ruleSet);
this.dispatchEventToListeners(Events.MODEL_UPDATED);
}
onRuleSetRemoved(event: Protocol.Preload.RuleSetRemovedEvent): void {
const id = event.id;
for (const document of this.documents.values()) {
document.ruleSets.delete(id);
}
this.dispatchEventToListeners(Events.MODEL_UPDATED);
}
onPreloadingAttemptSourcesUpdated(event: Protocol.Preload.PreloadingAttemptSourcesUpdatedEvent): void {
const loaderId = event.loaderId;
this.ensureDocumentPreloadingData(loaderId);
const document = this.documents.get(loaderId);
if (document === undefined) {
return;
}
document.sources.update(event.preloadingAttemptSources);
document.preloadingAttempts.maybeRegisterNotTriggered(document.sources);
document.preloadingAttempts.cleanUpRemovedAttempts(document.sources);
this.dispatchEventToListeners(Events.MODEL_UPDATED);
}
onPrefetchStatusUpdated(event: Protocol.Preload.PrefetchStatusUpdatedEvent): void {
// We ignore this event to avoid reinserting an attempt after it was removed by
// onPreloadingAttemptSourcesUpdated.
if (event.prefetchStatus === Protocol.Preload.PrefetchStatus.PrefetchEvictedAfterCandidateRemoved) {
return;
}
const loaderId = event.key.loaderId;
this.ensureDocumentPreloadingData(loaderId);
const attempt: PrefetchAttemptInternal = {
action: Protocol.Preload.SpeculationAction.Prefetch,
key: event.key,
pipelineId: event.pipelineId,
status: convertPreloadingStatus(event.status),
prefetchStatus: event.prefetchStatus || null,
requestId: event.requestId,
};
this.documents.get(loaderId)?.preloadingAttempts.upsert(attempt);
this.dispatchEventToListeners(Events.MODEL_UPDATED);
}
onPrerenderStatusUpdated(event: Protocol.Preload.PrerenderStatusUpdatedEvent): void {
const loaderId = event.key.loaderId;
this.ensureDocumentPreloadingData(loaderId);
const attempt: PrerenderAttemptInternal = {
action: Protocol.Preload.SpeculationAction.Prerender,
key: event.key,
pipelineId: event.pipelineId,
status: convertPreloadingStatus(event.status),
prerenderStatus: event.prerenderStatus || null,
disallowedMojoInterface: event.disallowedMojoInterface || null,
mismatchedHeaders: event.mismatchedHeaders || null,
};
this.documents.get(loaderId)?.preloadingAttempts.upsert(attempt);
this.dispatchEventToListeners(Events.MODEL_UPDATED);
}
onPreloadEnabledStateUpdated(event: Protocol.Preload.PreloadEnabledStateUpdatedEvent): void {
this.dispatchEventToListeners(Events.WARNINGS_UPDATED, event);
}
}
SDKModel.register(PreloadingModel, {capabilities: Capability.DOM, autostart: false});
export const enum Events {
MODEL_UPDATED = 'ModelUpdated',
WARNINGS_UPDATED = 'WarningsUpdated',
}
export interface EventTypes {
[Events.MODEL_UPDATED]: void;
[Events.WARNINGS_UPDATED]: Protocol.Preload.PreloadEnabledStateUpdatedEvent;
}
class PreloadDispatcher implements ProtocolProxyApi.PreloadDispatcher {
private model: PreloadingModel;
constructor(model: PreloadingModel) {
this.model = model;
}
ruleSetUpdated(event: Protocol.Preload.RuleSetUpdatedEvent): void {
this.model.onRuleSetUpdated(event);
}
ruleSetRemoved(event: Protocol.Preload.RuleSetRemovedEvent): void {
this.model.onRuleSetRemoved(event);
}
preloadingAttemptSourcesUpdated(event: Protocol.Preload.PreloadingAttemptSourcesUpdatedEvent): void {
this.model.onPreloadingAttemptSourcesUpdated(event);
}
prefetchStatusUpdated(event: Protocol.Preload.PrefetchStatusUpdatedEvent): void {
this.model.onPrefetchStatusUpdated(event);
}
prerenderStatusUpdated(event: Protocol.Preload.PrerenderStatusUpdatedEvent): void {
this.model.onPrerenderStatusUpdated(event);
}
preloadEnabledStateUpdated(event: Protocol.Preload.PreloadEnabledStateUpdatedEvent): void {
void this.model.onPreloadEnabledStateUpdated(event);
}
}
class DocumentPreloadingData {
ruleSets: RuleSetRegistry = new RuleSetRegistry();
preloadingAttempts: PreloadingAttemptRegistry = new PreloadingAttemptRegistry();
sources: SourceRegistry = new SourceRegistry();
mergePrevious(prev: DocumentPreloadingData): void {
// Note that CDP events Preload.ruleSetUpdated/Deleted and
// Preload.preloadingAttemptSourcesUpdated with a loaderId are emitted to target that bounded to
// a document with the loaderId. On the other hand, prerendering activation changes targets
// of Preload.prefetch/prerenderStatusUpdated, i.e. activated page receives those events for
// triggering outcome "Success".
if (!this.ruleSets.isEmpty() || !this.sources.isEmpty()) {
throw new Error('unreachable');
}
this.ruleSets = prev.ruleSets;
this.preloadingAttempts.mergePrevious(prev.preloadingAttempts);
this.sources = prev.sources;
}
}
class RuleSetRegistry {
private map: Map<Protocol.Preload.RuleSetId, Protocol.Preload.RuleSet> =
new Map<Protocol.Preload.RuleSetId, Protocol.Preload.RuleSet>();
isEmpty(): boolean {
return this.map.size === 0;
}
// Returns reference. Don't save returned values.
// Returned values may or may not be updated as the time grows.
getById(id: Protocol.Preload.RuleSetId): Protocol.Preload.RuleSet|null {
return this.map.get(id) || null;
}
// Returns reference. Don't save returned values.
// Returned values may or may not be updated as the time grows.
getAll(): Array<WithId<Protocol.Preload.RuleSetId, Protocol.Preload.RuleSet>> {
return Array.from(this.map.entries()).map(([id, value]) => ({id, value}));
}
upsert(ruleSet: Protocol.Preload.RuleSet): void {
this.map.set(ruleSet.id, ruleSet);
}
delete(id: Protocol.Preload.RuleSetId): void {
this.map.delete(id);
}
}
// Protocol.Preload.PreloadingStatus|'NotTriggered'
//
// A renderer sends SpeculationCandidate to the browser process and the
// browser process checks eligibilities, and starts PreloadingAttempt.
//
// In the frontend, "NotTriggered" is used to denote that a
// PreloadingAttempt is waiting for at trigger event (eg:
// mousedown/mouseover). All PreloadingAttempts will start off as
// "NotTriggered", but "eager" preloading attempts (attempts not
// actually waiting for any trigger) will be processed by the browser
// immediately, and will not stay in this state for long.
//
// TODO(https://crbug.com/1384419): Add NotEligible.
export const enum PreloadingStatus {
NOT_TRIGGERED = 'NotTriggered',
PENDING = 'Pending',
RUNNING = 'Running',
READY = 'Ready',
SUCCESS = 'Success',
FAILURE = 'Failure',
NOT_SUPPORTED = 'NotSupported',
}
function convertPreloadingStatus(status: Protocol.Preload.PreloadingStatus): PreloadingStatus {
switch (status) {
case Protocol.Preload.PreloadingStatus.Pending:
return PreloadingStatus.PENDING;
case Protocol.Preload.PreloadingStatus.Running:
return PreloadingStatus.RUNNING;
case Protocol.Preload.PreloadingStatus.Ready:
return PreloadingStatus.READY;
case Protocol.Preload.PreloadingStatus.Success:
return PreloadingStatus.SUCCESS;
case Protocol.Preload.PreloadingStatus.Failure:
return PreloadingStatus.FAILURE;
case Protocol.Preload.PreloadingStatus.NotSupported:
return PreloadingStatus.NOT_SUPPORTED;
}
throw new Error('unreachable');
}
export type PreloadingAttemptId = string;
export type PreloadingAttempt = PrefetchAttempt|PrerenderAttempt;
export interface PrefetchAttempt {
action: Protocol.Preload.SpeculationAction.Prefetch;
key: Protocol.Preload.PreloadingAttemptKey;
pipelineId: Protocol.Preload.PreloadPipelineId|null;
status: PreloadingStatus;
prefetchStatus: Protocol.Preload.PrefetchStatus|null;
requestId: Protocol.Network.RequestId;
ruleSetIds: Protocol.Preload.RuleSetId[];
nodeIds: Protocol.DOM.BackendNodeId[];
}
export interface PrerenderAttempt {
action: Protocol.Preload.SpeculationAction.Prerender;
key: Protocol.Preload.PreloadingAttemptKey;
pipelineId: Protocol.Preload.PreloadPipelineId|null;
status: PreloadingStatus;
prerenderStatus: Protocol.Preload.PrerenderFinalStatus|null;
disallowedMojoInterface: string|null;
mismatchedHeaders: Protocol.Preload.PrerenderMismatchedHeaders[]|null;
ruleSetIds: Protocol.Preload.RuleSetId[];
nodeIds: Protocol.DOM.BackendNodeId[];
}
export type PreloadingAttemptInternal = PrefetchAttemptInternal|PrerenderAttemptInternal;
export interface PrefetchAttemptInternal {
action: Protocol.Preload.SpeculationAction.Prefetch;
key: Protocol.Preload.PreloadingAttemptKey;
pipelineId: Protocol.Preload.PreloadPipelineId|null;
status: PreloadingStatus;
prefetchStatus: Protocol.Preload.PrefetchStatus|null;
requestId: Protocol.Network.RequestId;
}
export interface PrerenderAttemptInternal {
action: Protocol.Preload.SpeculationAction.Prerender;
key: Protocol.Preload.PreloadingAttemptKey;
pipelineId: Protocol.Preload.PreloadPipelineId|null;
status: PreloadingStatus;
prerenderStatus: Protocol.Preload.PrerenderFinalStatus|null;
disallowedMojoInterface: string|null;
mismatchedHeaders: Protocol.Preload.PrerenderMismatchedHeaders[]|null;
}
function makePreloadingAttemptId(key: Protocol.Preload.PreloadingAttemptKey): PreloadingAttemptId {
let action;
switch (key.action) {
case Protocol.Preload.SpeculationAction.Prefetch:
action = 'Prefetch';
break;
case Protocol.Preload.SpeculationAction.Prerender:
action = 'Prerender';
break;
}
let targetHint;
switch (key.targetHint) {
case undefined:
targetHint = 'undefined';
break;
case Protocol.Preload.SpeculationTargetHint.Blank:
targetHint = 'Blank';
break;
case Protocol.Preload.SpeculationTargetHint.Self:
targetHint = 'Self';
break;
}
return `${key.loaderId}:${action}:${key.url}:${targetHint}`;
}
export class PreloadPipeline {
private inner: Map<Protocol.Preload.SpeculationAction, PreloadingAttempt>;
constructor(inner: Map<Protocol.Preload.SpeculationAction, PreloadingAttempt>) {
if (inner.size === 0) {
throw new Error('unreachable');
}
this.inner = inner;
}
static newFromAttemptsForTesting(attempts: PreloadingAttempt[]): PreloadPipeline {
const inner = new Map();
for (const attempt of attempts) {
inner.set(attempt.action, attempt);
}
return new PreloadPipeline(inner);
}
getOriginallyTriggered(): PreloadingAttempt {
const attempt = this.getPrerender() || this.getPrefetch();
assertNotNullOrUndefined(attempt);
return attempt;
}
getPrefetch(): PreloadingAttempt|null {
return this.inner.get(Protocol.Preload.SpeculationAction.Prefetch) || null;
}
getPrerender(): PreloadingAttempt|null {
return this.inner.get(Protocol.Preload.SpeculationAction.Prerender) || null;
}
// Returns attempts in the order: prefetch < prerender.
// Currently unused.
getAttempts(): PreloadingAttempt[] {
const ret = [];
const prefetch = this.getPrefetch();
if (prefetch !== null) {
ret.push(prefetch);
}
const prerender = this.getPrerender();
if (prerender !== null) {
ret.push(prerender);
}
if (ret.length === 0) {
throw new Error('unreachable');
}
return ret;
}
}
class PreloadingAttemptRegistry {
private map: Map<PreloadingAttemptId, PreloadingAttemptInternal> =
new Map<PreloadingAttemptId, PreloadingAttemptInternal>();
private pipelines:
MapWithDefault<Protocol.Preload.PreloadPipelineId, Map<Protocol.Preload.SpeculationAction, PreloadingAttemptId>> =
new MapWithDefault<
Protocol.Preload.PreloadPipelineId, Map<Protocol.Preload.SpeculationAction, PreloadingAttemptId>>();
private enrich(attempt: PreloadingAttemptInternal, source: Protocol.Preload.PreloadingAttemptSource|null):
PreloadingAttempt {
let ruleSetIds: Protocol.Preload.RuleSetId[] = [];
let nodeIds: Protocol.DOM.BackendNodeId[] = [];
if (source !== null) {
ruleSetIds = source.ruleSetIds;
nodeIds = source.nodeIds;
}
return {
...attempt,
ruleSetIds,
nodeIds,
};
}
// Returns true iff the attempt is triggered by a SpecRules, not automatically derived.
//
// In some cases, browsers automatically triggers preloads. For example, Chrome triggers prefetch
// ahead of prerender to prevent multiple fetches in case that the prerender failed due to, e.g.
// use of forbidden mojo APIs. Such prefetch and prerender sit in the same preload pipeline.
//
// We regard them as not representative and only show the representative ones to represent
// pipelines.
private isAttemptRepresentative(attempt: PreloadingAttempt): boolean {
function getSortKey(action: Protocol.Preload.SpeculationAction): number {
switch (action) {
case Protocol.Preload.SpeculationAction.Prefetch:
return 0;
case Protocol.Preload.SpeculationAction.Prerender:
return 1;
}
}
// Attempt with status `NOT_TRIGGERED` is a representative of a pipeline.
if (attempt.pipelineId === null) {
return true;
}
// Attempt with the strongest action in pipeline is a representative of a pipeline.
// Order: prefetch < prerender.
const pipeline = this.pipelines.get(attempt.pipelineId);
assertNotNullOrUndefined(pipeline);
if (pipeline.size === 0) {
throw new Error('unreachable');
}
return [...pipeline.keys()].every(action => getSortKey(action) <= getSortKey(attempt.action));
}
// Returns reference. Don't save returned values.
// Returned values may or may not be updated as the time grows.
getById(id: PreloadingAttemptId, sources: SourceRegistry): PreloadingAttempt|null {
const attempt = this.map.get(id) || null;
if (attempt === null) {
return null;
}
return this.enrich(attempt, sources.getById(id));
}
// Returns representative preloading attempts that triggered by the rule set with `ruleSetId`.
// `ruleSetId === null` means "do not filter".
//
// Returns reference. Don't save returned values.
// Returned values may or may not be updated as the time grows.
getAllRepresentative(ruleSetId: Protocol.Preload.RuleSetId|null, sources: SourceRegistry):
Array<WithId<PreloadingAttemptId, PreloadingAttempt>> {
return [...this.map.entries()]
.map(([id, value]) => ({id, value: this.enrich(value, sources.getById(id))}))
.filter(({value}) => !ruleSetId || value.ruleSetIds.includes(ruleSetId))
.filter(({value}) => this.isAttemptRepresentative(value));
}
getPipeline(pipelineId: Protocol.Preload.PreloadPipelineId, sources: SourceRegistry):
Map<Protocol.Preload.SpeculationAction, PreloadingAttempt>|null {
const pipeline = this.pipelines.get(pipelineId);
if (pipeline === undefined || pipeline.size === 0) {
return null;
}
const map: Record<PreloadingAttemptId, PreloadingAttemptInternal> = {};
for (const [id, attempt] of this.map.entries()) {
map[id] = attempt;
}
return new Map(pipeline.entries().map(([action, id]) => {
const attempt = this.getById(id, sources);
assertNotNullOrUndefined(attempt);
return [action, attempt];
}));
}
upsert(attempt: PreloadingAttemptInternal): void {
const id = makePreloadingAttemptId(attempt.key);
this.map.set(id, attempt);
if (attempt.pipelineId !== null) {
this.pipelines.getOrInsertComputed(attempt.pipelineId, () => new Map()).set(attempt.action, id);
}
}
private reconstructPipelines(): void {
this.pipelines.clear();
for (const [id, attempt] of this.map.entries()) {
if (attempt.pipelineId === null) {
continue;
}
const pipeline = this.pipelines.getOrInsertComputed(attempt.pipelineId, () => new Map());
pipeline.set(attempt.action, id);
}
}
// Speculation rules emits a CDP event Preload.preloadingAttemptSourcesUpdated
// and an IPC SpeculationHost::UpdateSpeculationCandidates. The latter emits
// Preload.prefetch/prerenderAttemptUpdated for each preload attempt triggered.
// In general, "Not triggered to triggered" period is short (resp. long) for
// eager (resp. non-eager) preloads. For not yet emitted ones, we fill
// "Not triggered" preload attempts and show them.
maybeRegisterNotTriggered(sources: SourceRegistry): void {
for (const [id, {key}] of sources.entries()) {
if (this.map.get(id) !== undefined) {
continue;
}
let attempt: PreloadingAttemptInternal;
switch (key.action) {
case Protocol.Preload.SpeculationAction.Prefetch:
attempt = {
action: Protocol.Preload.SpeculationAction.Prefetch,
key,
pipelineId: null,
status: PreloadingStatus.NOT_TRIGGERED,
prefetchStatus: null,
// Fill invalid request id.
requestId: '' as Protocol.Network.RequestId,
};
break;
case Protocol.Preload.SpeculationAction.Prerender:
attempt = {
action: Protocol.Preload.SpeculationAction.Prerender,
key,
pipelineId: null,
status: PreloadingStatus.NOT_TRIGGERED,
prerenderStatus: null,
disallowedMojoInterface: null,
mismatchedHeaders: null,
};
break;
}
this.map.set(id, attempt);
}
}
// Removes keys in `this.map` that are not in `sources`. This is used to
// remove attempts that no longer have a matching speculation rule.
cleanUpRemovedAttempts(sources: SourceRegistry): void {
const keysToRemove = Array.from(this.map.keys()).filter(key => !sources.getById(key));
for (const key of keysToRemove) {
this.map.delete(key);
}
this.reconstructPipelines();
}
mergePrevious(prev: PreloadingAttemptRegistry): void {
for (const [id, attempt] of this.map.entries()) {
prev.map.set(id, attempt);
}
this.map = prev.map;
this.reconstructPipelines();
}
}
class SourceRegistry {
private map: Map<PreloadingAttemptId, Protocol.Preload.PreloadingAttemptSource> =
new Map<PreloadingAttemptId, Protocol.Preload.PreloadingAttemptSource>();
entries(): IterableIterator<[PreloadingAttemptId, Protocol.Preload.PreloadingAttemptSource]> {
return this.map.entries();
}
isEmpty(): boolean {
return this.map.size === 0;
}
getById(id: PreloadingAttemptId): Protocol.Preload.PreloadingAttemptSource|null {
return this.map.get(id) || null;
}
update(sources: Protocol.Preload.PreloadingAttemptSource[]): void {
this.map = new Map(sources.map(s => [makePreloadingAttemptId(s.key), s]));
}
}