Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions libs/cua-driver/rust/crates/platform-linux/src/atspi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,27 @@ pub fn walk_tree_bounded(
max_elements: Option<usize>,
max_depth: Option<usize>,
) -> AtspiTreeResult {
// Native AT-SPI (most complete).
if let Ok(Some((raw_md, nodes))) = native::walk_tree_bounded(pid, max_elements, max_depth) {
if !raw_md.is_empty() {
let md = if let Some(q) = query { filter_tree(&raw_md, q) } else { raw_md };
return AtspiTreeResult { tree_markdown: md, nodes };
// Native AT-SPI (most complete). On a COLD launch the Qt6 (and some GTK)
// AT-SPI bridge registers lazily — the first walk against a freshly
// launched app can come back with just the root window (element_count=1,
// no children) because `org.a11y.atspi.Registry` hasn't finished
// enumerating the app's tree yet. Retry a few times with a short backoff
// while the tree is suspiciously root-only, so the first get_window_state
// after launch returns the real tree instead of an empty one. See #1927.
const MAX_ATTEMPTS: usize = 4;
for attempt in 0..MAX_ATTEMPTS {
if let Ok(Some((raw_md, nodes))) = native::walk_tree_bounded(pid, max_elements, max_depth) {
// `nodes.len() <= 1` == only the root window resolved: the
// cold-registry symptom. Accept any real tree immediately; only
// keep waiting on the degenerate case, and accept it anyway on the
// final attempt rather than discarding a (minimal) valid result.
if !raw_md.is_empty() && (nodes.len() > 1 || attempt == MAX_ATTEMPTS - 1) {
let md = if let Some(q) = query { filter_tree(&raw_md, q) } else { raw_md };
return AtspiTreeResult { tree_markdown: md, nodes };
}
}
if attempt < MAX_ATTEMPTS - 1 {
std::thread::sleep(std::time::Duration::from_millis(150));
Comment on lines +67 to +79

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Retry heuristic should be cap-aware to avoid forced backoff on intentionally tiny trees.

Line 73 treats nodes.len() <= 1 as a cold-start symptom unconditionally. But callers can intentionally request tiny traversals (max_elements=1 / max_depth=1), where a single-node result is expected; this path then always incurs 3 sleeps (~450ms) before returning.

Proposed fix
 pub fn walk_tree_bounded(
     pid: u32,
     xid: u64,
     query: Option<&str>,
     max_elements: Option<usize>,
     max_depth: Option<usize>,
 ) -> AtspiTreeResult {
     const MAX_ATTEMPTS: usize = 4;
+    const BACKOFF_MS: u64 = 150;
+    let retry_root_only = max_elements.map_or(true, |m| m > 1)
+        && max_depth.map_or(true, |d| d > 1);
+
     for attempt in 0..MAX_ATTEMPTS {
         if let Ok(Some((raw_md, nodes))) = native::walk_tree_bounded(pid, max_elements, max_depth) {
-            if !raw_md.is_empty() && (nodes.len() > 1 || attempt == MAX_ATTEMPTS - 1) {
+            let root_only = nodes.len() <= 1;
+            let should_retry = retry_root_only && root_only && attempt < MAX_ATTEMPTS - 1;
+            if !raw_md.is_empty() && !should_retry {
                 let md = if let Some(q) = query { filter_tree(&raw_md, q) } else { raw_md };
                 return AtspiTreeResult { tree_markdown: md, nodes };
             }
         }
         if attempt < MAX_ATTEMPTS - 1 {
-            std::thread::sleep(std::time::Duration::from_millis(150));
+            std::thread::sleep(std::time::Duration::from_millis(BACKOFF_MS));
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/rust/crates/platform-linux/src/atspi/mod.rs` around lines 67
- 79, The retry heuristic in the walk_tree_bounded loop treats nodes.len() <= 1
as a cold-start symptom unconditionally, causing unnecessary sleep delays even
when callers intentionally request tiny traversals via max_elements=1 or
max_depth=1. Modify the condition on line 73 that checks nodes.len() > 1 to also
account for the max_elements and max_depth parameters, so that single-node
results are accepted immediately when they match the caller's intentional
constraints, rather than forcing the retry loop to completion. Only treat
minimal results as cold-start symptoms when they're genuinely unexpected given
the traversal caps.

}
}

Expand Down
Loading