mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(vom): add fixtures, remove fixed canvas numbers under budget, improve efficiency
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
import type { Rect } from "@browser-skill/vom";
|
||||
import type { RenderedSurface, RenderedSurfaceGroup } from "./types";
|
||||
import type { ClusteredRenderedSurfaces, RenderedSurface, RenderedSurfaceGroup } from "./types";
|
||||
|
||||
const STACK_IOU_RATIO = 0.9;
|
||||
const STACK_SIZE_RATIO = 0.9;
|
||||
const SIZE_BAND_BASE = 1 / STACK_SIZE_RATIO;
|
||||
const POSITION_CELL_RATIO = 0.1;
|
||||
const MAX_CLUSTER_COMPARISONS = 1_000_000;
|
||||
const MAX_CLUSTER_SURFACES = 50_000;
|
||||
|
||||
interface IndexedGroup extends RenderedSurfaceGroup {
|
||||
creationIndex: number;
|
||||
indexKey: string;
|
||||
}
|
||||
|
||||
function area(rect: Rect): number {
|
||||
return Math.max(0, rect.w) * Math.max(0, rect.h);
|
||||
@@ -14,9 +23,7 @@ function intersectionArea(a: Rect, b: Rect): number {
|
||||
return width * height;
|
||||
}
|
||||
|
||||
function sameVisualStack(a: RenderedSurface, b: RenderedSurface): boolean {
|
||||
if (a.frameId !== b.frameId) return false;
|
||||
if (a.parentBackendNodeId !== b.parentBackendNodeId) return false;
|
||||
function stackScore(a: RenderedSurface, b: RenderedSurface): number | null {
|
||||
const aArea = area(a.visibleRect);
|
||||
const bArea = area(b.visibleRect);
|
||||
const intersection = intersectionArea(a.visibleRect, b.visibleRect);
|
||||
@@ -25,41 +32,158 @@ function sameVisualStack(a: RenderedSurface, b: RenderedSurface): boolean {
|
||||
Math.min(a.visibleRect.w, b.visibleRect.w) / Math.max(a.visibleRect.w, b.visibleRect.w);
|
||||
const heightRatio =
|
||||
Math.min(a.visibleRect.h, b.visibleRect.h) / Math.max(a.visibleRect.h, b.visibleRect.h);
|
||||
return (
|
||||
union > 0 &&
|
||||
intersection / union >= STACK_IOU_RATIO &&
|
||||
widthRatio >= STACK_SIZE_RATIO &&
|
||||
heightRatio >= STACK_SIZE_RATIO
|
||||
);
|
||||
const iou = union > 0 ? intersection / union : 0;
|
||||
return iou >= STACK_IOU_RATIO && widthRatio >= STACK_SIZE_RATIO && heightRatio >= STACK_SIZE_RATIO
|
||||
? iou
|
||||
: null;
|
||||
}
|
||||
|
||||
function toGroup(members: RenderedSurface[]): RenderedSurfaceGroup {
|
||||
const representative = [...members].sort(
|
||||
(a, b) => area(b.visibleRect) - area(a.visibleRect) || b.paintOrder - a.paintOrder,
|
||||
)[0];
|
||||
function sizeBand(value: number): number {
|
||||
return Math.floor(Math.log(Math.max(Number.EPSILON, value)) / Math.log(SIZE_BAND_BASE));
|
||||
}
|
||||
|
||||
function bandSize(band: number): number {
|
||||
return SIZE_BAND_BASE ** band;
|
||||
}
|
||||
|
||||
function spatialKey(rect: Rect): string {
|
||||
const widthBand = sizeBand(rect.w);
|
||||
const heightBand = sizeBand(rect.h);
|
||||
const cellWidth = bandSize(widthBand) * POSITION_CELL_RATIO;
|
||||
const cellHeight = bandSize(heightBand) * POSITION_CELL_RATIO;
|
||||
const centerX = rect.x + rect.w / 2;
|
||||
const centerY = rect.y + rect.h / 2;
|
||||
return `${widthBand}:${heightBand}:${Math.floor(centerX / cellWidth)}:${Math.floor(centerY / cellHeight)}`;
|
||||
}
|
||||
|
||||
function candidateKeys(rect: Rect): string[] {
|
||||
const keys: string[] = [];
|
||||
const centerX = rect.x + rect.w / 2;
|
||||
const centerY = rect.y + rect.h / 2;
|
||||
const widthBand = sizeBand(rect.w);
|
||||
const heightBand = sizeBand(rect.h);
|
||||
// A >=0.9 size ratio crosses at most one logarithmic band. IoU >=0.9
|
||||
// likewise keeps centers within one 10%-of-size position cell.
|
||||
for (let dw = -1; dw <= 1; dw += 1) {
|
||||
for (let dh = -1; dh <= 1; dh += 1) {
|
||||
const candidateWidthBand = widthBand + dw;
|
||||
const candidateHeightBand = heightBand + dh;
|
||||
const cellWidth = bandSize(candidateWidthBand) * POSITION_CELL_RATIO;
|
||||
const cellHeight = bandSize(candidateHeightBand) * POSITION_CELL_RATIO;
|
||||
const cellX = Math.floor(centerX / cellWidth);
|
||||
const cellY = Math.floor(centerY / cellHeight);
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
keys.push(`${candidateWidthBand}:${candidateHeightBand}:${cellX + dx}:${cellY + dy}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function preferredRepresentative(a: RenderedSurface, b: RenderedSurface): RenderedSurface {
|
||||
const aArea = area(a.visibleRect);
|
||||
const bArea = area(b.visibleRect);
|
||||
return bArea > aArea || (bArea === aArea && b.paintOrder > a.paintOrder) ? b : a;
|
||||
}
|
||||
|
||||
function partitionKey(surface: RenderedSurface): string {
|
||||
return `${surface.frameId}\u0000${surface.parentBackendNodeId ?? "root"}`;
|
||||
}
|
||||
|
||||
export function clusterRenderedSurfaces(surfaces: RenderedSurface[]): ClusteredRenderedSurfaces {
|
||||
const partitions = new Map<string, Map<string, Set<IndexedGroup>>>();
|
||||
const groups: IndexedGroup[] = [];
|
||||
let comparisons = 0;
|
||||
let processed = 0;
|
||||
let truncated = false;
|
||||
|
||||
for (const surface of surfaces) {
|
||||
if (processed >= MAX_CLUSTER_SURFACES || comparisons >= MAX_CLUSTER_COMPARISONS) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
const key = partitionKey(surface);
|
||||
let index = partitions.get(key);
|
||||
if (!index) {
|
||||
index = new Map();
|
||||
partitions.set(key, index);
|
||||
}
|
||||
const candidates = new Set<IndexedGroup>();
|
||||
for (const candidateKey of candidateKeys(surface.visibleRect)) {
|
||||
for (const candidate of index.get(candidateKey) ?? []) candidates.add(candidate);
|
||||
}
|
||||
|
||||
let best: { group: IndexedGroup; score: number; areaDelta: number } | null = null;
|
||||
for (const group of candidates) {
|
||||
comparisons += 1;
|
||||
if (comparisons > MAX_CLUSTER_COMPARISONS) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
const score = stackScore(group.representative, surface);
|
||||
if (score === null) continue;
|
||||
const areaDelta = Math.abs(
|
||||
area(group.representative.visibleRect) - area(surface.visibleRect),
|
||||
);
|
||||
if (
|
||||
!best ||
|
||||
score > best.score ||
|
||||
(score === best.score && areaDelta < best.areaDelta) ||
|
||||
(score === best.score &&
|
||||
areaDelta === best.areaDelta &&
|
||||
group.creationIndex < best.group.creationIndex)
|
||||
) {
|
||||
best = { group, score, areaDelta };
|
||||
}
|
||||
}
|
||||
if (truncated) break;
|
||||
|
||||
if (best) {
|
||||
const group = best.group;
|
||||
const representative = preferredRepresentative(group.representative, surface);
|
||||
group.memberCount += 1;
|
||||
group.label ??= surface.label;
|
||||
if (representative !== group.representative) {
|
||||
index.get(group.indexKey)?.delete(group);
|
||||
group.representative = representative;
|
||||
group.indexKey = spatialKey(representative.visibleRect);
|
||||
const indexed = index.get(group.indexKey) ?? new Set<IndexedGroup>();
|
||||
indexed.add(group);
|
||||
index.set(group.indexKey, indexed);
|
||||
}
|
||||
processed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const indexKey = spatialKey(surface.visibleRect);
|
||||
const group: IndexedGroup = {
|
||||
frameId: surface.frameId,
|
||||
parentBackendNodeId: surface.parentBackendNodeId,
|
||||
representative: surface,
|
||||
...(surface.label ? { label: surface.label } : {}),
|
||||
memberCount: 1,
|
||||
creationIndex: groups.length,
|
||||
indexKey,
|
||||
};
|
||||
groups.push(group);
|
||||
const indexed = index.get(indexKey) ?? new Set<IndexedGroup>();
|
||||
indexed.add(group);
|
||||
index.set(indexKey, indexed);
|
||||
processed += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
frameId: representative.frameId,
|
||||
members,
|
||||
representative,
|
||||
label: members.map((surface) => surface.label).find((label) => label !== undefined),
|
||||
groups: groups
|
||||
.map(({ creationIndex: _creationIndex, indexKey: _indexKey, ...group }) => group)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.frameId.localeCompare(b.frameId) ||
|
||||
a.representative.visibleRect.y - b.representative.visibleRect.y ||
|
||||
a.representative.visibleRect.x - b.representative.visibleRect.x,
|
||||
),
|
||||
truncated,
|
||||
...(truncated ? { omittedCount: surfaces.length - processed } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function clusterRenderedSurfaces(surfaces: RenderedSurface[]): RenderedSurfaceGroup[] {
|
||||
const groups: RenderedSurface[][] = [];
|
||||
for (const surface of surfaces) {
|
||||
const matching = groups.find((group) =>
|
||||
group.some((member) => sameVisualStack(member, surface)),
|
||||
);
|
||||
if (matching) matching.push(surface);
|
||||
else groups.push([surface]);
|
||||
}
|
||||
return groups
|
||||
.map(toGroup)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.frameId.localeCompare(b.frameId) ||
|
||||
a.representative.visibleRect.y - b.representative.visibleRect.y ||
|
||||
a.representative.visibleRect.x - b.representative.visibleRect.x,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user