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.
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
| call | endpoint | measured | needs a session | source |
|---|---|---|---|---|
| foreground app info | GET /wda/activeAppInfo | 43 ms | no | FINDINGS s.2 |
| screenshot | GET /screenshot | 72 ms | no | FINDINGS s.2 |
| accessibility tree, depth 20 | GET /source | 159 to 210 ms | no | FINDINGS s.2 |
| tap | POST /session/:id/actions | 500 ms | yes | FINDINGS s.2 |
| swipe | POST /wda/swipe | 1,000 ms | yes | FINDINGS s.2 |
| drag | POST /wda/dragfromtoforduration | 1,400 ms | yes | FINDINGS s.2 |
| scroll | POST /wda/scroll | 7,350 ms | yes | FINDINGS s.2 |
| drag | POST /session/:id/actions | 19,000 ms | yes | FINDINGS s.2 |
| same drag, 1 move step | POST /session/:id/actions | 19.2 s | yes | FINDINGS s.2 |
| same drag, 12 move steps | POST /session/:id/actions | 19.8 s | yes | FINDINGS s.2 |
| same drag, no pauses | POST /session/:id/actions | 18.9 s | yes | FINDINGS s.2 |
| picker wheel tap, round trip | POST /session/:id/actions | 550 ms | yes | FINDINGS 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.
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.
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.
- Route single-finger drags off the standard path. Same gesture, same pixels, 1.4 s instead of 19 s. If your client only speaks W3C actions, this is the cost of portability and it is worth knowing you are paying it.
- Batch the turns, not the milliseconds. Our tool design used to force two
turns per action, an
observeto learn element ids and then atapusing one, so a three-screen navigation cost six round trips and roughly thirty seconds for work the engine could do deterministically in three. A compound call that runs a list of steps and stops at the first failure removed that floor. On an identical three-turn shape, returning one line for an unchanged screen instead of a full tree took a task from $0.1011 to $0.0345, with cache-write tokens falling from 18,584 to 1,278.
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.
- If anyone times a moving
/actionschain on iOS 26.6 at under 5 seconds with a stock WebDriverAgent build, our number is an artifact of this rig rather than a property of the path, and the routing rule should be dropped. We predict it holds on WebDriverAgent v16.12.x and iOS 26.6 through 31 December 2026. - If a later WebDriverAgent or Xcode release changes the synthesis path and the gap closes, the rule expires rather than being wrong. That is the likeliest way this note ages, and the version numbers are on it for exactly that reason.
- The multi-finger claim is the weakest one here. We measured that multi-finger paths still cost about 20 seconds, but we did not measure a pinch as carefully as we measured the drag, and a reader who does should not treat our 20 as precise.
Sources
- phoneshell, FINDINGS.md sections 2, 8 and 9, published 15 September 2026. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
- 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
- Appium XCUITest driver, Execute Methods reference, including mobile: dragFromToForDuration and its duration argument. https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/
- W3C, WebDriver Level 2, the Actions interface. https://www.w3.org/TR/webdriver2/
- 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
- Wu et al., OSWorld-Human: Benchmarking the Efficiency of Computer-Use Agents, arXiv:2506.16042. https://arxiv.org/abs/2506.16042