Skip to content

perf(highlight): cut tab init from ~20s to ~5s on heavy pages#63

Merged
softpudding merged 4 commits into
feat/ob-routines-skillfrom
perf/tab-init-highlight-cache
Apr 18, 2026
Merged

perf(highlight): cut tab init from ~20s to ~5s on heavy pages#63
softpudding merged 4 commits into
feat/ob-routines-skillfrom
perf/tab-init-highlight-cache

Conversation

@softpudding

Copy link
Copy Markdown
Owner

Summary

Tab init's two heaviest phases on dense pages (finviz.com/screener) were the in-page DOM scan (~10s) and the background pagination (~4s). Both share the same shape — per-element loops that re-do work the previous step already paid for. This PR cuts them with caching and a spatial index, no algorithmic risk to correctness.

Phase Baseline After Δ
In-page scan (finviz, ~349 candidates) 6537 ms (10 s before any caching) 585 ms −91%
Pagination 4106 ms 300 ms −93%
End-to-end tab init 17.8 s 5.0 s −72%

On the 16 deterministic mock sites, aggregate latency drops 12–47%, biggest wins on the heavier pages (bluebook −47%, techforum −31%).

Commits

  • `96efe39` perf(highlight): cache layout reads + spatial-index pagination
  • `3299e5f` perf(highlight): cache resolve-phase classifiers + emit perf breakdown
  • `01af8f6` set extension auto reload timeout to 40s

What changed

`extension/src/commands/highlight-detection.injected.js`

  • `withScanLayoutCache(fn)` monkey-patches `Element.prototype.getBoundingClientRect`, `SVGGraphicsElement.prototype.getBoundingClientRect`, `Document.prototype.elementsFromPoint`, and `window.getComputedStyle` for the duration of one scan. Backed by per-scan WeakMap/Map caches; restored in `finally`.
  • Per-scan WeakMap memoization for resolve-phase classifiers that are pure functions of element + DOM state: `getSemanticClickableSignal`, `isClickableCandidate`, `getBaseClickableSignal`, `hasExplicitClickableAncestor`, `getElementTextForDetection`, `getElementSearchText`. The resolve phase was 6.1s of the 6.5s scan; this drops it to ~50ms.
  • Tag pre-filter for inert elements (script/style/meta/...) before any layout read.
  • `_perf` field on the response payload with `scan_ms`, `scan_stats`, `scan_times`, `pagination_ms`, `screenshot_ms`, `consistency_ms` — for tooling/profiling without parsing console output.

`extension/src/utils/collision-detection.ts`

  • `SelectedSpatialIndex` (96 px grid) keyed on `union(bbox, labelBBox)` of placed elements.
  • `isPlacementFeasible` now iterates only nearby placed elements via `nearbySelectedFor`, which queries by `inflate(union(candidate.bbox, candidate.labelBBox), CLEARANCE)` — covering all four collision tests (label-label, bbox-bbox, candidate-label-vs-selected-bbox, candidate-bbox-vs-selected-label).
  • `chooseLeastBlockingPlacement` uses an "influence rect" to skip re-evaluating spatially-far future candidates whose feasibility a hypothetical placement cannot affect.

`extension/src/background/index.ts`: thread `_perf` into the response.

`extension/vite.config.ts`: dev-reload wait bumped 10s → 40s to cover MV3 service-worker keepalive cycles.

Why caching is safe

The scan runs in a single synchronous `Runtime.evaluate` call. No `await`, no `setTimeout`, no MutationObserver callbacks. Page JS cannot run concurrently. Layout/style cannot mutate mid-scan, so cached rect/style/EFP values are guaranteed fresh. Originals are restored in `finally` regardless of throw.

Correctness verification

  • 181 / 181 extension unit tests pass (incl. `highlight-placement`, `highlight-integration`, `hit-test-visibility`).
  • Strict integration check (selector + type + labelPosition + bbox + element ORDER) passes on all 16 deterministic mock sites — same elements, same labels, same visual order.
  • Real finviz returns identical 336 elements / 6 pages / 138 on page 1 across baseline and optimized runs.
  • Codex code review consulted on both the plan and the diff; concerns surfaced (NaN guard in `forEachCell`, restoring the original `elementsFromPoint` to the prototype rather than to a bound wrapper) were addressed before commit.

