Very High 20s Latency for Scrolling [iOS][XCUITest] (local USB, local Appium Server)

Issue

I’m currently running into an issue where it is taking up to 20 seconds (!) for scroll/tap gestures (for the Appium server to respond w/ the gesture completed).

The goal is to get scrolls / taps down to ~<1s or even faster (I using my keyboard to send events & the idea is to have near-0 latency on the device executing what I’m typing/directing via keyboard).

Setup specs:

  • Appium Client: Python client (Appium-Python-Client v3.1.1)
  • Machine (my laptop): MacBook Air (M1, 2020) (Sonoma 14.0)
  • Phone: iPhone 13 Pro Max (iOS version 17.1)

(phone connected directly to laptop via USB — all local setup)

Scroll Code

The following is my scrolling code — I originally had it running all synchronously but took the gesture off the main thread to ensure it was not any of my other application code blocking the next gesture. Both versions (synchronous & executing on a seperate thread) produce the same latency.

    def _scroll(self, direction: str, duration_ms: int = 100, offset: int = 100):
        start_x = self.device_center_x
        end_x = self.device_center_x

        # calculate start/end y positions
        if direction == "up":
            start_y = self.device_center_y - offset
            end_y = self.device_center_y + offset
        elif direction == "down":
            start_y = self.device_center_y + offset
            end_y = self.device_center_y - offset
        else:
            raise Exception(f"fatal: invalid scroll direction {direction}")

        # perform the scroll

        def execute_gesture(driver, start_x, start_y, end_x, end_y, duration_ms):
            logger.debug(f"scroll [{'↑' if direction == 'up' else '↓'}] start")

            actions = TouchAction(driver)
            actions.press(x=start_x, y=start_y).move_to(x=end_x, y=end_y).wait(
                ms=duration_ms
            ).release().perform()

            logger.debug(f"scroll [{'↑' if direction == 'up' else '↓'}] end")

        # run the gesture in a separate thread so we can continue to receive keyboard events
        gesture_thread = threading.Thread(
            target=execute_gesture,
            args=(self.driver, start_x, start_y, end_x, end_y, duration_ms),
        )
        gesture_thread.start()

Tap Code

The following is my tap code.

    def _tap(self, x: int, y: int):
        def execute_gesture(driver, x, y):
            logger.debug(f"tap start")

            actions = TouchAction(driver)
            actions.tap(x=x, y=y).perform()

            logger.debug(f"tap end")

        # run the gesture in a separate thread so we can continue to receive keyboard events
        gesture_thread = threading.Thread(
            target=execute_gesture, args=(self.driver, x, y)
        )
        gesture_thread.start()

Error Showing Latency

I’ve pinned down that the issue is in the test driver responding very slowly (and subsequently not serving later requests from my code). The below error shows the latency I’m experiencing executing a scroll:

[HTTP] --> POST /session/4cbf5668-97b4-48de-8e4a-f03bc392d79a/touch/perform
[HTTP] {"actions":[{"action":"press","options":{"x":214,"y":263}},{"action":"moveTo","options":{"x":214,"y":663}},{"action":"wait","options":{"ms":100}},{"action":"release","options":{}}]}
[XCUITestDriver@f45f (4cbf5668)] Calling AppiumDriver.performTouch() with args: [[{"action":"press","options":{"x":214,"y":263}},{"action":"moveTo","options":{"x":214,"y":663}},{"action":"wait","options":{"ms":100}},{"action":"release","options":{}}],"4cbf5668-97b4-48de-8e4a-f03bc392d79a"]
[XCUITestDriver@f45f (4cbf5668)] Executing command 'performTouch'
[XCUITestDriver@f45f (4cbf5668)] Got response with status 200: {"value":null,"sessionId":"3AA5E225-CFFE-48BE-B993-578AF65379AF"}
[XCUITestDriver@f45f (4cbf5668)] Received the following touch action: press(options={"x":214,"y":263})-moveTo(options={"x":214,"y":663})-wait(options={"ms":100})-release(options={})
[XCUITestDriver@f45f (4cbf5668)] Proxying [POST /wda/touch/perform] to [POST http://127.0.0.1:8100/session/3AA5E225-CFFE-48BE-B993-578AF65379AF/wda/touch/perform] with body: {"actions":[{"action":"press","options":{"x":214,"y":263}},{"action":"moveTo","options":{"x":214,"y":663}},{"action":"wait","options":{"ms":100}},{"action":"release","options":{}}]}
[XCUITestDriver@f45f (4cbf5668)] Responding to client with driver.performTouch() result: null
[HTTP] <-- POST /session/4cbf5668-97b4-48de-8e4a-f03bc392d79a/touch/perform 200 23623 ms - 14
[HTTP] 

As you can see it took ~24s for the server to respond successfully.

The actual scroll completes nearly immediately, <~1s (then there is a long wait for the server to unblock).

Webdriver Config (capabilities)

{
    "platformName": "iOS",
    "automationName": "XCUITest",
    "platformVersion": "17.1",
    "udid": _omitted_,
    "xcodeOrgId": _omitted_,
    "xcodeSigningId": "iPhone Developer",
    "noReset": False,
    "newCommandTimeout": 60 * 60,
    "waitForQuiescence": False, # tried this
    "waitForIdleTimeout": 0, # tried this
}

Things I’ve Tried

  • Restarting my iPhone
  • Restarting my laptop
  • Restarting the Appium server
  • Deleting the WebDriverAgentRunner-Runner app (on my phone), restarting the Appium server, then running my application again (which installs it)


If I can’t solve this would also love to know other recommendations to achieve the low latency I’m trying to go for (though I’m enjoying using Appium!).

Thanks for all the help!

Try https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-scrolltoelement

and/or

Also make sure the app under test does not block the main event loop for too long. You may also want to reduce the waitForIdleTimeout/animationCoolOffTimeout setting values if it does

Also make sure the app under test does not block the main event loop for too long. You may also want to reduce the waitForIdleTimeout/animationCoolOffTimeout settings values if it does

Earmarking, will return to these — let me try the first 2 suggestions.


I just refactored things a bit so I have the rich WebElements in-hand that I can get an identifier from (.id) to try the above methods (was parsing the raw page xml via driver.page_source for absolute element positions & markup data before).

Off the bat, calling .click() is really snappy and responsive.

scrollToElement

Really snappy now, near instant.

Pre-finding all the nodes I want to jump to is a bit expensive (via driver.find_element), but now I’m getting scrolls in ~200ms. :+1: (so the server bottleneck is solved)

My code currently looks like this:

self.driver.execute_script(
    "mobile: scrollToElement",
    {
        "elementId": element.id
    }
)
dragFromToForDuration

This hits ~1.6s-2.2s per scroll e2e w/ the below code:

self.driver.execute_script(
    "mobile: dragFromToForDuration",
    {
        "duration": .5,
        "fromX": 300, 
        "fromY": 800,  # arbitrarily chosen
        "toX": 300,
        "toY": 100   # arbitrarily chosen
    }
)

I would be nice for the drag gesture to not block for those ~2s, but having scrollToElement as an alternative helps (though it requires an id, will see how I use each as I continue developing).

Nice — I think this solves it (server is responding in ~200ms).

Thanks!

1 Like