ریزسنجش و Hilt

بسیاری از برنامه‌ها از Hilt برای تزریق رفتارهای مختلف به تنوع‌های ساخت مختلف استفاده می‌کنند. این کار به‌ویژه هنگام «ریز محک‌زنی» برنامه‌تان می‌تواند مفید باشد زیرا به شما امکان می‌دهد مؤلفه‌ای را که می‌تواند نتایج را منحرف کند تغییر دهید. برای مثال، تکه‌کد زیر مخزنی را نشان می‌دهد که فهرستی از نام‌ها را واکشی و مرتب می‌کند:

کاتلین

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

جاوا

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

اگر هنگام محک‌زنی، تماس شبکه‌ای را لحاظ می‌کنید، تماس شبکه‌ای جعلی را پیاده‌سازی کنید تا نتیجه دقیق‌تری دریافت کنید.

گنجاندن تماس شبکه واقعی هنگام محک‌زنی، تفسیر نتایج محک را دشوارتر می‌کند. فراخوانی‌های شبکه می‌توانند تحت تأثیر عوامل خارجی زیادی قرار گیرند و مدت‌زمان آن‌ها می‌تواند بین تکرارهای اجرای محک متفاوت باشد. مدت تماس‌های شبکه می‌تواند بیشتر از مرتب‌سازی طول بکشد.

Implement a fake network call using Hilt

فراخوانی dataSource.getPeople()، همان‌طور که در مثال قبلی نشان داده شده است، شامل فراخوانی شبکه است. بااین‌حال، نمونه NetworkDataSource توسط Hilt تزریق می‌شود و می‌توانید آن را با پیاده‌سازی جعلی زیر برای محک‌زنی جایگزین کنید:

کاتلین

class FakeNetworkDataSource @Inject constructor(
    private val people: List<Person>
) : NetworkDataSource {
    override fun getPeople(): List<Person> = people
}

جاوا

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 بتواند این را تزریق کند، از ارائه‌دهنده زیر استفاده می‌شود:

کاتلین

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

جاوا

@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 حاوی پیاده‌سازی جعلی به‌صورت زیر است:

کاتلین

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 به‌صورت زیر پشتیبانی کند:

کاتلین

class HiltBenchmarkRunner : AndroidBenchmarkRunner() {

    override fun newApplication(
        cl: ClassLoader?,
        className: String?,
        context: Context?
    ): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

جاوا

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 را گسترش دهد. تغییرات زیر را در پیکربندی ساخت ایجاد کنید:

کاتلین

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 را ایجاد می‌کند که محک‌زنی را انجام می‌دهد.

ایجاد ریز محک

محک‌زنی به‌صورت زیر اجرا می‌شود:

کاتلین

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

جاوا

@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...

مثال قبلی نتیجه محک را بین ۰٫۶ میلی‌ثانیه و ۱٫۴ میلی‌ثانیه برای اجرای الگوریتم مرتب‌سازی در فهرستی از ۱۰۰۰ مورد نشان می‌دهد. اما اگر فراخوانی شبکه را در محک بگنجانید، واریانس بین تکرارها بیشتر از زمانی است که خود مرتب‌سازی برای اجرا نیاز دارد، بنابراین لازم است مرتب‌سازی را از فراخوانی شبکه جدا کنید.

همیشه می‌توانید کد را بازسازی کنید تا اجرای مرتب‌سازی به‌صورت مجزا آسان‌تر شود، اما اگر ازقبل از Hilt استفاده می‌کنید، می‌توانید از آن برای تزریق داده‌های جعلی برای محک‌زنی استفاده کنید.