مقياس الأداء الصغرى وأداة Hilt

تستخدم العديد من التطبيقات 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);
    }
}

يتم تحميل البيانات من مواد العرض باستخدام طلب إدخال/إخراج قد يكون له طول متغيّر. ومع ذلك، يتم إجراء ذلك أثناء الإعداد ولن يؤدي إلى أي مخالفات عند استدعاء getPeople() أثناء قياس الأداء.

تستخدم بعض التطبيقات بالفعل بيانات وهمية في إصدارات تصحيح الأخطاء لإزالة أي تبعيات على الخلفية. ومع ذلك، عليك إجراء قياس الأداء على بنية أقرب ما يمكن إلى بنية الإصدار. يستخدم باقي هذا المستند بنية متعددة الوحدات ومتعددة التصاميم المتنوعة كما هو موضّح في إعداد المشروع الكامل.

هناك ثلاث وحدات:

  • benchmarkable: تحتوي على الرمز البرمجي لقياس الأداء.
  • benchmark: تحتوي على رمز قياس الأداء.
  • app: تحتوي على رمز التطبيق المتبقي.

تحتوي كل وحدة من الوحدات السابقة على تصميم متنوع باسم benchmark بالإضافة إلى التصميمَين المتنوعَين المعتادَين debug وrelease.

ضبط وحدة قياس الأداء

يوجد رمز الطلب الوهمي على الشبكة في مجموعة رموز المصدر debug التابعة لوحدة benchmarkable، ويوجد تنفيذ الشبكة الكامل في مجموعة رموز المصدر release التابعة للوحدة نفسها. يوجد ملف مواد العرض الذي يحتوي على البيانات التي يعرضها التنفيذ الوهمي في مجموعة رموز المصدر debug لتجنُّب أي تضخّم في ملف APK في إصدار release. يجب أن يستند التصميم المتنوع benchmark إلى release وأن يستخدم مجموعة رموز المصدر debug. في ما يلي إعدادات التصميم للتصميم المتنوع benchmark التابع لوحدة benchmarkable الذي يحتوي على التنفيذ الوهمي:

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

أنيق

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، أضِف برنامجًا مخصّصًا لتشغيل الاختبارات ينشئ Application لتشغيل الاختبارات فيه، ما يتيح استخدام Hilt على النحو التالي:

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

أنيق

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.
  • يضيف التبعيات المطلوبة.

عليك تغيير testBuildType للتأكّد من أنّ Gradle ينشئ مهمة connectedBenchmarkAndroidTest التي تجري قياس الأداء.

إنشاء قياس الأداء الدقيق

يتم تنفيذ قياس الأداء على النحو التالي:

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 عملية إدخال التبعية في فئة اختبار قياس الأداء. عليك استدعاء طريقة inject() لقاعدة Hilt في دالة @Before لإجراء عملية الإدخال قبل تشغيل أي اختبارات فردية.

يتوقف قياس الأداء مؤقتًا عن تحديد الوقت أثناء تسجيل أداة مراقبة 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...

يعرض المثال السابق نتيجة قياس الأداء بين 0.6 ملي ثانية و1.4 ملي ثانية لتشغيل خوارزمية الفرز على قائمة تضم 1,000 عنصر. ومع ذلك، إذا تضمّن قياس الأداء الطلب على الشبكة، سيكون التباين بين التكرارات أكبر من الوقت الذي يستغرقه الفرز نفسه، لذا يجب عزل الفرز عن الطلب على الشبكة.

يمكنك دائمًا إعادة تصميم الرمز البرمجي لتسهيل تشغيل الفرز بشكل منفصل، ولكن إذا كنت تستخدم Hilt، يمكنك استخدامه لإدخال بيانات وهمية لقياس الأداء بدلاً من ذلك.