Skip to content

perf(core): acquire snapshots concurrently#2011

Merged
Nikhil (shadowfax92) merged 11 commits into
mainfrom
feat/snapshot-concurrent-acquisition
Jul 23, 2026
Merged

perf(core): acquire snapshots concurrently#2011
Nikhil (shadowfax92) merged 11 commits into
mainfrom
feat/snapshot-concurrent-acquisition

Conversation

@shadowfax92

Copy link
Copy Markdown
Contributor

Summary

  • overlap AX, cursor, validation, and sibling-frame snapshot acquisition under one capture-local eight-request CDP budget
  • batch cursor handle collection and main-world resolution while preserving page attributes and cancellation cleanup
  • retain deterministic reverse iframe assembly/ref allocation, retries, rollback, and tabs.new behavior, with stage timing

Design decisions

  • reuse the existing Tokio runtime; each permit covers one pending CDP request only, with no custom pool or global CDP scheduler
  • keep acquisition immutable/concurrent and rendering/ref mutation sequential in the historical reverse stitch order
  • use target-session default worlds for root/OOPIF frames and resolved Document wrappers for same-process frames; inherit nested target sessions and post-validate child loaders

Test plan

  • cd packages/browseros-agent && cargo fmt --all --check
  • cd packages/browseros-agent && cargo clippy --workspace --all-targets -- -D warnings
  • cd packages/browseros-agent && cargo test --workspace
  • live Chrome exact cursor-asset check for page-assigned onclick in same-process iframe and OOPIF

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished Nikhil (@shadowfax92)'s task in 4m 22s —— View job


Review: concurrent snapshot acquisition

Went through observer/acquisition.rs (new, 2133 lines), the observer.rs orchestration changes, frames.rs, and the cursor-augment.js marker-based rework. This is a large but well-structured refactor — solid design overall.

