ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション開発チームのテックブログです

AppiumでXPathを使用すると遅いらしいけど、どれくらい遅いの?

ども。LDI品質管理部の仙波です。
スマホアプリの自動テストを担当しています。

最近は基本的にリモートワークですが、気分転換に商店街まで散歩してたい焼きを買って帰るのが楽しみです。


前回はXPathを使って画面要素を柔軟に取得する方法を紹介しました。
その際、XPathの使用は他の方法に比べてパフォーマンスに問題があるという通説があることに触れました。

問題があるっていうけど、どれくらいインパクトがある話なの?と言われると自分でも明確に説明できないことに気づいたので、今回は簡単なパフォーマンス計測用のコードを作成し、どれくらい遅いのか計測してみました。


パフォーマンス計測(Androidの場合)

計測に使用したコード

import io.appium.java_client.MobileElement
import io.appium.java_client.android.AndroidDriver
import org.apache.commons.lang3.time.StopWatch
import org.junit.jupiter.api.Test
import org.openqa.selenium.remote.DesiredCapabilities
import java.net.URL

class Performance {

    private fun getAppiumDriver(): AndroidDriver<MobileElement> {

        // DesiredCapabilities
        val capabilities = DesiredCapabilities()
        capabilities.setCapability("automationName", "UiAutomator2")
        capabilities.setCapability("platformName", "Android")
        capabilities.setCapability("platformVersion", "10")
        capabilities.setCapability("appPackage", "com.android.settings")
        capabilities.setCapability("appActivity", "com.android.settings.Settings")

        // appiumServerAddress
        val appiumServerAddress = URL("http://127.0.0.1:4723/wd/hub")

        // appiumDriver
        val appiumDriver = AndroidDriver<MobileElement>(appiumServerAddress, capabilities)
        return appiumDriver
    }

    private val loopCount = 10

    @Test
    fun performance() {

        val d = getAppiumDriver()
        d.findElementsById("com.android.settings:id/main_content")

        val sw = StopWatch()

        val results1 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementById("com.android.settings:id/main_content")
            }
            results1.add(time)
            println("byId    $time")
        }

        val results2 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementByXPath("//*[@resource-id='com.android.settings:id/main_content']")
            }
            results2.add(time)
            println("byXPath $time")
        }

        println("--- results ---")
        println("byId    average:${results1.average()}/min:${results1.minOrNull()}/max:${results1.maxOrNull()}")
        println("byXPath average:${results2.average()}/min:${results2.minOrNull()}/max:${results2.maxOrNull()}")
    }

    private fun measureTime(sw: StopWatch, proc: () -> Unit): Long {

        sw.reset()
        sw.start()
        proc()
        sw.stop()
        return sw.time
    }

    @Test
    fun performance2() {

        val d = getAppiumDriver()
        d.findElementsById("com.android.settings:id/main_content")

        val sw = StopWatch()

        val results1 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementByClassName("android.widget.ScrollView")
            }
            results1.add(time)
            println("byClass $time")
        }

        val results2 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementByXPath("//*[@class='android.widget.ScrollView']")
            }
            results2.add(time)
            println("byXPath $time")
        }

        println("--- results ---")
        println("byClass average:${results1.average()}/min:${results1.minOrNull()}/max:${results1.maxOrNull()}")
        println("byXPath average:${results2.average()}/min:${results2.minOrNull()}/max:${results2.maxOrNull()}")
    }

}

計測結果

むしろXPathの方が平均的に速く、通説を覆す結果となりました。(MacでJava/KotlinでUIAutomator2の場合)

単位はmsです。


byId vs byXPath

byId    49
byId    157
byId    527
byId    37
byId    37
byId    173
byId    145
byId    27
byId    29
byId    35
byXPath 128
byXPath 65
byXPath 67
byXPath 59
byXPath 59
byXPath 57
byXPath 226
byXPath 110
byXPath 73
byXPath 59
--- results ---
byId    average:121.6/min:27/max:527
byXPath average:90.3/min:57/max:226


byClass vs byXPath

byClass 66
byClass 353
byClass 323
byClass 32
byClass 30
byClass 34
byClass 235
byClass 80
byClass 34
byClass 34
byXPath 132
byXPath 58
byXPath 58
byXPath 63
byXPath 53
byXPath 51
byXPath 64
byXPath 278
byXPath 47
byXPath 52
--- results ---
byClass average:122.2/min:30/max:354
byXPath average:85.6/min:47/max:278

この結果からはbyIdやbyClassの方がbyXPathよりも高速であると言うことはできず、むしろbyXPathの方が安定して速いと言えます。

ただし、1項目あたり50ms以内の差です。


パフォーマンス計測( iOSの場合)

計測に使用したコード

import io.appium.java_client.AppiumDriver
import io.appium.java_client.MobileElement
import io.appium.java_client.ios.IOSDriver
import org.apache.commons.lang3.time.StopWatch
import org.junit.jupiter.api.Test
import org.openqa.selenium.remote.DesiredCapabilities
import java.net.URL

class PerformanceIos {

