mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
perf(geometry): batch iframe owner size measurements
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
import { type CdpRunner, sendToCdpTarget } from "../shared";
|
||||
import type { CoordinateOwner, CssViewport, SnapshotProjectionResult } from "./coordinate-types";
|
||||
|
||||
import { readSnapshotOwnerSizes } from "./snapshot-owner-sizes";
|
||||
|
||||
export interface LayoutMetrics {
|
||||
cssVisualViewport?: { zoom?: number };
|
||||
visualViewport?: { zoom?: number };
|
||||
@@ -45,6 +47,7 @@ export class GeometryContext {
|
||||
private readonly owners = new Map<string, Promise<Quad | null>>();
|
||||
private readonly projections = new Map<string, Promise<GeometryProjection | null>>();
|
||||
private readonly snapshotOwners = new Map<string, Promise<{ quad: Quad; size: Size } | null>>();
|
||||
private readonly sizeBatches = new Map<string, () => Promise<Map<number, Size>>>();
|
||||
private active = 0;
|
||||
private readonly waiting: Array<() => void> = [];
|
||||
private graphPromise?: Promise<CdpFrameGraph | null>;
|
||||
@@ -186,12 +189,34 @@ export class GeometryContext {
|
||||
: { status: "unavailable", source, ownerBackendNodeId };
|
||||
}
|
||||
|
||||
/** Register one target's snapshot owners; the batch starts only when a
|
||||
* projection actually needs a size. Single owners keep the cheaper old path. */
|
||||
registerSnapshotOwners(target: CdpTarget, owners: ReadonlySet<number>): void {
|
||||
const key = cdpTargetKey(target);
|
||||
if (owners.size < 2 || this.sizeBatches.has(key)) return;
|
||||
let pending: Promise<Map<number, Size>> | undefined;
|
||||
this.sizeBatches.set(key, () => {
|
||||
pending ??= readSnapshotOwnerSizes(
|
||||
(method, params, cleanup) => this.request(target, method, params, cleanup),
|
||||
owners,
|
||||
).catch((error) => {
|
||||
if (error && typeof error === "object" && "name" in error && error.name === "AbortError")
|
||||
throw error;
|
||||
return new Map<number, Size>();
|
||||
});
|
||||
return pending;
|
||||
});
|
||||
}
|
||||
|
||||
private async snapshotOwner(
|
||||
target: CdpTarget,
|
||||
backendNodeId: number,
|
||||
): Promise<{ quad: Quad; size: Size } | null> {
|
||||
const quad = await this.ownerContent(target, backendNodeId);
|
||||
if (!quad) return null;
|
||||
const batchSize = (await this.sizeBatches.get(cdpTargetKey(target))?.())?.get(backendNodeId);
|
||||
if (this.signal?.aborted) throw new DOMException("geometry aborted", "AbortError");
|
||||
if (batchSize) return { quad, size: batchSize };
|
||||
const resolved = await this.request<{ object?: { objectId?: string } }>(
|
||||
target,
|
||||
"DOM.resolveNode",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Size } from "../geometry";
|
||||
|
||||
interface SerializedValue {
|
||||
type: string;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
type Send = <T>(method: string, params: object, cleanup?: boolean) => Promise<T>;
|
||||
|
||||
/** Batch only accessible frame owners. Missing/shadow/cross-origin owners keep
|
||||
* the existing node-scoped read; no page-wide style columns are requested. */
|
||||
export async function readSnapshotOwnerSizes(
|
||||
send: Send,
|
||||
owners: ReadonlySet<number>,
|
||||
): Promise<Map<number, Size>> {
|
||||
const sizes = new Map<number, Size>();
|
||||
const objectGroup = `bsk-frame-sizes-${crypto.randomUUID()}`;
|
||||
try {
|
||||
const reply = await send<{ result?: { deepSerializedValue?: SerializedValue } }>(
|
||||
"Runtime.evaluate",
|
||||
{
|
||||
expression: `(() => {
|
||||
const result = [];
|
||||
const documents = [document];
|
||||
for (let i = 0; i < documents.length && result.length < ${owners.size}; i++) {
|
||||
for (const owner of documents[i].querySelectorAll('iframe,frame')) {
|
||||
if (result.length >= ${owners.size}) break;
|
||||
if (!owner.isConnected) continue;
|
||||
const style = getComputedStyle(owner);
|
||||
result.push([owner, JSON.stringify({
|
||||
width: owner.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight),
|
||||
height: owner.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom)
|
||||
})]);
|
||||
try { if (owner.contentDocument) documents.push(owner.contentDocument); } catch {}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
})()`,
|
||||
objectGroup,
|
||||
serializationOptions: {
|
||||
serialization: "deep",
|
||||
maxDepth: 3,
|
||||
additionalParameters: { maxNodeDepth: 0, includeShadowTree: "none" },
|
||||
},
|
||||
},
|
||||
);
|
||||
const result = reply.result?.deepSerializedValue;
|
||||
if (result?.type !== "array" || !Array.isArray(result.value)) return sizes;
|
||||
for (const entry of result.value as SerializedValue[]) {
|
||||
if (entry.type !== "array" || !Array.isArray(entry.value)) continue;
|
||||
const [node, data] = entry.value as SerializedValue[];
|
||||
if (node?.type !== "node" || data?.type !== "string" || typeof data.value !== "string")
|
||||
continue;
|
||||
const id = (node.value as { backendNodeId?: number } | undefined)?.backendNodeId;
|
||||
if (id === undefined || !owners.has(id)) continue;
|
||||
try {
|
||||
const size = JSON.parse(data.value) as Size;
|
||||
if (
|
||||
size &&
|
||||
Number.isFinite(size.width) &&
|
||||
Number.isFinite(size.height) &&
|
||||
size.width > 0 &&
|
||||
size.height > 0
|
||||
)
|
||||
sizes.set(id, size);
|
||||
} catch {
|
||||
/* An invalid entry uses the existing individual read. */
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await send("Runtime.releaseObjectGroup", { objectGroup }, true).catch(() => {});
|
||||
}
|
||||
return sizes;
|
||||
}
|
||||
Reference in New Issue
Block a user