What's good

  • The SnapshotBudget semaphore model (observer/acquisition.rs:100-132) is sound: a permit is held only for the duration of a single pending CDP request/response and dropped immediately after, so there's no scenario where a future holds a permit while blocked waiting on another future that itself needs a permit — no deadlock risk from the shared 8-request cap.
  • Reverse iframe assembly order is correctly preserved: acquire_child_frames fans out unordered via FuturesUnordered but re-sorts by stitch_index before returning (observer/acquisition.rs:216-224), and the caller in observer.rs iterates .rev() to restore the historical ref-numbering order. This is exactly the kind of thing that's easy to get subtly wrong in a concurrency refactor, and it's handled carefully with a test (observer_acquires_siblings_out_of_order_but_assembles_reverse_stitch_order).
  • Cursor marker cleanup on cancellation (CursorCleanup's Drop impl, observer/acquisition.rs:485-517) correctly detaches a cleanup task via tokio::runtime::Handle::try_current() so task.abort() mid-scan doesn't leak the object group or leave a marker attribute on the live page. Covered by cancelling_cursor_resolution_still_cleans_marker_and_object_group and cancelling_document_resolution_still_releases_its_object_group.
  • The find_cursor_hits batching (single getProperties call instead of one round-trip per candidate) plus the sparse-marker/compact-candidate index reconciliation is non-trivial but well tested against out-of-order describeNode completions and vanished candidates.
  • frames.rs's new same_process_sessions cache correctly inherits the nearest parent's target session (so a same-process child under an OOPIF still evaluates in the OOPIF's session), and is cleaned up alongside oopif_sessions on session detach.
  • Test coverage is unusually thorough for a concurrency change — explicit gated/interleaved connections to force overlap, verify budget caps (cursor_describe_requests_never_exceed_capture_budget), and check cancellation cleanup paths.

Minor findings (non-blocking)

  1. Trace outcome mislabels expected cycle-skips as "partial". In Observer::acquire_child_frames (observer/acquisition.rs:225-231):

    let outcome = if acquired.len() == stitches.len()
        && acquired.iter().all(|child| child.result.is_ok())
    { "success" } else { "partial" };

    A child frame that's legitimately skipped because it's already in visited (cycle detection) also makes acquired.len() < stitches.len(), so a normal, expected skip gets traced as "partial" alongside genuine failures. This is just an observability nit (the snapshot_stage=sibling_acquisition trace could be misread as a partial failure when nothing actually went wrong), not a functional bug.
    Fix this →

  2. js_protocol.json's new getProperties/PropertyDescriptor schema entries appear unused. acquisition.rs calls Runtime.getProperties via budget.send::<GetPropertiesResult>(...) using its own hand-rolled GetPropertiesResult/PropertyDescriptor structs (observer/acquisition.rs:410-419), not the codegen'd bindings the protocol JSON change would produce in browseros-cdp. A repo-wide search turns up no other caller of the generated get_properties binding. Worth confirming this schema addition is intentional (e.g. for protocol-completeness/future typed use) rather than a leftover from an earlier approach — harmless either way, just extra surface to maintain.

Not verified

  • I did static review only; I didn't run cargo fmt/clippy/cargo test locally (sandboxed environment). CI shows "Code Quality" passed and "Tests" is still in progress as of this review — worth confirming it goes green before merge.
  • Didn't independently verify the live-Chrome cursor-asset behavior mentioned in the test plan (same-process iframe / OOPIF onclick) — that's inherently something only a live browser run can confirm.

Nothing here blocks merging; both findings are low-severity and non-functional.

@shadowfax92

Copy link
Copy Markdown
Contributor Author

DeepScan’s UNUSED_EXPR finding was a false positive for the cursor asset: CDP Runtime.callFunctionOn consumes the file as an uninvoked functionDeclaration. Added the documented rule-specific suppression plus a purpose comment in e226a2a; the 16 acquisition tests, formatting, diff check, and Biome hook are green.

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Snapshot acquisition now overlaps accessibility, cursor, document-validation, and sibling-frame CDP work while preserving deterministic assembly.

  • Adds an eight-request capture-local CDP concurrency budget and stage timing.
  • Batches cursor candidate resolution with collision-safe markers and cancellation cleanup.
  • Caches inherited target sessions for nested same-process frames.
  • Extends the Runtime protocol and adds concurrency, retry, cleanup, and frame-depth tests.

Confidence Score: 2/5

The reconnect-sensitive frame-session cache and stale child-frame commit path must be corrected before this PR is safe to merge.

Prior-epoch sessions remain selectable after reconnect, and child-frame loader changes discard only ref identity while retaining AX data and cursor hits acquired from the replaced document.

packages/browseros-agent/crates/browseros-core/src/frames.rs, packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs

Important Files Changed

Filename Overview
packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs Introduces bounded concurrent CDP acquisition and cleanup, but child-loader changes still allow previously acquired child data to be committed.
packages/browseros-agent/crates/browseros-core/src/observer.rs Reworks capture orchestration to acquire immutable frame data concurrently and assemble snapshots deterministically.
packages/browseros-agent/crates/browseros-core/src/frames.rs Adds inherited-session caching for same-process frames, but the cache is not invalidated when the CDP connection epoch changes.
packages/browseros-agent/crates/browseros-core/src/assets/cursor-augment.js Parameterizes cursor markers and returns collision-aware candidate batches.
packages/browseros-agent/crates/browseros-cdp/protocol/js_protocol.json Adds the Runtime.getProperties command and its property descriptor shape for batched remote-object collection.

Sequence Diagram

sequenceDiagram
    participant Observer
    participant Budget as SnapshotBudget
    participant AX as Accessibility
    participant Runtime
    participant Frames as Child Frames
    Observer->>Budget: Begin capture
    par Acquire AX tree
        Budget->>AX: getFullAXTree
        AX-->>Observer: nodes
    and Scan cursor candidates
        Budget->>Runtime: evaluate/callFunctionOn
        Runtime-->>Observer: candidate handles
    and Validate document
        Budget->>Frames: getFrameTree
        Frames-->>Observer: loader identities
    end
    par Acquire sibling frames
        Observer->>Frames: acquire child A
    and
        Observer->>Frames: acquire child B
    end
    Observer->>Observer: Restore stitch order
    Observer->>Observer: Sequentially render and assign refs
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/browseros-agent/crates/browseros-core/src/frames.rs:97-102
**Stale sessions survive reconnects**

When CDP reconnects after a same-process frame session has been cached, this lookup returns the prior connection epoch's session because only `PageManager` clears its sessions. Resolving an existing frame ref then sends click, typing, scrolling, or annotation commands through an invalid session and fails with an unknown or detached-session protocol error.

### Issue 2 of 2
packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs:294-296
**Changed child snapshots remain committable**

When a child frame navigates after its AX data is acquired, revalidation reports the changed loader but only clears `document_id`; the old nodes and cursor hits are still returned and committed because capture-level stability checks cover only the main frame. The resulting snapshot exposes stale child content and actionable refs from the replaced document.

Reviews (1): Last reviewed commit: "fix: suppress intentional cursor asset e..." | Re-trigger Greptile

Comment thread packages/browseros-agent/crates/browseros-core/src/frames.rs
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Concurrent snapshot acquisition is introduced while preserving sequential rendering semantics.

  • Adds an eight-request capture-local CDP budget and overlaps accessibility-tree, cursor, document-validation, and sibling-frame acquisition.
  • Batches cursor candidate resolution with collision-safe DOM markers, object-group cleanup, and same-process frame document targeting.
  • Caches inherited sessions for same-process frames and restores deterministic reverse iframe assembly and ref allocation.
  • Extends the Runtime protocol definitions and adds concurrency, retry, cleanup, frame-session, and ordering tests.

Confidence Score: 5/5

The pull request appears safe to merge, with concurrency bounded locally and historical snapshot assembly behavior preserved.

The changed acquisition paths restore deterministic stitch order before sequential ref mutation, isolate cursor markers per capture, clean remote objects on success and cancellation, and retain snapshot retry and child-failure rollback behavior.

Important Files Changed

Filename Overview
packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs Adds bounded concurrent frame, cursor, and document acquisition with deterministic result ordering and cancellation cleanup.
packages/browseros-agent/crates/browseros-core/src/observer.rs Integrates concurrent acquisition into capture retries while retaining sequential iframe assembly, ref rollback, and stable ordering.
packages/browseros-agent/crates/browseros-core/src/frames.rs Adds inherited session tracking for same-process frames nested beneath page or OOPIF targets.
packages/browseros-agent/crates/browseros-core/src/assets/cursor-augment.js Changes cursor scanning to use capture-specific marker attributes and structured collision results.
packages/browseros-agent/crates/browseros-cdp/protocol/js_protocol.json Adds the Runtime property-descriptor and getProperties definitions required for batched cursor handle collection.

Sequence Diagram

sequenceDiagram
    participant Observer
    participant Budget as SnapshotBudget
    participant Frames as FrameRegistry
    participant CDP
    participant Renderer

    Observer->>Budget: Read initial frame tree
    par Acquire AX tree
        Budget->>CDP: Accessibility.getFullAXTree
    and Acquire cursor candidates
        Budget->>CDP: Runtime.evaluate/callFunctionOn
        Budget->>CDP: Runtime.getProperties
        Budget->>CDP: DOM.describeNode (bounded)
    and Validate document
        Budget->>CDP: Page.getFrameTree
    end
    Observer->>Renderer: Render immutable frame inputs
    Renderer-->>Observer: Iframe stitches
    par Acquire sibling frames
        Observer->>Frames: Resolve inherited target sessions
        Frames->>CDP: Acquire child frame inputs
    end
    Observer->>Renderer: Assemble children in reverse stitch order
    Observer->>Budget: Read final frame tree
    Observer-->>Observer: Commit snapshot or retry
Loading

Reviews (2): Last reviewed commit: "fix: suppress intentional cursor asset e..." | Re-trigger Greptile

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Addressed both non-blocking Claude findings in 71871c9: expected visited-frame cycle skips no longer label sibling acquisition as partial, and cursor acquisition now deserializes Runtime.getProperties through the generated protocol result type. Formatting, workspace Clippy with warnings denied, and the full workspace test suite are green.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Tests passed — 1694/1698

Suite Passed Failed Skipped
agent 350/350 0 0
build 34/34 0 0
claw-app 214/214 0 0
⚠️ claw-mcp 0/0 0 0
⚠️ claw-onboard 0/0 0 0
⚠️ claw-server-rust-quality 0/0 0 0
⚠️ claw-server-rust 0/0 0 0
server-agent 314/314 0 0
server-api 171/171 0 0
server-browser 10/10 0 0
server-integration 10/10 0 0
server-lib 299/300 0 1
server-root 38/41 0 3
server-tools 254/254 0 0

View workflow run

@shadowfax92
Nikhil (shadowfax92) merged commit 161d39d into main Jul 23, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant