Viele Apps verwenden Hilt, um verschiedene Verhaltensweisen in verschiedene Build-Varianten einzuschleusen. Dies kann besonders beim Micro-Benchmarking Ihrer Anwendung nützlich sein, da Sie damit eine Komponente austauschen können, die die Ergebnisse verzerren kann. Das folgende Code-Snippet zeigt beispielsweise ein Repository, das eine Liste von Namen abruft und sortiert:
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(); } }
Wenn Sie beim Benchmarking einen Netzwerkaufruf einbeziehen, implementieren Sie einen fiktiven Netzwerkaufruf, um ein genaueres Ergebnis zu erhalten.
Wenn Sie beim Benchmarking einen echten Netzwerkaufruf einbeziehen, wird die Interpretation von Benchmarkergebnissen erschwert. Netzwerkaufrufe können von vielen externen Faktoren beeinflusst werden und ihre Dauer kann zwischen den Iterationen der Benchmarkausführung variieren. Die Dauer von Netzwerkaufrufen kann länger dauern als die Sortierung.
Gefälschten Netzwerkanruf mit Hilt implementieren
Der Aufruf von dataSource.getPeople()
, wie im vorherigen Beispiel gezeigt, enthält einen Netzwerkaufruf. Die Instanz NetworkDataSource
wird jedoch von Hilt eingeschleust und Sie können sie durch die folgende fiktive Implementierung für das Benchmarking ersetzen:
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; } }
Dieser fiktive Netzwerkaufruf soll so schnell wie möglich ausgeführt werden, wenn Sie die Methode getPeople()
aufrufen. Damit Hilt dies einschleusen kann, wird der folgende Anbieter verwendet:
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); } }
Die Daten werden über einen E/A-Aufruf mit potenziell variabler Länge aus Assets geladen.
Dies erfolgt jedoch während der Initialisierung und führt nicht zu Unregelmäßigkeiten, wenn getPeople()
während des Benchmarking aufgerufen wird.
Einige Apps verwenden bereits Fälschungen für Debug-Builds, um Back-End-Abhängigkeiten zu entfernen. Sie müssen jedoch für einen Build, der so nahe wie möglich am Release-Build liegt, ein Benchmarking durchführen. Im weiteren Verlauf dieses Dokuments wird eine Struktur mit mehreren Modulen und Varianten verwendet, wie unter Vollständige Projekteinrichtung beschrieben.
Es gibt drei Module:
benchmarkable
: enthält den Code für das Benchmarking.benchmark
: enthält den Benchmark-Code.app
: enthält den verbleibenden App-Code.
Jedes der vorherigen Module hat eine Build-Variante namens benchmark
sowie die üblichen debug
- und release
-Varianten.
Benchmarkmodul konfigurieren
Der Code für den fiktiven Netzwerkaufruf befindet sich im Quellsatz debug
des Moduls benchmarkable
und die vollständige Netzwerkimplementierung befindet sich im Quellsatz release
desselben Moduls. Die Asset-Datei mit den von der fiktiven Implementierung zurückgegebenen Daten befindet sich im Quellsatz debug
, um ein aufgeblähtes APK im release
-Build zu vermeiden. Die Variante benchmark
muss auf release
basieren und den Quellsatz debug
verwenden. Die Build-Konfiguration für die Variante benchmark
des Moduls benchmarkable
, das die fiktive Implementierung enthält, sieht so aus:
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")) } } }
Groovig
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'] } } }
Fügen Sie im Modul benchmark
einen benutzerdefinierten Test-Runner hinzu, der einen Application
für die auszuführenden Tests erstellt und Hilt unterstützt:
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); } }
Dadurch erweitert das Application
-Objekt, in dem die Tests ausgeführt werden, die Klasse HiltTestApplication
. Nehmen Sie die folgenden Änderungen an der Build-Konfiguration vor:
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) }
Groovig
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 }
Im vorherigen Beispiel wird Folgendes ausgeführt:
- Wendet die erforderlichen Gradle-Plug-ins auf den Build an.
- Gibt an, dass der benutzerdefinierte Test-Runner zum Ausführen der Tests verwendet wird.
- Gibt an, dass die Variante
benchmark
der Testtyp für dieses Modul ist. - Die Variante
benchmark
wird hinzugefügt. - Fügt die erforderlichen Abhängigkeiten hinzu.
Sie müssen testBuildType
ändern, damit Gradle die Aufgabe connectedBenchmarkAndroidTest
erstellt, die das Benchmarking ausführt.
Mikro-Benchmark erstellen
Die Benchmark wird so implementiert:
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; } }
Im vorherigen Beispiel werden Regeln sowohl für die Benchmark als auch für den Hilt erstellt.
benchmarkRule
führt das Timing der Benchmark aus. hiltRule
führt die Abhängigkeitsinjektion von der Benchmark-Testklasse aus. Sie müssen die Methode inject()
der Hilt-Regel in einer @Before
-Funktion aufrufen, um die Injektion durchzuführen, bevor Sie einzelne Tests ausführen.
Die Benchmark selbst pausiert das Timing, während der LiveData
-Beobachter registriert wird. Anschließend wird mit einem Latch gewartet, bis LiveData
aktualisiert wurde. Da die Sortierung in der Zeit zwischen dem Aufruf von peopleRepository.update()
und dem Zeitpunkt, an dem LiveData
aktualisiert wird, ausgeführt wird, wird die Dauer der Sortierung in die Benchmark-Zeitangabe einbezogen.
Mikrovergleich durchführen
Führen Sie die Benchmark mit ./gradlew :benchmark:connectedBenchmarkAndroidTest
aus, um sie über viele Iterationen hinweg durchzuführen und die Zeitdaten in Logcat auszugeben:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
Das vorherige Beispiel zeigt das Benchmark-Ergebnis zwischen 0,6 ms und 1,4 ms, um den Sortieralgorithmus für eine Liste von 1.000 Elementen auszuführen. Wenn Sie jedoch den Netzwerkaufruf in die Benchmark aufnehmen, ist die Abweichung zwischen den Iterationen größer als die Zeit, die die Sortierung selbst benötigt. Daher muss die Sortierung vom Netzwerkaufruf isoliert werden.
Sie können Code jederzeit refaktorieren, um das Sortieren leichter isoliert auszuführen. Wenn Sie jedoch bereits Hilt verwenden, können Sie stattdessen Fälschungen für das Benchmarking einschleusen.