@@ -8,7 +8,7 @@ extension RunnerTests {
88 /// depth-capped Bluesky-class tree resolves in 1-3 chained requests (~100-300ms
99 /// each); the bound exists so a pathological tree cannot stack requests past
1010 /// the capture-plan deadline, which is also enforced per call.
11- private static let privateAXDeepExtensionCallLimit = 8
11+ static let privateAXDeepExtensionCallLimit = 8
1212
1313 /// Upper bound on per-element custom-action reads per capture. Each is its own
1414 /// AX round trip (~100ms on an idle simulator), so this caps the opt-in cost
@@ -67,6 +67,65 @@ extension RunnerTests {
6767 read : read , candidates : candidates , truncated : truncated , blocked : blocked )
6868 }
6969
70+ struct PrivateAXLadderOutcome {
71+ let response : [ String : Any ]
72+ let effectiveDepth : Int
73+ let deadlineSpent : Bool
74+ let lastError : String
75+
76+ var succeeded : Bool { response [ " ok " ] as? Bool == true }
77+ }
78+
79+ /// Walks the ladder rungs until one capture succeeds. The first rung always runs (the plan
80+ /// gated entry on its own budget); later rungs stop when the capture-plan deadline is spent so
81+ /// ladder retries can never stack past the runner's main-thread watchdog (#1105).
82+ static func privateAXLadderCapture (
83+ attemptDepths : [ Int ] ,
84+ deadline : Date ,
85+ capture : ( Int ) -> [ String : Any ]
86+ ) -> PrivateAXLadderOutcome {
87+ var response : [ String : Any ] = [ : ]
88+ var effectiveDepth = attemptDepths . first ?? 0
89+ var lastError = " unknown private AX snapshot failure "
90+ for depth in attemptDepths {
91+ if depth != attemptDepths . first , Date ( ) >= deadline {
92+ NSLog ( " AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_BUDGET_EXHAUSTED depth=%ld " , depth )
93+ return PrivateAXLadderOutcome (
94+ response : response , effectiveDepth : effectiveDepth , deadlineSpent : true ,
95+ lastError : lastError )
96+ }
97+ response = capture ( depth )
98+ if response [ " ok " ] as? Bool == true {
99+ effectiveDepth = depth
100+ break
101+ }
102+ lastError = response [ " error " ] as? String ?? lastError
103+ NSLog (
104+ " AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_DEPTH_RETRY depth=%ld error=%@ " ,
105+ depth ,
106+ lastError
107+ )
108+ }
109+ return PrivateAXLadderOutcome (
110+ response : response , effectiveDepth : effectiveDepth , deadlineSpent : false , lastError : lastError )
111+ }
112+
113+ /// Only a capture that actually descended records memory: a first-rung success on a
114+ /// remembered depth deliberately does NOT refresh the TTL, so expiry re-probes the full
115+ /// requested depth once per window instead of capping this screen class forever.
116+ func recordPrivateAXAcceptedDepth (
117+ exactDepthRequested : Bool ,
118+ effectiveDepth : Int ,
119+ attemptDepths : [ Int ]
120+ ) {
121+ guard ! exactDepthRequested , effectiveDepth != attemptDepths . first else { return }
122+ rememberPrivateAXAcceptedDepth (
123+ bundleId : currentBundleId ,
124+ processIdentifier : currentAppProcessIdentifier ,
125+ depth : effectiveDepth
126+ )
127+ }
128+
70129 func rememberPrivateAXAcceptedDepth ( bundleId : String ? , processIdentifier : Int ? , depth : Int ) {
71130 // No PID means no way to notice a relaunch later; record nothing rather than risk serving
72131 // a stale shallow rung to a fresh process.
@@ -125,54 +184,32 @@ extension RunnerTests {
125184 requestedDepth : requestedDepth ,
126185 rememberedDepth : rememberedDepth
127186 )
128- var response : [ String : Any ] = [ : ]
129- var effectiveDepth = requestedDepth
130- var lastError = " unknown private AX snapshot failure "
131- for depth in attemptDepths {
132- // The first rung always runs (the plan gated entry on its own budget); later rungs
133- // stop when the capture-plan deadline is spent so ladder retries can never stack
134- // past the runner's main-thread watchdog (#1105).
135- if depth != attemptDepths . first , Date ( ) >= deadline {
136- NSLog ( " AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_BUDGET_EXHAUSTED depth=%ld " , depth )
137- break
138- }
139- // Declared residue (#1797): the bridge caps the tree at 5000 nodes while serializing,
140- // BEFORE either projection exists, so a raw capture of a huge screen is bounded rather
141- // than failing the way the tree backend's own raw cap does. The cap is disclosed as
142- // `truncated`, and it applied to the acquired tree before this projection split too.
143- response = RunnerAXSnapshotBridge . snapshotTree (
187+ // Declared residue (#1797): the bridge caps the tree at 5000 nodes while serializing,
188+ // BEFORE either projection exists, so a raw capture of a huge screen is bounded rather
189+ // than failing the way the tree backend's own raw cap does. The cap is disclosed as
190+ // `truncated`, and it applied to the acquired tree before this projection split too.
191+ let ladder = Self . privateAXLadderCapture ( attemptDepths : attemptDepths , deadline : deadline ) {
192+ depth in
193+ RunnerAXSnapshotBridge . snapshotTree (
144194 for : app ,
145195 maxDepth : depth ,
146196 maxNodes : Self . privateAXSnapshotMaxNodes ,
147197 deepExtensionCallLimit : exactDepthRequested ? 0 : Self . privateAXDeepExtensionCallLimit ,
148198 customActionLimit : hint . customActions ? Self . privateAXCustomActionLimit : 0 ,
149199 deadline : deadline
150200 )
151- if response [ " ok " ] as? Bool == true {
152- effectiveDepth = depth
153- break
154- }
155- lastError = response [ " error " ] as? String ?? lastError
156- NSLog (
157- " AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_DEPTH_RETRY depth=%ld error=%@ " ,
158- depth ,
159- lastError
160- )
161201 }
162- guard response [ " ok " ] as? Bool == true else {
163- NSLog ( " AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=%@ " , lastError )
202+ let response = ladder . response
203+ let effectiveDepth = ladder . effectiveDepth
204+ guard ladder . succeeded else {
205+ NSLog ( " AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=%@ " , ladder . lastError )
164206 return nil
165207 }
166- // Only a capture that actually descended records memory: a first-rung success on a
167- // remembered depth deliberately does NOT refresh the TTL, so expiry re-probes the full
168- // requested depth once per window instead of capping this screen class forever.
169- if ! exactDepthRequested , effectiveDepth != attemptDepths . first {
170- rememberPrivateAXAcceptedDepth (
171- bundleId : currentBundleId ,
172- processIdentifier : currentAppProcessIdentifier ,
173- depth : effectiveDepth
174- )
175- }
208+ recordPrivateAXAcceptedDepth (
209+ exactDepthRequested : exactDepthRequested ,
210+ effectiveDepth : effectiveDepth ,
211+ attemptDepths : attemptDepths
212+ )
176213 guard let root = response [ " root " ] as? [ String : Any ] else {
177214 NSLog ( " AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=missing root " )
178215 return nil
0 commit comments