De nombreuses applications utilisent Hilt pour injecter différents comportements dans différentes variantes de compilation. Cela peut être particulièrement utile lors du microbenchmarking de votre application, car il permet vous remplacez un composant qui peut fausser les résultats. Par exemple : l'extrait de code montre un dépôt qui récupère et trie une liste de noms:
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(); } }
Si vous incluez un appel réseau lors d'un benchmark, implémentez un faux appel réseau pour obtenir un résultat plus précis.
L'inclusion d'un appel réseau réel lors de l'analyse comparative rend plus difficile l'interprétation les résultats de l'analyse comparative. Les appels réseau peuvent être affectés par de nombreux facteurs externes, et leur durée peut varier d'une itération à l'autre de l'exécution du benchmark. La la durée des appels réseau peut être plus longue que le tri.
Implémenter un faux appel réseau à l'aide de Hilt
L'appel à dataSource.getPeople()
, comme illustré dans l'exemple précédent,
contient un appel réseau. Toutefois, l'instance NetworkDataSource
est injectée par Hilt, et vous pouvez la remplacer par la fausse implémentation suivante à des fins de benchmarking :
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; } }
Ce faux appel réseau est conçu pour s'exécuter aussi rapidement que possible lorsque vous appelez la méthode getPeople()
. Pour que Hilt puisse l'injecter, le fournisseur suivant est utilisé :
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); } }
Les données sont chargées à partir d'éléments à l'aide d'un appel d'E/S potentiellement de longueur variable.
Cependant, cela se fait lors de l'initialisation et ne cause aucune irrégularité
Lorsque getPeople()
est appelé lors de l'analyse comparative.
Certaines applications utilisent déjà des faux dans les builds de débogage pour supprimer toutes les dépendances du backend. Cependant, vous devez effectuer une analyse comparative sur un build aussi proche que possible du build possible. Le reste de ce document utilise une structure multimodule et multivariante, comme décrit dans la section Configuration complète du projet.
Il existe trois modules :
benchmarkable
: contient le code à comparer.benchmark
: contient le code du benchmark.app
: contient le reste du code de l'application.
Chacun des modules précédents possède une variante de compilation nommée benchmark
, ainsi que les variantes debug
et release
habituelles.
Configurer le module d'analyse comparative
Le code du faux appel réseau se trouve dans l'ensemble de sources debug
du
benchmarkable
, et l'implémentation complète du réseau se trouve dans release
l'ensemble de sources du même module. Le fichier d'éléments contenant les données renvoyées par
l'implémentation fictive se trouve dans l'ensemble de sources debug
pour éviter toute surcharge d'APK dans
la compilation release
. La variante benchmark
doit être basée sur release
et
utilisez l'ensemble de sources debug
. La configuration de compilation pour la variante benchmark
du module benchmarkable
contenant la fausse implémentation est la suivante :
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'] } } }
Dans le module benchmark
, ajoutez un exécuteur de test personnalisé qui crée un Application
dans lequel les tests s'exécutent et qui est compatible avec Hilt comme suit :
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); } }
L'objet Application
dans lequel les tests sont exécutés étend la classe HiltTestApplication
. Apportez les modifications suivantes à la configuration de compilation :
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 }
L'exemple précédent montre ce qui suit:
- Applique les plug-ins Gradle nécessaires à la compilation.
- Indique que le lanceur de test personnalisé est utilisé pour exécuter les tests.
- Indique que la variante
benchmark
est le type de test pour ce module. - Ajoute la variante
benchmark
. - Ajoute les dépendances requises.
Vous devez modifier testBuildType
pour vous assurer que Gradle crée la tâche connectedBenchmarkAndroidTest
, qui effectue l'analyse comparative.
Créer le microbenchmark
L'analyse comparative est implémentée comme suit :
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; } }
L'exemple précédent crée des règles pour l'analyse comparative et pour Hilt.
benchmarkRule
effectue le chronométrage du benchmark. hiltRule
effectue les
l'injection de dépendances sur la classe de test d'analyse comparative. Vous devez appeler la fonction
inject()
de la règle Hilt dans une fonction @Before
pour effectuer la
avant d'exécuter des tests individuels.
Le benchmark lui-même met en pause le chronométrage pendant l'enregistrement de l'observateur LiveData
. Ensuite, il utilise un loquet pour attendre que LiveData
soit mis à jour avant
à la fin du processus. Comme le tri est exécuté
dans la période entre le moment où
peopleRepository.update()
est appelé, et lorsque LiveData
reçoit une mise à jour,
la durée du tri est incluse
dans le temps d'analyse comparative.
Exécuter le microbenchmark
Exécutez le benchmark avec ./gradlew :benchmark:connectedBenchmarkAndroidTest
pour effectuer le benchmark sur de nombreuses itérations et pour imprimer les données de chronométrage dans Logcat :
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
L'exemple précédent montre que l'exécution de l'algorithme de tri sur une liste de 1 000 éléments prend entre 0,6 ms et 1,4 ms. Toutefois, si vous incluez l'appel réseau dans le benchmark, la variance entre les itérations est supérieure au temps d'exécution du tri lui-même. Il est donc nécessaire d'isoler le tri de l'appel réseau.
Vous pouvez toujours refactoriser le code pour faciliter l'exécution du tri mais si vous utilisez déjà Hilt, vous pouvez l'utiliser pour injecter des données fictives l'analyse comparative.