Blolabel
← Notes
analysis 7 min read

A moving W3C pointer chain costs 19 seconds on a real iPhone, and the identical drag costs 1.4

The specification-compliant path is the slow one, and the cost does not scale with the number of points in the chain. Here is the per-call table, the mechanism, and what it does to an agent's step budget.

A W3C pointer chain that moves costs a flat 19 seconds on a physical iPhone. The same one-finger drag sent to WebDriverAgent's native /wda/dragfromtoforduration endpoint costs 1.4 seconds, a factor of 13.6. It is dragging specifically: a tap through the same W3C /actions endpoint returns in 500 ms. The 19 seconds does not move with the shape of the chain either, measured at 19.2 s for one move step, 19.8 s for twelve, and 18.9 s with every pause removed.

Abstract technical illustration of two parallel signal paths of very different thickness carrying the same gesture, one dense and slow, the other thin and fast, over a dark grid

What was measured, and why it is the standard path that is slow

Every figure below was measured best of three on a physical iPhone 17 Pro Max running iOS 26.6, driven from macOS 26.5 through Appium's WebDriverAgent at tag v16.12.4, released 6 September 2026. They are recorded in FINDINGS.md section 2 of the phoneshell repository, published in the 15 September 2026 commit. Four WebDriverAgent releases have shipped since, v16.12.5 on 7 September through v16.12.8 on 11 September, and we have not re-measured against them.

The awkward part is which path is which. POST /session/:id/actions is the W3C WebDriver standard, so it is what a portable client emits: Selenium, WebdriverIO and Appium's own action builders all compose pointer chains. The fast route is a WebDriverAgent extension with no place in the specification, documented by Appium as mobile: dragFromToForDuration, whose duration argument is not the travel time at all. The docs define it as "how long the tap gesture at starting drag point should be before to start dragging", in the range 0.5 to 60 seconds. Appium's own documentation says of the neighbouring scroll extension that it "relies on several undocumented XCTest features, which might not always be reliable". So the trade is a standard interface at 19 seconds against a vendor-specific one at 1.4.

What each call costs on a real device

callendpointmeasuredneeds a sessionsource
foreground app infoGET /wda/activeAppInfo43 msnoFINDINGS s.2
screenshotGET /screenshot72 msnoFINDINGS s.2
accessibility tree, depth 20GET /source159 to 210 msnoFINDINGS s.2
tapPOST /session/:id/actions500 msyesFINDINGS s.2
swipePOST /wda/swipe1,000 msyesFINDINGS s.2
dragPOST /wda/dragfromtoforduration1,400 msyesFINDINGS s.2
scrollPOST /wda/scroll7,350 msyesFINDINGS s.2
dragPOST /session/:id/actions19,000 msyesFINDINGS s.2
same drag, 1 move stepPOST /session/:id/actions19.2 syesFINDINGS s.2
same drag, 12 move stepsPOST /session/:id/actions19.8 syesFINDINGS s.2
same drag, no pausesPOST /session/:id/actions18.9 syesFINDINGS s.2
picker wheel tap, round tripPOST /session/:id/actions550 msyesFINDINGS s.15

The read path is the cheap part and it does not need a session, which matters for a different reason: observing the phone through a session can disturb the phone. The write path is where the seconds are, and one call on it is worth 90 reads of the foreground app.

One gesture costs more than ninety reads, log scale iPhone 17 Pro Max, iOS 26.6, WebDriverAgent v16.12.4, best of three activeAppInfoscreenshotsource, depth 20 tap, W3C actionsswipe, native drag, nativescroll, native drag, W3C actions 43 ms72 ms210 ms 500 ms1,000 ms1,400 ms 7,350 ms19,000 ms 10 ms100 ms1 s10 s100 s round-trip wall clock, logarithmic
The same one-finger drag appears twice: 1,400 ms through the native endpoint and 19,000 ms through the W3C chain that produces the identical gesture.

Why the standard path is the expensive one

A W3C pointer chain is a sequence the client composes and the runner replays: pointerMove, pointerDown, pauses, a run of interpolated pointerMove steps, pointerUp. XCTest synthesises that as an event record and waits on it. The native endpoint asks XCTest for one canned gesture instead, the coordinate press-and-drag that XCUICoordinate exposes, and XCTest performs it as a unit. That is why the cost does not scale with the number of points: the 19 seconds is the synthesis and settle path, not the travel. A chain with one move step and a chain with twelve differ by 0.6 s, which is noise beside the constant.

Our routing rule follows the measurement exactly. In phoneshell/gestures.py, perform() checks whether the gesture is one finger with two points, and if so sends it to the native endpoint with press_ms mapped onto the endpoint's press-before-drag argument. Everything else, meaning genuinely multi-finger paths and multi-waypoint paths, goes through W3C and still costs about 20 seconds. Pinch is expensive and there is no way around it today.

