多くのアプリでは、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 })) ) } } }}
Java
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 }
Java
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) } }
Java
@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()
が呼び出されても異常は発生しません。
一部のアプリでは、バックエンドの依存関係を削除するために、デバッグビルドでフェイクがすでに使用されています。ただし、できる限りリリースビルドに近いビルドでベンチマークを行う必要があります。このドキュメントの残りの部分では、完全なプロジェクトの設定で説明されているマルチモジュール、マルチバリアントの構造を使用します。
3 つのモジュールがあります。
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) } }
Java
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() } } }
Java
@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.4 ミリ秒の間であることを示しています。ただし、ネットワーク呼び出しをベンチマークに含めると、イテレーション間の分散がソート自体の実行にかかる時間よりも大きくなるため、ソートをネットワーク呼び出しから分離する必要があります。
コードをリファクタリングして、ソートを単独で実行しやすくすることはできますが、すでに Hilt を使用している場合は、それを使用してベンチマーク用のフェイクを挿入することもできます。