Molte app utilizzano Hilt per inserire comportamenti diversi per varianti della build. Questo può essere particolarmente utile quando esegui il Microbenchmarking della tua app perché ti consente si sostituisce un componente che può disallineare i risultati. Ad esempio, lo snippet di codice seguente mostra un repository che recupera e ordina un elenco di nomi:
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(); } }
Se includi una chiamata di rete durante il benchmarking, implementa una chiamata di rete falsa per ottenere un risultato più preciso.
L'inclusione di una chiamata di rete reale durante il benchmarking rende più difficile interpretare i risultati del benchmarking. Le chiamate di rete possono essere influenzate da molti fattori esterni e la loro durata può variare a seconda dell'esecuzione del benchmark. La delle chiamate di rete può richiedere più tempo rispetto all'ordinamento.
Implementare una chiamata di rete falsa utilizzando Hilt
La chiamata a dataSource.getPeople()
, come mostrato nell'esempio precedente,
contiene una chiamata di rete. Tuttavia, l'istanza NetworkDataSource
viene iniettata da Hilt e puoi sostituirla con la seguente implementazione falsa per il 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; } }
Questa chiamata di rete falsa è progettata per essere eseguita il più rapidamente possibile quando chiami
il metodo getPeople()
. Affinché Hilt possa iniettare questo, viene utilizzato il seguente fornitore:
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 dati vengono caricati dagli asset utilizzando una chiamata I/O di durata potenzialmente variabile.
Tuttavia, questa operazione viene eseguita durante l'inizializzazione e non causerà irregolarità.
quando getPeople()
viene chiamato durante il benchmarking.
Alcune app utilizzano già falsi nelle build di debug per rimuovere eventuali dipendenze dal backend. Devi però fare il benchmark per una build il più vicina possibile a quella della release. possibile. Il resto del documento utilizza una struttura multimodulo e multivariante come descritto in Configurazione completa del progetto.
Sono disponibili tre moduli:
benchmarkable
: contiene il codice da confrontare.benchmark
: contiene il codice benchmark.app
: contiene il codice dell'app rimanente.
Ciascuno dei moduli precedenti ha una variante di build denominata benchmark
insieme a
le solite varianti debug
e release
.
Configurare il modulo di benchmark
Il codice della chiamata di rete falsa si trova nel set di origine debug
della
benchmarkable
e l'implementazione completa della rete è nel release
insieme di origini dello stesso modulo. Il file di asset contenente i dati restituiti da
la falsa implementazione è nell'origine debug
impostata per evitare sovraccarichi dell'APK
la build release
. La variante benchmark
deve essere basata su release
e
utilizza il set di origini debug
. La configurazione di compilazione per la variante benchmark
del modulo benchmarkable
contenente l'implementazione falsa è la seguente:
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")) } } }
Alla moda
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'] } } }
Nel modulo benchmark
, aggiungi un runner di test personalizzato che crei un Application
per l'esecuzione dei test che supporta Hilt come segue:
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); } }
Di conseguenza, l'oggetto Application
in cui vengono eseguiti i test estende la
HiltTestApplication
. Apporta le seguenti modifiche alla configurazione della compilazione:
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'esempio precedente esegue le seguenti operazioni:
- Applica i plug-in Gradle necessari alla build.
- Specifica che viene utilizzato l'esecutore del test personalizzato per eseguire i test.
- Specifica che la variante
benchmark
è il tipo di test per questo modulo. - Aggiunge la variante
benchmark
. - Aggiunge le dipendenze richieste.
Devi modificare testBuildType
per assicurarti che Gradle crei l'attività connectedBenchmarkAndroidTest
, che esegue il benchmarking.
Crea il microbenchmark
Il benchmark viene implementato nel seguente modo:
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'esempio precedente crea regole sia per il benchmark sia per Hilt.
benchmarkRule
esegue la temporizzazione del benchmark. hiltRule
esegue l'iniezione di dipendenza nella classe di test di benchmark. Devi richiamare il metodo
Metodo inject()
della regola Hilt in una funzione @Before
per eseguire il
prima di eseguire singoli test.
Il benchmark stesso mette in pausa il conteggio del tempo mentre l'osservatore LiveData
è registrato. Quindi utilizza un fermo per attendere l'aggiornamento di LiveData
prima
per terminare. Poiché l'ordinamento viene eseguito nell'intervallo tra il momento in cui
peopleRepository.update()
viene chiamato e quando LiveData
riceve un aggiornamento,
la durata dell'ordinamento è inclusa nei tempi di benchmark.
Esegui il microbenchmark
Esegui il benchmark con ./gradlew :benchmark:connectedBenchmarkAndroidTest
eseguire il benchmark in molte iterazioni e stampare i dati dei tempi
Logcat:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
L'esempio precedente mostra il risultato del benchmark compreso tra 0,6 ms e 1,4 ms per l'esecuzione dell'algoritmo di ordinamento su un elenco di 1000 elementi. Tuttavia, se includi la chiamata di rete nel benchmark, la varianza tra le iterazioni è maggiore del tempo necessario per l'esecuzione dell'ordinamento stesso, da qui la necessità di isolare l'ordinamento dalla chiamata di rete.
Puoi sempre eseguire il refactoring del codice per semplificare l'esecuzione dell'ordinamento in isolamento, ma se utilizzi già Hilt, puoi utilizzarlo per iniettare dati falsi per il benchmarking.