Two routes to the same finger movement POST /session/:id/actions (W3C) pointerMove d=0 -> pointerDown -> pause 8 interpolated pointerMove steps pause -> pointerUp XCTest synthesises and settles the record 19.2 s at 1 step, 19.8 s at 12, 18.9 s with no pauses POST /wda/dragfromtoforduration fromX, fromY, toX, toY, duration duration = the press BEFORE the drag one XCUICoordinate press-and-drag XCTest performs one canned gesture 1.4 s, the same pixels moved same gesture the routing rule in phoneshell/gestures.py len(paths) == 1 andlen(points) == 2 native endpoint1.4 s multi-finger or waypointsW3C, about 20 s Both write paths require a session. The read path (source, screenshot, activeAppInfo, window size) is registered withoutSession upstream, so observing the phone costs 43 to 210 ms and never creates one.
The specification-compliant route is the slow one. The fast one is a WebDriverAgent extension, and its duration argument means the press before the drag, not the travel.

What this changes if you are costing an agent's step budget

Device latency is usually not the lever. Measured across the tasks that passed on our benchmark, a turn takes 4.1 to 6.8 seconds end to end and the phone accounts for about one second of that; the rest is the model round trip. OSWorld-Human (arXiv:2506.16042) reports the same shape on desktop, where large model calls for planning and reflection dominate latency and the best agents still take 1.4 to 2.7 times more steps than necessary. So the ordinary optimisation is to ask the model fewer times, not to shave milliseconds off the device.

A 19-second gesture is the exception that breaks that rule. It is three to four turns' worth of wall clock inside a single action, and a task with a 240-second budget that needs six drags has spent almost half of it before the model has thought about anything. Two rules follow, and they are independent.

The turn is the unit of cost, so the fix is fewer turns 4.1 to 6.8 s per turn measured end to end, of which the phone is about 1 s observe, then act: six round trips observe tap Settings observe tap General observe tap About 6 turns, about 30 s, and every tree read stays in the conversation and is re-read on every later turn one compound call: one round trip [{"open_app": "Settings"}, {"tap": "General"}, {"tap": "About"}] engine verifies each step returns at the first divergence Same three-turn shape, unchanged screens returned as one line instead of a full tree: $0.1011 -> $0.0345 cache-write 18,584 -> 1,278 tokens A failed run costs the same as a successful one, so preventing avoidable failures is the cheapest optimisation available.
Device milliseconds are the small term. The tool shape that forces two turns per action is the large one, and a 19-second gesture is the case where both bite at once.

The two interact in one place worth naming. Picker wheels are turned by tapping, 550 ms per tap round trip, and a drag carries about nine rows for 1.4 s. Nine taps cost five seconds, so the native drag wins for long distances and taps win for the last few rows. That arithmetic only exists because the drag is 1.4 s. At 19 s it would never be worth taking.

Check it yourself

You need a Mac, an iPhone, a cable and a free Apple developer account. Both write endpoints need a session, so create one first and reuse its id.

git clone https://github.com/Blomega/phoneshell && cd phoneshell
bin/phoneshell doctor && bin/phoneshell up     # bridge on localhost:8100

SID=$(curl -s -X POST localhost:8100/session -H 'content-type: application/json' \
  -d '{"capabilities":{"alwaysMatch":{}}}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["sessionId"])')

# native, one canned gesture
time curl -s -X POST localhost:8100/wda/dragfromtoforduration -H 'content-type: application/json' \
  -d '{"fromX":200,"fromY":600,"toX":200,"toY":200,"duration":0.1}' > /dev/null

# W3C, the identical movement as a pointer chain
time curl -s -X POST localhost:8100/session/$SID/actions -H 'content-type: application/json' \
  -d '{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},
       "actions":[{"type":"pointerMove","duration":0,"x":200,"y":600},{"type":"pointerDown","button":0},
       {"type":"pointerMove","duration":300,"x":200,"y":200},{"type":"pointerUp","button":0}]}]}' > /dev/null

Run each three times and take the best, because the first call after a session is created pays for the session. Then vary the chain: add ten more interpolated pointerMove steps, or remove the pauses, and watch the total stay near 19 seconds. For contrast, swap pointerDown plus pointerUp with no movement between them, which is a tap, and the same endpoint answers in about half a second. The routing code that reacts to all of this is Gestures.perform in phoneshell/gestures.py.

What would prove this wrong

The claim is that on iOS 26.6 with WebDriverAgent v16.12.4, a moving W3C pointer chain costs about 19 seconds independent of its shape, and the native drag endpoint performs the same gesture in 1.4 seconds.

Sources

  1. phoneshell, FINDINGS.md sections 2, 8 and 9, published 15 September 2026. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
  2. phoneshell, phoneshell/gestures.py, the routing rule that sends one-finger drags to the native endpoint. https://github.com/Blomega/phoneshell/blob/main/phoneshell/gestures.py
  3. Appium XCUITest driver, Execute Methods reference, including mobile: dragFromToForDuration and its duration argument. https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/
  4. W3C, WebDriver Level 2, the Actions interface. https://www.w3.org/TR/webdriver2/
  5. Appium WebDriverAgent release v16.12.4, 6 September 2026, the build every measurement was taken against. https://github.com/appium/WebDriverAgent/releases/tag/v16.12.4
  6. Wu et al., OSWorld-Human: Benchmarking the Efficiency of Computer-Use Agents, arXiv:2506.16042. https://arxiv.org/abs/2506.16042