mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
perf(observation): reduce document processing scheduling overhead
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { OVERLAY_HOST_MARKER_ATTR } from "@/lib/overlay-bridge";
|
import { OVERLAY_HOST_MARKER_ATTR } from "@/lib/overlay-bridge";
|
||||||
|
import { createCaptureCheckpoint } from "../capture-abort";
|
||||||
import { buildDocumentIndex, type DecodedNode } from "../facts";
|
import { buildDocumentIndex, type DecodedNode } from "../facts";
|
||||||
import { decodeDocument, REQUESTED_STYLES } from "../snapshot";
|
import { decodeDocument, REQUESTED_STYLES } from "../snapshot";
|
||||||
|
|
||||||
@@ -90,7 +91,23 @@ describe("document facts", () => {
|
|||||||
expect(nodes[0]).not.toHaveProperty("excluded");
|
expect(nodes[0]).not.toHaveProperty("excluded");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("yields during large inputs so cancellation interrupts indexing", async () => {
|
it("keeps short blocks synchronous and checks cancellation after yielding", async () => {
|
||||||
|
const clock = vi.spyOn(performance, "now").mockReturnValue(0);
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const checkpoint = createCaptureCheckpoint(controller.signal);
|
||||||
|
for (let i = 0; i < 20; i++) expect(checkpoint()).toBeUndefined();
|
||||||
|
clock.mockReturnValue(9);
|
||||||
|
const pending = checkpoint();
|
||||||
|
expect(pending).toBeInstanceOf(Promise);
|
||||||
|
controller.abort();
|
||||||
|
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
|
||||||
|
} finally {
|
||||||
|
clock.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks cancellation at bounded indexing blocks", async () => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const input = Array.from({ length: 100_000 }, (_, i) => node(i, i ? i - 1 : null));
|
const input = Array.from({ length: 100_000 }, (_, i) => node(i, i ? i - 1 : null));
|
||||||
let reads = 0;
|
let reads = 0;
|
||||||
|
|||||||
@@ -16,17 +16,25 @@ export function throwIfAborted(signal: AbortSignal | undefined): void {
|
|||||||
if (signal?.aborted) throw captureAbortError();
|
if (signal?.aborted) throw captureAbortError();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called only at bounded block boundaries, not once per node. */
|
/** Check at bounded work blocks; yield only after a processing slice is spent.
|
||||||
export async function captureCheckpoint(signal?: AbortSignal): Promise<void> {
|
* Each caller owns its deadline, so unrelated captures do not share state. */
|
||||||
throwIfAborted(signal);
|
export function createCaptureCheckpoint(signal?: AbortSignal): () => Promise<void> | undefined {
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const channel = new MessageChannel();
|
|
||||||
channel.port1.onmessage = () => {
|
|
||||||
channel.port1.close();
|
|
||||||
channel.port2.close();
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
channel.port2.postMessage(null);
|
|
||||||
});
|
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
|
let deadline = performance.now() + 8;
|
||||||
|
return () => {
|
||||||
|
throwIfAborted(signal);
|
||||||
|
if (performance.now() < deadline) return;
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
channel.port1.onmessage = () => {
|
||||||
|
channel.port1.close();
|
||||||
|
channel.port2.close();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
channel.port2.postMessage(null);
|
||||||
|
}).then(() => {
|
||||||
|
throwIfAborted(signal);
|
||||||
|
deadline = performance.now() + 8;
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isOverlayHostNode } from "@/lib/overlay-bridge";
|
import { isOverlayHostNode } from "@/lib/overlay-bridge";
|
||||||
import { captureCheckpoint } from "./capture-abort";
|
import { createCaptureCheckpoint } from "./capture-abort";
|
||||||
import type { CapturedNode } from "./capture-types";
|
import type { CapturedNode } from "./capture-types";
|
||||||
|
|
||||||
/** Bounds retain their snapshot document CSS units until normalization. */
|
/** Bounds retain their snapshot document CSS units until normalization. */
|
||||||
@@ -34,24 +34,34 @@ export async function buildDocumentIndex<T extends DecodedNode>(
|
|||||||
input: readonly T[],
|
input: readonly T[],
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<DocumentIndex<T>> {
|
): Promise<DocumentIndex<T>> {
|
||||||
|
const checkpoint = createCaptureCheckpoint(signal);
|
||||||
const nodes = new Map<number, T>();
|
const nodes = new Map<number, T>();
|
||||||
const overlayByNode = new Map<number, boolean>();
|
const overlayByNode = new Map<number, boolean>();
|
||||||
const excludedBackendNodeIds = new Set<number>();
|
const excludedBackendNodeIds = new Set<number>();
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
if (i % 256 === 0) await captureCheckpoint(signal);
|
if (i % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
const node = input[i];
|
const node = input[i];
|
||||||
nodes.set(node.backendNodeId, node);
|
nodes.set(node.backendNodeId, node);
|
||||||
}
|
}
|
||||||
let work = 0;
|
let work = 0;
|
||||||
for (const node of input) {
|
for (const node of input) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
if (overlayByNode.has(node.backendNodeId)) continue;
|
if (overlayByNode.has(node.backendNodeId)) continue;
|
||||||
const path: DecodedNode[] = [];
|
const path: DecodedNode[] = [];
|
||||||
const visiting = new Set<number>();
|
const visiting = new Set<number>();
|
||||||
let current: DecodedNode | undefined = node;
|
let current: DecodedNode | undefined = node;
|
||||||
let overlay = false;
|
let overlay = false;
|
||||||
while (current && !overlayByNode.has(current.backendNodeId)) {
|
while (current && !overlayByNode.has(current.backendNodeId)) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
if (visiting.has(current.backendNodeId)) {
|
if (visiting.has(current.backendNodeId)) {
|
||||||
// Only an overlay inside the cycle may mark the cycle and its descendants.
|
// Only an overlay inside the cycle may mark the cycle and its descendants.
|
||||||
overlay = path
|
overlay = path
|
||||||
@@ -70,7 +80,10 @@ export async function buildDocumentIndex<T extends DecodedNode>(
|
|||||||
if (current && overlayByNode.has(current.backendNodeId))
|
if (current && overlayByNode.has(current.backendNodeId))
|
||||||
overlay = overlayByNode.get(current.backendNodeId)!;
|
overlay = overlayByNode.get(current.backendNodeId)!;
|
||||||
for (let i = path.length - 1; i >= 0; i--) {
|
for (let i = path.length - 1; i >= 0; i--) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
const item = path[i];
|
const item = path[i];
|
||||||
overlay = overlay || isOverlayHostNode(item.tag, Object.keys(item.attrs));
|
overlay = overlay || isOverlayHostNode(item.tag, Object.keys(item.attrs));
|
||||||
overlayByNode.set(item.backendNodeId, overlay);
|
overlayByNode.set(item.backendNodeId, overlay);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
type CdpTarget,
|
type CdpTarget,
|
||||||
cdpTargetKey,
|
cdpTargetKey,
|
||||||
} from "@/browser-driver/frame-graph";
|
} from "@/browser-driver/frame-graph";
|
||||||
import { captureCheckpoint } from "./capture-abort";
|
import { createCaptureCheckpoint } from "./capture-abort";
|
||||||
import type { CapturedNode } from "./capture-types";
|
import type { CapturedNode } from "./capture-types";
|
||||||
|
|
||||||
export interface FrameOwnedAxNode {
|
export interface FrameOwnedAxNode {
|
||||||
@@ -88,6 +88,7 @@ export async function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
|||||||
captured: FrameDomInput,
|
captured: FrameDomInput,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<FrameDocument<T>[]> {
|
): Promise<FrameDocument<T>[]> {
|
||||||
|
const checkpoint = createCaptureCheckpoint(signal);
|
||||||
let work = 0;
|
let work = 0;
|
||||||
const frames = frameList(graph, batches, captured);
|
const frames = frameList(graph, batches, captured);
|
||||||
const frameById = new Map(frames.map((frame) => [frame.frameId, frame]));
|
const frameById = new Map(frames.map((frame) => [frame.frameId, frame]));
|
||||||
@@ -97,7 +98,10 @@ export async function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
|||||||
const backendOwner = new Map<string, string>();
|
const backendOwner = new Map<string, string>();
|
||||||
for (const frame of frames) {
|
for (const frame of frames) {
|
||||||
for (const node of domNodesForFrame(frame.frameId)) {
|
for (const node of domNodesForFrame(frame.frameId)) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
backendOwner.set(targetBackendKey(frame.target, node.backendNodeId), frame.frameId);
|
backendOwner.set(targetBackendKey(frame.target, node.backendNodeId), frame.frameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,17 +110,23 @@ export async function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
|||||||
for (const batch of batches) {
|
for (const batch of batches) {
|
||||||
const nodeById = new Map<string, T>();
|
const nodeById = new Map<string, T>();
|
||||||
for (const node of batch.nodes) {
|
for (const node of batch.nodes) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
nodeById.set(node.nodeId, node);
|
nodeById.set(node.nodeId, node);
|
||||||
}
|
}
|
||||||
const ownershipByNodeId = new Map<string, Ownership>();
|
const ownershipByNodeId = new Map<string, Ownership>();
|
||||||
const resolveOwnership = async (node: T): Promise<Ownership> => {
|
for (const node of batch.nodes) {
|
||||||
const path: T[] = [];
|
const path: T[] = [];
|
||||||
const visiting = new Set<string>();
|
const visiting = new Set<string>();
|
||||||
let current: T | undefined = node;
|
let current: T | undefined = node;
|
||||||
let ownership: Ownership = { frameId: batch.frame.frameId, strength: 1 };
|
let ownership: Ownership = { frameId: batch.frame.frameId, strength: 1 };
|
||||||
while (current) {
|
while (current) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
const cached = ownershipByNodeId.get(current.nodeId);
|
const cached = ownershipByNodeId.get(current.nodeId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
ownership = cached;
|
ownership = cached;
|
||||||
@@ -142,17 +152,14 @@ export async function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
|||||||
current = current.parentId ? nodeById.get(current.parentId) : undefined;
|
current = current.parentId ? nodeById.get(current.parentId) : undefined;
|
||||||
}
|
}
|
||||||
for (let i = path.length - 1; i >= 0; i--) {
|
for (let i = path.length - 1; i >= 0; i--) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
ownership = { frameId: ownership.frameId, strength: Math.min(2, ownership.strength) };
|
ownership = { frameId: ownership.frameId, strength: Math.min(2, ownership.strength) };
|
||||||
ownershipByNodeId.set(path[i].nodeId, ownership);
|
ownershipByNodeId.set(path[i].nodeId, ownership);
|
||||||
}
|
}
|
||||||
if (!ownershipByNodeId.has(node.nodeId)) ownershipByNodeId.set(node.nodeId, ownership);
|
if (!ownershipByNodeId.has(node.nodeId)) ownershipByNodeId.set(node.nodeId, ownership);
|
||||||
return ownership;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const node of batch.nodes) {
|
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
|
||||||
const ownership = await resolveOwnership(node);
|
|
||||||
const key = targetNodeKey(batch.frame.target, node.nodeId);
|
const key = targetNodeKey(batch.frame.target, node.nodeId);
|
||||||
const existing = candidates.get(key);
|
const existing = candidates.get(key);
|
||||||
if (!existing || ownership.strength > existing.ownership.strength) {
|
if (!existing || ownership.strength > existing.ownership.strength) {
|
||||||
@@ -166,7 +173,10 @@ export async function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
|||||||
);
|
);
|
||||||
const axNodesByFrame = new Map<string, T[]>();
|
const axNodesByFrame = new Map<string, T[]>();
|
||||||
for (const candidate of candidates.values()) {
|
for (const candidate of candidates.values()) {
|
||||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
if (work++ % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
const { node, target, ownership } = candidate;
|
const { node, target, ownership } = candidate;
|
||||||
const parentFrameId = node.parentId
|
const parentFrameId = node.parentId
|
||||||
? ownershipByTargetNode.get(targetNodeKey(target, node.parentId))
|
? ownershipByTargetNode.get(targetNodeKey(target, node.parentId))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
projectSnapshotRect,
|
projectSnapshotRect,
|
||||||
snapshotViewportRect,
|
snapshotViewportRect,
|
||||||
} from "../geometry/coordinate-types";
|
} from "../geometry/coordinate-types";
|
||||||
import { captureCheckpoint } from "./capture-abort";
|
import { createCaptureCheckpoint } from "./capture-abort";
|
||||||
import { buildDocumentIndex, type DocumentIndex, type NodeFacts } from "./facts";
|
import { buildDocumentIndex, type DocumentIndex, type NodeFacts } from "./facts";
|
||||||
import { decodeDocument, type SnapshotDocument } from "./snapshot";
|
import { decodeDocument, type SnapshotDocument } from "./snapshot";
|
||||||
|
|
||||||
@@ -30,9 +30,13 @@ export async function normalizeDocument(
|
|||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<NormalizedDocument> {
|
): Promise<NormalizedDocument> {
|
||||||
const decoded = await decodeDocument(doc, strings, signal);
|
const decoded = await decodeDocument(doc, strings, signal);
|
||||||
|
const checkpoint = createCaptureCheckpoint(signal);
|
||||||
const nodes: NodeFacts[] = [];
|
const nodes: NodeFacts[] = [];
|
||||||
for (let i = 0; i < decoded.nodes.length; i++) {
|
for (let i = 0; i < decoded.nodes.length; i++) {
|
||||||
if (i % 256 === 0) await captureCheckpoint(signal);
|
if (i % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
const node = decoded.nodes[i];
|
const node = decoded.nodes[i];
|
||||||
const input = snapshotViewportRect(
|
const input = snapshotViewportRect(
|
||||||
node.layout?.bounds ?? [],
|
node.layout?.bounds ?? [],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { captureCheckpoint } from "./capture-abort";
|
import { createCaptureCheckpoint } from "./capture-abort";
|
||||||
import type { CapturedNode } from "./capture-types";
|
import type { CapturedNode } from "./capture-types";
|
||||||
import type { DecodedDocument, DecodedNode } from "./facts";
|
import type { DecodedDocument, DecodedNode } from "./facts";
|
||||||
export const REQUESTED_STYLES = [
|
export const REQUESTED_STYLES = [
|
||||||
@@ -102,6 +102,7 @@ export async function decodeDocument(
|
|||||||
strings: string[],
|
strings: string[],
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<DecodedDocument> {
|
): Promise<DecodedDocument> {
|
||||||
|
const checkpoint = createCaptureCheckpoint(signal);
|
||||||
const dn = doc.nodes;
|
const dn = doc.nodes;
|
||||||
const dl = doc.layout;
|
const dl = doc.layout;
|
||||||
if (!dn?.backendNodeId) {
|
if (!dn?.backendNodeId) {
|
||||||
@@ -111,7 +112,10 @@ export async function decodeDocument(
|
|||||||
const count = dn.backendNodeId.length;
|
const count = dn.backendNodeId.length;
|
||||||
const layoutByNode = new Map<number, number>();
|
const layoutByNode = new Map<number, number>();
|
||||||
for (let i = 0; i < (dl?.nodeIndex?.length ?? 0); i++) {
|
for (let i = 0; i < (dl?.nodeIndex?.length ?? 0); i++) {
|
||||||
if (i % 256 === 0) await captureCheckpoint(signal);
|
if (i % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
layoutByNode.set(dl!.nodeIndex![i], i);
|
layoutByNode.set(dl!.nodeIndex![i], i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +130,10 @@ export async function decodeDocument(
|
|||||||
const nodeTextContent = new Map<number, string[]>();
|
const nodeTextContent = new Map<number, string[]>();
|
||||||
if (dn.nodeValue) {
|
if (dn.nodeValue) {
|
||||||
for (let n = 0; n < count; n++) {
|
for (let n = 0; n < count; n++) {
|
||||||
if (n % 256 === 0) await captureCheckpoint(signal);
|
if (n % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
const nvIdx = dn.nodeValue[n] ?? -1;
|
const nvIdx = dn.nodeValue[n] ?? -1;
|
||||||
if (nvIdx < 0) continue;
|
if (nvIdx < 0) continue;
|
||||||
const text = str(strings, nvIdx).trim();
|
const text = str(strings, nvIdx).trim();
|
||||||
@@ -143,7 +150,10 @@ export async function decodeDocument(
|
|||||||
// Decode fields without assigning a coordinate projection or semantic policy.
|
// Decode fields without assigning a coordinate projection or semantic policy.
|
||||||
const nodes: DecodedNode[] = [];
|
const nodes: DecodedNode[] = [];
|
||||||
for (let n = 0; n < count; n++) {
|
for (let n = 0; n < count; n++) {
|
||||||
if (n % 256 === 0) await captureCheckpoint(signal);
|
if (n % 256 === 0) {
|
||||||
|
const pending = checkpoint();
|
||||||
|
if (pending) await pending;
|
||||||
|
}
|
||||||
const backendNodeId = dn.backendNodeId[n];
|
const backendNodeId = dn.backendNodeId[n];
|
||||||
const parentIdx = dn.parentIndex?.[n] ?? -1;
|
const parentIdx = dn.parentIndex?.[n] ?? -1;
|
||||||
const parentBackendNodeId = parentIdx >= 0 ? (dn.backendNodeId[parentIdx] ?? null) : null;
|
const parentBackendNodeId = parentIdx >= 0 ? (dn.backendNodeId[parentIdx] ?? null) : null;
|
||||||
|
|||||||
Reference in New Issue
Block a user