많은 앱에서 Hilt를 사용하여 여러 빌드 변형에 서로 다른 동작을 삽입합니다. 이는 앱을 마이크로벤치마킹할 때 특히 유용합니다. 결과를 왜곡할 수 있는 구성 요소를 바꿀 수 있습니다. 예를 들어 다음 코드 스니펫은 이름 목록을 가져와 정렬하는 저장소를 보여줍니다.
Kotlin
class PeopleRepository @Inject constructor( @Kotlin private val dataSource: NetworkDataSource, @Dispatcher(DispatcherEnum.IO) private val dispatcher: CoroutineDispatcher ) { private val _peopleLiveData = MutableLiveData<List<Person>>() val peopleLiveData: LiveData<List<Person>> get() = _peopleLiveData suspend fun update() { withContext(dispatcher) { _peopleLiveData.postValue( dataSource.getPeople() .sortedWith(compareBy({ it.lastName }, { it.firstName })) ) } } }}
자바
public class PeopleRepository { private final MutableLiveData<List<Person>> peopleLiveData = new MutableLiveData<>(); private final NetworkDataSource dataSource; public LiveData<List<Person>> getPeopleLiveData() { return peopleLiveData; } @Inject public PeopleRepository(NetworkDataSource dataSource) { this.dataSource = dataSource; } private final Comparator<Person> comparator = Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName); public void update() { Runnable task = new Runnable() { @Override public void run() { peopleLiveData.postValue( dataSource.getPeople() .stream() .sorted(comparator) .collect(Collectors.toList()) ); } }; new Thread(task).start(); } }
벤치마킹 시 네트워크 호출을 포함하는 경우 가짜 네트워크 호출을 구현합니다. 보다 정확한 결과를 얻을 수 있습니다
벤치마킹 시 실제 네트워크 호출을 포함하면 해석하기가 더 어려워집니다. 벤치마크 결과 네트워크 호출은 다양한 외부 요인의 영향을 받을 수 있습니다. 기간은 벤치마크를 실행하는 반복마다 다를 수 있습니다. 네트워크 호출 시간은 정렬보다 오래 걸릴 수 있습니다.
Hilt를 사용하여 모조 네트워크 호출 구현
위의 예에서 볼 수 있듯이 dataSource.getPeople()
호출에는 네트워크 호출이 포함되어 있습니다. 하지만 NetworkDataSource
인스턴스는 Hilt에 의해 삽입되며 벤치마킹을 위해 다음과 같은 가짜 구현으로 대체할 수 있습니다.
Kotlin
class FakeNetworkDataSource @Inject constructor( private val people: List<Person> ) : NetworkDataSource { override fun getPeople(): List<Person> = people }
자바
public class FakeNetworkDataSource implements NetworkDataSource{ private List<Person> people; @Inject public FakeNetworkDataSource(List<Person> people) { this.people = people; } @Override public List<Person> getPeople() { return people; } }
이 모조 네트워크 호출은
getPeople()
메서드를 사용하여 지도 가장자리에
패딩을 추가할 수 있습니다. Hilt가 이를 삽입할 수 있도록 다음 제공자가 사용됩니다.
Kotlin
@Module @InstallIn(SingletonComponent::class) object FakekNetworkModule { @Provides @Kotlin fun provideNetworkDataSource(@ApplicationContext context: Context): NetworkDataSource { val data = context.assets.open("fakedata.json").use { inputStream -> val bytes = ByteArray(inputStream.available()) inputStream.read(bytes) val gson = Gson() val type: Type = object : TypeToken<List<Person>>() {}.type gson.fromJson<List<Person>>(String(bytes), type) } return FakeNetworkDataSource(data) } }
자바
@Module @InstallIn(SingletonComponent.class) public class FakeNetworkModule { @Provides @Java NetworkDataSource provideNetworkDataSource( @ApplicationContext Context context ) { List<Person> data = new ArrayList<>(); try (InputStream inputStream = context.getAssets().open("fakedata.json")) { int size = inputStream.available(); byte[] bytes = new byte[size]; if (inputStream.read(bytes) == size) { Gson gson = new Gson(); Type type = new TypeToken<ArrayList<Person>>() { }.getType(); data = gson.fromJson(new String(bytes), type); } } catch (IOException e) { // Do something } return new FakeNetworkDataSource(data); } }
데이터는 잠재적으로 가변 길이의 I/O 호출을 사용하여 애셋에서 로드됩니다.
그러나 이는 초기화 중에 실행되며 벤치마킹 중에 getPeople()
가 호출될 때는 어떠한 부rregularity도 일으키지 않습니다.
일부 앱은 이미 디버그 빌드에서 가짜 함수를 사용하여 백엔드 종속 항목을 삭제합니다. 하지만 출시 빌드에 가까운 빌드를 벤치마킹해야 하는데, 있습니다. 이 문서의 나머지 부분에서는 전체 프로젝트 설정에 설명된 대로 다중 모듈, 다중 변형 구조를 사용합니다.
다음과 같은 세 가지 모듈이 있습니다.
benchmarkable
: 벤치마킹할 코드가 포함되어 있습니다.benchmark
: 벤치마크 코드가 포함되어 있습니다.app
: 나머지 앱 코드를 포함합니다.
위의 각 모듈에는 일반적인 debug
및 release
변형과 함께 benchmark
라는 빌드 변형이 있습니다.
벤치마크 모듈 구성
모조 네트워크 호출의 코드는 debug
소스 세트에 있습니다.
benchmarkable
모듈, 전체 네트워크 구현은 release
에 있음
같은 모듈의 소스 세트입니다. 가짜 구현에서 반환된 데이터가 포함된 애셋 파일은 release
빌드에서 APK 확장을 방지하기 위해 debug
소스 세트에 있습니다. benchmark
변형은 release
및
debug
소스 세트를 사용합니다. benchmark
변형의 빌드 구성
다음은 모조 구현을 포함하는 benchmarkable
모듈의 예입니다.
Kotlin
android { ... buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } create("benchmark") { initWith(getByName("release")) } } ... sourceSets { getByName("benchmark") { java.setSrcDirs(listOf("src/debug/java")) assets.setSrcDirs(listOf("src/debug/assets")) } } }
Groovy
android { ... buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' ) } benchmark { initWith release } } ... sourceSets { benchmark { java.setSrcDirs ['src/debug/java'] assets.setSrcDirs(listOf ['src/debug/assets'] } } }
benchmark
모듈에서 Application
를 만드는 맞춤 테스트 실행기를 추가합니다.
다음과 같이 Hilt를 지원하는 테스트를 실행할 수 있습니다.
Kotlin
class HiltBenchmarkRunner : AndroidBenchmarkRunner() { override fun newApplication( cl: ClassLoader?, className: String?, context: Context? ): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
자바
public class JavaHiltBenchmarkRunner extends AndroidBenchmarkRunner { @Override public Application newApplication( ClassLoader cl, String className, Context context ) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication(cl, HiltTestApplication.class.getName(), context); } }
이렇게 하면 테스트가 실행되는 Application
객체가 HiltTestApplication
클래스를 확장하게 됩니다. 빌드를 다음과 같이 변경합니다.
구성:
Kotlin
plugins { alias(libs.plugins.android.library) alias(libs.plugins.benchmark) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kapt) alias(libs.plugins.hilt) } android { namespace = "com.example.hiltmicrobenchmark.benchmark" compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner = "com.example.hiltbenchmark.HiltBenchmarkRunner" } testBuildType = "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" ) } create("benchmark") { initWith(getByName("debug")) } } } dependencies { androidTestImplementation(libs.bundles.hilt) androidTestImplementation(project(":benchmarkable")) implementation(libs.androidx.runner) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) implementation(libs.androidx.benchmark) implementation(libs.google.dagger.hiltTesting) kaptAndroidTest(libs.google.dagger.hiltCompiler) androidTestAnnotationProcessor(libs.google.dagger.hiltCompiler) }
Groovy
plugins { alias libs.plugins.android.library alias libs.plugins.benchmark alias libs.plugins.jetbrains.kotlin.android alias libs.plugins.kapt alias libs.plugins.hilt } android { namespace = 'com.example.hiltmicrobenchmark.benchmark' compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner 'com.example.hiltbenchmark.HiltBenchmarkRunner' } testBuildType "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. minifyEnabled true proguardFiles( getDefaultProguardFile('proguard-android-optimize.txt'), 'benchmark-proguard-rules.pro' ) } benchmark { initWith debug" } } } dependencies { androidTestImplementation libs.bundles.hilt androidTestImplementation project(':benchmarkable') implementation libs.androidx.runner androidTestImplementation libs.androidx.junit androidTestImplementation libs.junit implementation libs.androidx.benchmark implementation libs.google.dagger.hiltTesting kaptAndroidTest libs.google.dagger.hiltCompiler androidTestAnnotationProcessor libs.google.dagger.hiltCompiler }
위의 예에서는 다음을 실행합니다.
- 빌드에 필요한 Gradle 플러그인을 적용합니다.
- 맞춤 테스트 실행기를 사용하여 테스트를 실행하도록 지정합니다.
benchmark
변형이 이 모듈의 테스트 유형임을 지정합니다.benchmark
변형을 추가합니다.- 필요한 종속 항목을 추가합니다.
Gradle이 벤치마킹을 실행하는 connectedBenchmarkAndroidTest
태스크를 만들도록 testBuildType
를 변경해야 합니다.
Microbenchmark 만들기
벤치마크는 다음과 같이 구현됩니다.
Kotlin
@RunWith(AndroidJUnit4::class) @HiltAndroidTest class PeopleRepositoryBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() @get:Rule val hiltRule = HiltAndroidRule(this) private val latch = CountdownLatch(1) @Inject lateinit var peopleRepository: PeopleRepository @Before fun setup() { hiltRule.inject() } @Test fun benchmarkSort() { benchmarkRule.measureRepeated { runBlocking { benchmarkRule.getStart().pauseTiming() withContext(Dispatchers.Main.immediate) { peopleRepository.peopleLiveData.observeForever(observer) } benchmarkRule.getStart().resumeTiming() peopleRepository.update() latch.await() assert(peopleRepository.peopleLiveData.value?.isNotEmpty() ?: false) } } } private val observer: Observer<List<Person>> = object : Observer<List<Person>> { override fun onChanged(people: List<Person>?) { peopleRepository.peopleLiveData.removeObserver(this) latch.countDown() } } }
자바
@RunWith(AndroidJUnit4.class) @HiltAndroidTest public class PeopleRepositoryBenchmark { @Rule public BenchmarkRule benchmarkRule = new BenchmarkRule(); @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this); private CountdownLatch latch = new CountdownLatch(1); @Inject JavaPeopleRepository peopleRepository; @Before public void setup() { hiltRule.inject(); } @Test public void benchmarkSort() { BenchmarkRuleKt.measureRepeated(benchmarkRule, (Function1<BenchmarkRule.Scope, Unit>) scope -> { benchmarkRule.getState().pauseTiming(); new Handler(Looper.getMainLooper()).post(() -> { awaitValue(peopleRepository.getPeopleLiveData()); }); benchmarkRule.getState().resumeTiming(); peopleRepository.update(); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } assert (!peopleRepository.getPeopleLiveData().getValue().isEmpty()); return Unit.INSTANCE; }); } private <T> void awaitValue(LiveData<T> liveData) { Observer<T> observer = new Observer<T>() { @Override public void onChanged(T t) { liveData.removeObserver(this); latch.countDown(); } }; liveData.observeForever(observer); return; } }
앞의 예에서는 벤치마크와 Hilt 모두에 관한 규칙을 만듭니다.
benchmarkRule
는 벤치마크의 타이밍을 실행합니다. hiltRule
는 벤치마크 테스트 클래스에 종속 항목 삽입을 실행합니다. 개별 테스트를 실행하기 전에 삽입을 실행하려면 @Before
함수에서 Hilt 규칙의 inject()
메서드를 호출해야 합니다.
벤치마크 자체는 LiveData
관찰자가 등록되는 동안 타이밍을 일시중지합니다. 그런 다음 래치를 사용하여 LiveData
가 업데이트될 때까지 기다린 후 완료합니다. 정렬은 다음 시점 사이의 시간에 실행되므로
peopleRepository.update()
가 호출되고 LiveData
가 업데이트를 받으면
정렬 기간은 벤치마크 타이밍에 포함됩니다.
Microbenchmark 실행
./gradlew :benchmark:connectedBenchmarkAndroidTest
로 벤치마크 실행
벤치마크를 여러 차례에 걸쳐 실행하고 타이밍 데이터를
Logcat:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
위 예에서는 1,000개의 항목 목록에서 정렬 알고리즘을 실행하는 데 걸리는 시간(0.6~1.4ms)을 나타내는 벤치마크 결과를 보여줍니다. 하지만 네트워크 호출이 벤치마크보다 크면 반복 간의 편차가 더 큽니다. 정렬이 실행되는 데 걸리는 시간보다 훨씬 짧기 때문에 네트워크 호출로부터의 정렬입니다.
언제든지 코드를 리팩터링하여 분리된 상태에서 정렬을 더 쉽게 실행할 수 있지만, 이미 Hilt를 사용하고 있다면 대신 이를 사용하여 벤치마킹을 위한 가짜를 삽입할 수 있습니다.