    private fun getAppiumDriver(): AppiumDriver<MobileElement> {

        // DesiredCapabilities
        val capabilities = DesiredCapabilities()
        capabilities.setCapability("automationName", "XCUITest")
        capabilities.setCapability("platformName", "iOS")
        capabilities.setCapability("platformVersion", "14.3")
        capabilities.setCapability("deviceName", "iPhone 12 Pro")
        capabilities.setCapability("simpleIsVisibleCheck", "true")
        capabilities.setCapability("useJSONSource", "true")
        capabilities.setCapability("bundleId", "com.apple.Preferences")

        // appiumServerAddress
        val appiumServerAddress = URL("http://127.0.0.1:4723/wd/hub")

        // appiumDriver
        val appiumDriver = IOSDriver<MobileElement>(appiumServerAddress, capabilities)
        return appiumDriver
    }

    private val loopCount = 10

    @Test
    fun performance() {

        val d = getAppiumDriver()
        d.findElementById("General")

        val sw = StopWatch()

        val results1 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementById("General")
            }
            results1.add(time)
            println("byId    $time")
        }

        val results2 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementByXPath("//*[@name='General']")
            }
            results2.add(time)
            println("byXPath $time")
        }

        println("--- results ---")
        println("byId    average:${results1.average()}/min:${results1.minOrNull()}/max:${results1.maxOrNull()}")
        println("byXPath average:${results2.average()}/min:${results2.minOrNull()}/max:${results2.maxOrNull()}")
    }

    private fun measureTime(sw: StopWatch, proc: () -> Unit): Long {

        sw.reset()
        sw.start()
        proc()
        sw.stop()
        return sw.time
    }

    @Test
    fun performance2() {

        val d = getAppiumDriver()
        d.findElementByClassName("XCUIElementTypeTable")

        val sw = StopWatch()

        val results1 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementByClassName("XCUIElementTypeTable")
            }
            results1.add(time)
            println("byClass $time")
        }

        val results2 = mutableListOf<Long>()
        for (i in 1..loopCount) {
            val time = measureTime(sw) {
                d.findElementByXPath("//*[@type='XCUIElementTypeTable']")
            }
            results2.add(time)
            println("byXPath $time")
        }

        println("--- results ---")
        println("byClass average:${results1.average()}/min:${results1.minOrNull()}/max:${results1.maxOrNull()}")
        println("byXPath average:${results2.average()}/min:${results2.minOrNull()}/max:${results2.maxOrNull()}")
    }

}

計測結果

XPathを使用すると常に遅く、通説通りの結果となりました。(MacでJava/KotlinでXCUITestの場合)

単位はmsです。


byId vs byXPath

byId    165
byId    182
byId    177
byId    163
byId    141
byId    147
byId    141
byId    141
byId    130
byId    127
byXPath 508
byXPath 487
byXPath 488
byXPath 496
byXPath 517
byXPath 513
byXPath 549
byXPath 602
byXPath 601
byXPath 518
--- results ---
byId    average:151.5/min:127/max:182
byXPath average:527.9/min:487/max:602


byClass vs byXPath

byClass 109
byClass 94
byClass 92
byClass 102
byClass 104
byClass 100
byClass 115
byClass 109
byClass 107
byClass 104
byXPath 277
byXPath 267
byXPath 268
byXPath 327
byXPath 295
byXPath 264
byXPath 259
byXPath 257
byXPath 283
byXPath 263
--- results ---
byClass average:103.7/min:92/max:115
byXPath average:276.0/min:257/max:327

パフォーマンス計測結果(サマリー)

計測結果を表にまとめます。

  • 平均は四捨五入
  • 青字が速い方、赤字が遅い方
OS ケース 検索する属性 メソッド 平均(ms) 最早(ms) 最遅(ms)
Android 1 resource-id byId
122
27
527
Android 1 resource-id byXPath
90
57
226
Android 2 className byClass
104
92
115
Android 2 className byXPath
86
47
278
iOS 3 name byId
152
127
182
iOS 3 name byXPath
523
487
602
iOS 4 type byClass
104
92
115
iOS 4 type byXPath
276
257
327

なぜAndroidとiOSで逆の結果となるのか

要素取得の速度はAndroidの方が常にiOSよりも速いので、AndroidのUIAutomator2は十分パフォーマンスチューニングされているのではないかと思われます。XPathを使用した方が良い結果が出る場合があるのは面白いところです。

iOSのXCUITestはUIAutomator2と比較すると常にもっさりしており、十分チューニングされていない印象を受けます。(全く同じ仕様のAndroidアプリとiOSアプリに対して同じ内容のテストを実行すると、常にAndroidの方が速いことが経験上わかっています。)

iOSについては通説が当てはまるので、XPathをできるだけ使用しないようにした方が、実行時間は短縮できそうです。


まとめ

  • XPathによる要素取得は遅いという通説について、実験の結果、iOS(XCUITest)ではその通りですが、Android(UIAutomator2)では当てはまらないことがわかりました
  • Android(UIAutomator2)については、XPathを使ってもパフォーマンス上の問題はなさそうです
  • iOS(XCUITest)はXPathを使うと遅いですが、使わなくてもAndroidよりパフォーマンスが悪いのでもっと頑張って欲しいです