block / block/trailblaze

web_snapshot O(tree-size) slow: enrichNodeWithLocatorBounds does per-node locator round-trips (~5s budget-cap on heavy pages)

Open
#199 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
Kotlin
Stars
310
Forks
28
Avg merge
7h 45m
Merged PRs (30d)
8

Description

## 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.

Contributor guide

Open the contributing guide

Research direction

Start at PlaywrightScreenState.enrichNodeWithLocatorBounds and inspect the existing BATCH_VIEWPORT_CHECK_JS path used by computeElementVisibility. Compare the batched DOM bounds with the current per-node locator calls, preserving the fallback for zero-size rects. Reproduce on a DOM-heavy page and confirm enrichment completes within the budget with complete bounds.

Written by the indexing model from the issue text.

Assessment

Tech stack
kotlin
Domain
performance, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.