Every checkbox, radio button and switch in a macOS snapshot pays two cross-process AX round-trips for one attribute.
walk reads AXValue as a string for AxNode.value:
// crates/glass-a11y-macos/src/reader.rs:477
let value = ffi::attribute_string(el, attr::VALUE);
and gather_states, called two lines later, reads the same attribute as an integer to decide checked:
// crates/glass-a11y-macos/src/reader.rs:622
let (checkable, checked) = if mapping::role_carries_checked(role) {
let value = ffi::attribute_i64(el, attr::VALUE);
Both go through ffi::copy_attribute (ffi.rs:99) — attribute_string at ffi.rs:222, attribute_i64 at ffi.rs:256 — so each is its own AXUIElementCopyAttributeValue call. attribute_i64 then does value.downcast_ref::<CFNumber>(), which means one copy_attribute plus a branch on CFString/CFNumber would serve both callers.
Only the three roles mapping::role_carries_checked admits pay it, so this is not every node — but it is every interactive toggle, which is a large share of the controls an agent actually drives.
Worth fixing because this crate already treats a per-node round-trip as a cost worth designing around: mapping::subrole_matters exists solely to avoid one, and its doc comment carries a measurement to justify the gate ("48 of 359 nodes gated, walk wall-clock 42.8ms to 45.4ms"). A duplicate read of the same attribute is the same cost with no gate in front of it.
Care needed on two points. The two reads have different failure semantics today — attribute_string is error-blind while the checked path deliberately logs an unreadable value (reader.rs:624-632, the #170 invariant) — so a shared read must keep the "no readable numeric value claims neither checked nor unchecked" behaviour rather than folding a missing CFNumber into a successful string read. And AXValue on a checkbox is a CFNumber, so the string read returns None for it today; a shared read must not start populating AxNode.value for toggles as a side effect.
Surfaced by the pre-merge review panel on #498 and verified by reading; out of scope there.
Every checkbox, radio button and switch in a macOS snapshot pays two cross-process AX round-trips for one attribute.
walkreadsAXValueas a string forAxNode.value:and
gather_states, called two lines later, reads the same attribute as an integer to decidechecked:Both go through
ffi::copy_attribute(ffi.rs:99) —attribute_stringatffi.rs:222,attribute_i64atffi.rs:256— so each is its ownAXUIElementCopyAttributeValuecall.attribute_i64then doesvalue.downcast_ref::<CFNumber>(), which means onecopy_attributeplus a branch onCFString/CFNumberwould serve both callers.Only the three roles
mapping::role_carries_checkedadmits pay it, so this is not every node — but it is every interactive toggle, which is a large share of the controls an agent actually drives.Worth fixing because this crate already treats a per-node round-trip as a cost worth designing around:
mapping::subrole_mattersexists solely to avoid one, and its doc comment carries a measurement to justify the gate ("48 of 359 nodes gated, walk wall-clock 42.8ms to 45.4ms"). A duplicate read of the same attribute is the same cost with no gate in front of it.Care needed on two points. The two reads have different failure semantics today —
attribute_stringis error-blind while thecheckedpath deliberately logs an unreadable value (reader.rs:624-632, the #170 invariant) — so a shared read must keep the "no readable numeric value claims neither checked nor unchecked" behaviour rather than folding a missingCFNumberinto a successful string read. AndAXValueon a checkbox is aCFNumber, so the string read returnsNonefor it today; a shared read must not start populatingAxNode.valuefor toggles as a side effect.Surfaced by the pre-merge review panel on #498 and verified by reading; out of scope there.