Jetpack Benchmark를 사용한 실행 시간 측정

입문


Android Advent Calendar 2019의 20일째.
이 글에서 구글 I/O 2019에서 발표된 Jetpack Benchmark 1.0.0은 지난달 말 stable를 발표했는데 이것을 소개하고 싶습니다.
※ 2019년 12월 20일 현재 일본어 문서는 알파 버전이므로 언어를 바꾸어 사용하는 것이 좋습니다.

Jetpack Benchmark 정보


Benchmark는 안드로이드 응용 프로그램의 코드 성능을 적절하게 측정하는 라이브러리입니다.
코드의 집행 속도를 측정하는 토대에서 단순한 시작 시간과 종료 시간의 차이에서 매번 측정할 때마다 불균형이 발생한다
단말기가 막 시작되거나 고부하 상태에 있는 등 불안정 요소의 존재로 인해 매번 적당한 측정이 어렵다.
이러한 문제에 대해 라이브러리는 예열 시간의 대응, 이상 값의 삭제, CPU 시계의 잠금 등 측정에서의 안정성을 제공하여 개발자는 더욱 테스트하고 싶은 코드에 집중할 수 있다.
  • Improving App Performance with Benchmarking (Google I/O'19)
  • 설치하다


    Benchmark app code에 설명된 대로 데이텀용 모듈을 생성합니다.
    Android Studio 3.6 이후의 경우 File > New > New Module에서 Benchmark Module을 선택하여 템플릿을 사용할 수 있습니다.
    Android Studio3.5의 경우Set Android Studio properties에 설명된 대로 IDE 속성을 사용자 정의하여 사용할 수 있습니다.

    이 모듈은 테스트에 디버거와 분석 도구를 사용할 수 없음을 지정합니다debuggable=false.
    디버거와의 연결로 운행 속도가 느려지고 지연 정도가 변동되기 때문에 기준 측정에 적합하지 않기 때문에 설정한 것이다.다음 그림에서 debuggable가 사실이라면deserialization은 1% 정도의 차이이고 inflateSimple은 80% 정도의 차이입니다.

    모듈 설정은 다음과 같다. 모듈 절단을 통해 제품의 설정은 직접 기준용 설정으로 측정할 수 있다.

    문제 해결


    2019년 12월 20일까지 템플릿에서 만든 의존성 설명이 업데이트되지 않았는지 sync를 통과할 수 없습니다.
    Detected usage of the testInstrumentationRunner,
                                androidx.benchmark.AndroidBenchmarkRunner, in project benchmark,
                                which is no longer valid as it has been moved to
                                androidx.benchmark.junit4.AndroidBenchmarkRunner.
    
    junit4 패키지를 사용하는 내용으로 변경합니다. 아래와 같습니다.
    diff --git a/benchmark/build.gradle b/benchmark/build.gradle
    index 82c8b333c1..89316a235a 100644
    --- a/benchmark/build.gradle
    +++ b/benchmark/build.gradle
    @@ -11,7 +11,7 @@ android {
             versionCode 1
             versionName "1.0"
    
    -        testInstrumentationRunner 'androidx.benchmark.AndroidBenchmarkRunner'
    +        testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner'
         }
    
         buildTypes {
    @@ -37,5 +41,5 @@ dependencies {
         androidTestImplementation 'androidx.test:runner:1.1.1'
         androidTestImplementation 'androidx.test.ext:junit:1.1.0'
         androidTestImplementation 'junit:junit:4.12'
    -    androidTestImplementation 'androidx.benchmark:benchmark:1.0.0-alpha01'
    +    androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.0.0'
     }
    diff --git a/benchmark/src/androidTest/java/com/example/benchmark/ExampleBenchmark.kt b/benchmark/src/androidTest/java/com/example/benchmark/ExampleBenchmark.kt
    index 11d4e320a4..05c506ebed 100644
    --- a/benchmark/src/androidTest/com/example/benchmark/ExampleBenchmark.kt
    +++ b/benchmark/src/androidTest/com/example/benchmark/ExampleBenchmark.kt
    @@ -1,8 +1,8 @@
     package com.example.benchmark
    
     import android.util.Log
    -import androidx.benchmark.BenchmarkRule
    -import androidx.benchmark.measureRepeated
    +import androidx.benchmark.junit4.BenchmarkRule
    +import androidx.benchmark.junit4.measureRepeated
     import androidx.test.ext.junit.runners.AndroidJUnit4
     import org.junit.Rule
     import org.junit.Test
    diff --git a/build.gradle b/build.gradle
    index 0ec2401160..4955fb048a 100644
    --- a/build.gradle
    +++ b/build.gradle
    @@ -72,7 +72,7 @@ buildscript {
             classpath "${Dep.GradlePlugin.butterknife}"
             classpath "com.github.mataku:releases-hub-gradle-plugin:0.0.2"
             classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    -        classpath 'androidx.benchmark:benchmark-gradle-plugin:1.0.0-alpha01'
    +        classpath 'androidx.benchmark:benchmark-gradle-plugin:1.0.0'
         }
     }
    
    

    데이텀 생성 및 실행


    모듈을 만들면 ExampleBenchmark 을 생성합니다. 이를 바탕으로 사용자 정의를 할 수 있습니다.
    ExampleBenchmark.kt
    @RunWith(AndroidJUnit4::class)
    class ExampleBenchmark {
    
        @get:Rule
        val benchmarkRule = BenchmarkRule()
    
        @Test
        fun log() {
            benchmarkRule.measureRepeated {
                Log.d("LogBenchmark", "the cost of writing this log method will be measured")
            }
        }
    }
    
    src/androidTest/java/ 산하에 BenchmarkInstrumented Unit Tests를 생성합니다.
    테스트 파일을 생성하여 BenchmarkRule 인스턴스(위에서 설명한 바와 같이)를 생성하고 measureRepeated 블록에서 구현된 내용을 측정할 수 있습니다.
    실행 시 Log.d 속도를 측정합니다.
    Started running tests
    benchmark:         4,482 ns ExampleBenchmark.log
    
    Tests ran to completion.
    
    또한 단말기의 배터리가 적은 등 정확하게 측정하지 못하는 주요 원인이 있는 경우 워닝 경고로 다음과 같다.
    Started running tests
    benchmark: WARNING: Device has low battery (16%)
    benchmark:     When battery is low, devices will often reduce performance (e.g. disabling big
    benchmark:     cores) to save remaining battery. This occurs even when they are plugged in.
    benchmark:     Wait for your battery to charge to at least 25%.
    benchmark:     Currently at 16%.
    benchmark:
    benchmark:         4,518 ns LOW-BATTERY_ExampleBenchmark.log
    
    Tests ran to completion.
    

    RecyclerView 스크롤 측정


    googlesamples/android-architecture-component의 PagingWithNetworkSample에 RecyclerView 데이텀 측정이 추가되었습니다.
    이 응용 프로그램은 Paging Library 에 요청을 하고 투고를 표시하는 응용 프로그램입니다 Raddit API.
    RadditActivity에서 사용하는 PostAdapter를 사용하여 RecyclerView를 측정합니다.
    @LargeTest
    @RunWith(AndroidJUnit4::class)
    class PostsAdapterBenchmark {
        @get:Rule
        val benchmarkRule = BenchmarkRule()
    
        @get:Rule
        val activityRule = ActivityTestRule(BenchmarkActivity::class.java)
    
        @UiThreadTest
        @Test
        fun scrollItem() {
            val activity = activityRule.activity
    
            // If RecyclerView has children, the items are attached, bound, and gone through layout.
            // Ready to benchmark.
            assertTrue("RecyclerView expected to have children", activity.list.childCount > 0)
    
            benchmarkRule.measureRepeated {
                activity.list.scrollByOneItem()
                runWithTimingDisabled {
                    activity.testExecutor.flush()
                }
            }
        }
    
        private fun RecyclerView.scrollByOneItem() {
            scrollBy(0, getChildAt(childCount - 1).height)
        }
    }
    
    이 예에서 우리는 BenchmarkActivity라는 가상 활동을 만들어서 ActivityTestRule 를 정의하고 활동에서 RecyclerView를 얻으며 scrollByOneItem 의 속도를 측정했다.@UiThreadTest 주석을 통해 테스트 방법은 주 라인에서 실행되고 runWithTimingDisabled를 사용하면 이 블록 내의 시간 측정을 배제하고 실행할 수 있다.
    inline fun <T> runWithTimingDisabled(block: () -> T): T {
        getOuterState().pauseTiming()
        val ret = block()
        getOuterState().resumeTiming()
        return ret
    }
    
    

    총결산


    독립된 모듈로 기준의 측정 환경을 구축하여 쉽게 측정하는 것이 매우 편리하다.
    코드 심사에서 작법에 따라 속도의 차이를 논의했다면 실제 쓰기 테스트를 통해 비교, 판단을 할 수 있다.
    한편, Android Dev Summit '19의 애니메이션과 미디어에 실린 기사Fighting Regressions with Benchmarks in CI
    )에 따르면 CI 등 개선을 계속하려는 경우 임계값 등을 설정해도 측정 대상에 따라 증감비율이 다르거나 임시 스매시 형태로 측정이 불안정해질 수 있음데이터 수집이 계속되는 상황에서도 재검토가 일어난 것은 감지하기 어렵다.상기 보도에서 절차적 의합을 통해 특정한 모델을 회귀로 판단하고 검측하는 내용을 소개했다.
    매일 CI에서 쉽게 측정할 수 있다기보다 분석을 통해 판단할 수 있는 인상이기 때문에 이 근처에서 더 쉽게 확인할 수 있다면 쉽게 사용할 수 있을 것 같습니다.

    참고 자료

  • Benchmark | Android Developers
  • Benchmark app code | Android Developers
  • Run benchmarks in Continuous Integration
  • Improving App Performance with Benchmarking (Google I/O'19)

  • Fighting Regressions with Benchmarks in CI (Android Dev Summit '19)
    )

  • Fighting Regressions with Benchmarks in CI (Medium)
    )
  • 좋은 웹페이지 즐겨찾기