Summary
Capturing screen state on a DOM-heavy page (e.g. a flight-search results page, ~8k DOM nodes) is dramatically slower than it should be — on the order of ~9–12 s, vs ~1.5 s for the accessibility-only path on the same page. Profiling shows the cost is entirely in per-node Playwright locator round-trips during view-hierarchy bounds enrichment, not in the DOM/JS work.
Root cause (profiled)
jstack-sampling the running daemon during a capture shows the hot method is:
xyz.block.trailblaze.playwright.PlaywrightScreenState.enrichNodeWithLocatorBounds
enrichNodeWithLocatorBounds recurses the ~thousand-node view-hierarchy tree and, for every node, makes individual Playwright locator round-trips:
PlaywrightAriaSnapshot.resolveElementRef(page, ElementRef(descriptor, nth))
locator.count()
locator.boundingBox(...)
That's ~2–3 CDP round-trips × N nodes. It's budget-capped at ENRICHMENT_BUDGET_MS = 5_000L, so on heavy pages it burns the entire 5 s budget and still returns partial/incomplete bounds (the "enrichment budget exceeded — returning partial bounds" log).
Evidence (isolated Playwright benchmark, same page, ~8.3k DOM nodes)
A) per-node handle.boundingBox() × 800 nodes: 304 ms (0.4 ms/node, 1 round-trip)
B) batched getBoundingClientRect over ALL 8316 nodes: 5 ms (one page.evaluate)
→ 61× on boundingBox alone
The real code does ~3 round-trips per node over the full tree, which is why it saturates the 5 s budget. Batching the whole tree into one page.evaluate is ~1000× fewer round-trips.
For reference, the individual DOM ops are all cheap (measured on the same page): full-DOM walk + getComputedStyle on all 8386 elements ≈ 24 ms; computedRole/computedName reads ≈ 1 ms; ariaSnapshot ≈ 55 ms; screenshot ≈ 75 ms. None of these are the cost — it is exclusively the per-node locator RPC fan-out.
Suggested fix
Resolve all node bounds in a single batched page.evaluate (a getBoundingClientRect pass over the resolved elements) instead of per-node locator round-trips. The role/name/nth → element resolution already exists batched in BATCH_VIEWPORT_CHECK_JS (used by computeElementVisibility for the annotation overlay) — so the same bounds are effectively computed twice today (once batched for the overlay, once per-node for the tree). Reuse the batched result for the tree.
Keep the existing per-node locator.boundingBox() only as a fallback for nodes whose batched DOM rect is zero-size — that's the Compose-Web-Wasm case the locator path exists for (accessibility overlay elements have zero-size DOM rects but valid positions in Playwright's a11y tree). On such pages the fallback set is small, so the fan-out is bounded.
Expected impact: view-hierarchy enrichment on heavy pages drops from ~5 s (budget-capped, partial bounds) to ~single-digit ms (complete bounds).
Repro
Capture screen state (any path that enriches the view hierarchy with bounds) on a large, dynamic results page and profile the daemon thread — enrichNodeWithLocatorBounds dominates. A ~1000+ node tree is enough to hit the 5 s budget cap.
Summary
Capturing screen state on a DOM-heavy page (e.g. a flight-search results page, ~8k DOM nodes) is dramatically slower than it should be — on the order of ~9–12 s, vs ~1.5 s for the accessibility-only path on the same page. Profiling shows the cost is entirely in per-node Playwright locator round-trips during view-hierarchy bounds enrichment, not in the DOM/JS work.
Root cause (profiled)
jstack-sampling the running daemon during a capture shows the hot method is:enrichNodeWithLocatorBoundsrecurses the ~thousand-node view-hierarchy tree and, for every node, makes individual Playwright locator round-trips:That's ~2–3 CDP round-trips × N nodes. It's budget-capped at
ENRICHMENT_BUDGET_MS = 5_000L, so on heavy pages it burns the entire 5 s budget and still returns partial/incomplete bounds (the "enrichment budget exceeded — returning partial bounds" log).Evidence (isolated Playwright benchmark, same page, ~8.3k DOM nodes)
The real code does ~3 round-trips per node over the full tree, which is why it saturates the 5 s budget. Batching the whole tree into one
page.evaluateis ~1000× fewer round-trips.For reference, the individual DOM ops are all cheap (measured on the same page): full-DOM walk +
getComputedStyleon all 8386 elements ≈ 24 ms;computedRole/computedNamereads ≈ 1 ms;ariaSnapshot≈ 55 ms;screenshot≈ 75 ms. None of these are the cost — it is exclusively the per-node locator RPC fan-out.Suggested fix
Resolve all node bounds in a single batched
page.evaluate(agetBoundingClientRectpass over the resolved elements) instead of per-node locator round-trips. The role/name/nth → element resolution already exists batched inBATCH_VIEWPORT_CHECK_JS(used bycomputeElementVisibilityfor the annotation overlay) — so the same bounds are effectively computed twice today (once batched for the overlay, once per-node for the tree). Reuse the batched result for the tree.Keep the existing per-node
locator.boundingBox()only as a fallback for nodes whose batched DOM rect is zero-size — that's the Compose-Web-Wasm case the locator path exists for (accessibility overlay elements have zero-size DOM rects but valid positions in Playwright's a11y tree). On such pages the fallback set is small, so the fan-out is bounded.Expected impact: view-hierarchy enrichment on heavy pages drops from ~5 s (budget-capped, partial bounds) to ~single-digit ms (complete bounds).
Repro
Capture screen state (any path that enriches the view hierarchy with bounds) on a large, dynamic results page and profile the daemon thread —
enrichNodeWithLocatorBoundsdominates. A ~1000+ node tree is enough to hit the 5 s budget cap.