MicroBenchmark und Hilt

Viele Apps nutzen Hilt, um verschiedenen Build-Varianten unterschiedliches Verhalten zu verleihen. Dies ist besonders beim Micro-Benchmarking Ihrer App nützlich, tauschen Sie eine Komponente aus, die die Ergebnisse verzerren kann. Beispiel: Das Code-Snippet zeigt 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 verwenden, implementieren Sie einen unechten Netzwerkaufruf um ein genaueres Ergebnis zu erhalten.

Die Einbeziehung eines echten Netzwerkaufrufs beim Benchmarking erschwert die Interpretation Benchmark-Ergebnisse. Netzwerkaufrufe können von vielen externen Faktoren beeinflusst werden. ihre Dauer zwischen den Iterationen der Benchmarkausführung variieren kann. Die von Netzwerkaufrufen kann länger dauern als das Sortieren.

Gefälschten Netzwerkaufruf mit Hilt implementieren

Durch den Aufruf von dataSource.getPeople(), wie im vorherigen Beispiel gezeigt, enthält einen Netzwerkaufruf. Die Instanz NetworkDataSource wird jedoch eingeschleust. von Hilt und Sie können sie durch die folgende fiktive Implementierung für 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;
    }
}

Dieser gefälschte Netzwerkaufruf wird so schnell wie möglich ausgeführt, wenn du die Methode getPeople(). Damit Hilt dies einschleusen kann, ist Folgendes erforderlich: Anbieter:

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 geschieht jedoch während der Initialisierung und verursacht keine Unregelmäßigkeiten. wenn getPeople() während des Benchmarking aufgerufen wird.

Einige Apps verwenden bereits Fälschungen in Debug-Builds, um Backend-Abhängigkeiten zu entfernen. Sie müssen jedoch ein Benchmarking für einen Build durchführen, der sich so nah wie möglich am Release-Build befindet wie möglich. Im weiteren Verlauf dieses Dokuments wird eine Struktur mit mehreren Modulen und Varianten verwendet. enthalten, 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 mit dem Namen benchmark sowie die üblichen Varianten debug und release.

Benchmark-Modul konfigurieren

Der Code für den gefälschten Netzwerkaufruf befindet sich im Quellsatz debug der Modul benchmarkable und die vollständige Netzwerkimplementierung befindet sich in der release Quell-Satz desselben Moduls. Die Asset-Datei mit den von Die gefälschte Implementierung befindet sich in der debug-Quelle, um eine Überlastung des APK zu vermeiden. den release-Build. Die Variante benchmark muss auf release und Verwenden Sie den Quellsatz debug. Die Build-Konfiguration für die Variante benchmark des benchmarkable-Moduls mit der fiktiven Implementierung 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"))
        }
    }
}

Cool

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 erstellt. damit die Tests ausgeführt werden, die Hilt unterstützen:

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 am Build vor Konfiguration:

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)
}

Cool

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.
  • Fügt die Variante benchmark hinzu.
  • Fügt die erforderlichen Abhängigkeiten hinzu.

Sie müssen die testBuildType ändern, damit Gradle das Feld connectedBenchmarkAndroidTest-Aufgabe, die das Benchmarking durchführt.

MicroBenchmark 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 den Benchmark als auch für „Hilt“ erstellt. benchmarkRule führt den Zeitpunkt der Benchmark aus. hiltRule führt die Abhängigkeitsinjektion für die Benchmark-Testklasse. Sie müssen die Methode inject()-Methode der Hilt-Regel in einer @Before-Funktion zur Durchführung der die Injektion, bevor Sie einzelne Tests ausführen.

Die Benchmark selbst pausiert das Timing, während der LiveData-Beobachter registriert. Dann wird mit einem Latch gewartet, bis LiveData aktualisiert wurde. . Da die Sortierung in der Zeit zwischen peopleRepository.update() wird aufgerufen. Wenn LiveData ein Update erhält, wird die Dauer der Sortierung im Benchmark-Timing berücksichtigt.

MicroBenchmark ausführen

Benchmark mit ./gradlew :benchmark:connectedBenchmarkAndroidTest ausführen um die Benchmark über viele Iterationen hinweg durchzuführen und die Zeitdaten Logcat:

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 für die Ausführung Sortieralgorithmus für eine Liste von 1.000 Elementen. Wenn Sie jedoch das Tag Netzwerkaufruf in der Benchmark, ist die Abweichung zwischen den Iterationen größer als die Zeit, die der Sortiervorgang selbst für die Ausführung benötigt. Daher ist es notwendig, aus dem Netzwerkaufruf sortieren.

Sie können Code jederzeit refaktorieren, um das Sortieren in Wenn Sie Hilt bereits nutzen, können Sie damit Fälschungen Benchmarking durchgeführt.