많은 앱에서 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
라는 빌드 변형이 있습니다.
벤치마크 모듈 구성
가짜 네트워크 호출 코드는 benchmarkable
모듈의 debug
소스 세트에 있고 전체 네트워크 구현은 동일한 모듈의 release
소스 세트에 있습니다. 가짜 구현에서 반환된 데이터가 포함된 애셋 파일은 release
빌드에서 APK 확장을 방지하기 위해 debug
소스 세트에 있습니다. benchmark
변형은 release
를 기반으로 하고 debug
소스 세트를 사용해야 합니다. 가짜 구현이 포함된 benchmarkable
모듈의 benchmark
변형에 대한 빌드 구성은 다음과 같습니다.
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
모듈에서 다음과 같이 Hilt를 지원하는 테스트를 실행할 Application
를 만드는 맞춤 테스트 실행기를 추가합니다.
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
를 변경해야 합니다.
마이크로 벤치마크 만들기
벤치마크는 다음과 같이 구현됩니다.
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
가 업데이트를 수신한 시점 사이의 시간에 실행되므로 정렬 시간은 벤치마크 타이밍에 포함됩니다.
마이크로 벤치마크 실행
./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를 사용하고 있다면 대신 이를 사용하여 벤치마킹을 위한 가짜를 삽입할 수 있습니다.