مقياس الأداء الصغرى وأداة 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 مللي ثانية لتشغيل خوارزمية الفرز في قائمة مكونة من 1000 عنصر. ومع ذلك، إذا قمت بتضمين استدعاء الشبكة في المعيار، فإن التباين بين التكرارات يكون أكبر من الوقت الذي يستغرقه الفرز نفسه للتشغيل، ومن ثم الحاجة إلى عزل الفرز من استدعاء الشبكة.

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