Test plan

  • `bun test` in `extension/` — 181 tests pass
  • Open the dev extension, init a tab to https://finviz.com/screener.ashx, confirm `_perf` shows `scan_ms < 1500`, `pagination_ms < 500`, total init < 7s
  • Repeat on at least 3 mock sites under `eval/` (finviz_mock, bluebook, techforum) — confirm element counts and visual order match baseline

Stack

This is PR 3 of 3:

  1. `feat/image-input-and-file-upload` → `main` (feat: image input for prompts + upload_file browser action #61)
  2. `feat/ob-routines-skill` → `feat/image-input-and-file-upload` (skill(ob-routines): record → compile → replay browser routines #62)
  3. This PR → `feat/ob-routines-skill`

After #61 and #62 merge to main, retarget this PR to `main`.

🤖 Generated with Claude Code

softpudding and others added 4 commits April 18, 2026 10:22
Tab init's two heaviest phases share the same shape: per-element loops that
re-do work the previous step already paid for. Cut both.

Scanner (highlight-detection.injected.js): wrap collectHighlightCandidates in
withScanLayoutCache, which monkey-patches Element.prototype.getBoundingClientRect,
SVGGraphicsElement.prototype.getBoundingClientRect, window.getComputedStyle, and
Document.prototype.elementsFromPoint with per-scan WeakMap/Map caches. The scan
runs in one synchronous Runtime.evaluate, so layout cannot change mid-task and
caching is safe; originals are restored in finally. Also skip inert tags
(script/style/meta/...) before the first layout read.

Pagination (collision-detection.ts): SelectedSpatialIndex (96px grid) keyed on
union(bbox, labelBBox) of placed elements. isPlacementFeasible now iterates
only nearby placed elements via nearbySelectedFor, which queries by inflate
(union(candidate.bbox, candidate.labelBBox), CLEARANCE) — covering all four
collision tests. chooseLeastBlockingPlacement also uses an "influence rect"
to skip re-evaluating spatially-far future candidates when a hypothetical
placement cannot affect them.

Measured (best run, fresh tab init):
- finviz.com (349 elements):   17.8s -> 13.7s  (-23%)
- bluebook mock (50):           6.3s -> 5.4s   (-14%)
- techforum mock (34):          4.3s -> 3.9s   (-11%)
- 16 mock sites aggregate:    -4% to -14%

Correctness:
- 181/181 extension unit tests pass.
- Strict integration check (selector + type + labelPosition + bbox + element
  ORDER) passes on all 16 deterministic mock sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pagination win revealed that scan-phase resolve was the new bottleneck:
on finviz the resolve phase alone was 6.1s of a 6.5s scan. Per candidate,
resolveClickableCandidate walks up to 5 ancestors, each calling
isClickableCandidate, which calls hasExplicitClickableAncestor that walks
ALL ancestors back to body, calling getSemanticClickableSignal at each.
For deep DOM (finviz tables) the same elements were classified dozens of
times per scan.

Add per-scan WeakMap memoization (cleared by withScanLayoutCache) for the
classifiers that are pure functions of element + DOM state:
- getSemanticClickableSignal
- isClickableCandidate
- getBaseClickableSignal
- hasExplicitClickableAncestor
- getElementTextForDetection (textContent walk)
- getElementSearchText

Also add scan_stats / scan_times to the response payload so harness/tooling
can attribute time per phase without parsing console output.

Measured (best run, finviz.com/screener.ashx, ~349 candidates):
- in-page scan:    6537ms -> 585ms     (-91%, ~11x)
- pagination:       397ms -> 300ms     (already optimized in prior commit)
- end-to-end:    17787ms -> 4975ms     (-72%, ~3.6x)

Resolve-phase breakdown after caching: 6121ms -> 51ms.

Correctness: 181/181 unit tests pass. Strict integration check (selector,
type, labelPosition, bbox, element ORDER) passes on all 16 deterministic
mock sites — same elements, same labels, same order. finviz_real returns
identical 336/6/138 element/page/page1 counts.

Caching is safe because the scan runs in one synchronous Runtime.evaluate
call and these classifiers depend only on DOM state that cannot mutate
during the scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@softpudding softpudding force-pushed the perf/tab-init-highlight-cache branch from 01af8f6 to 6aac696 Compare April 18, 2026 02:22
@softpudding softpudding force-pushed the feat/ob-routines-skill branch from 45a103e to a9c0c7c Compare April 18, 2026 02:22
@softpudding softpudding merged commit 2d2876c into feat/ob-routines-skill Apr 18, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant