Microbenchmark و Hilt

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

کاتلین

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

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

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

با استفاده از 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() در حین بنچمارک، بی‌نظمی ایجاد نمی‌کند.

برخی از برنامه‌ها در حال حاضر از تقلبی در ساخت‌های اشکال‌زدایی استفاده می‌کنند تا وابستگی‌های Backend را حذف کنند. با این حال، باید بنچمارک را در یک بیلد تا حد امکان نزدیک به نسخه منتشر کنید. بقیه این سند از ساختار چند ماژول و چند متغیره همانطور که در راه اندازی کامل پروژه توضیح داده شده است استفاده می کند.

سه ماژول وجود دارد:

  • 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 لازم را روی بیلد اعمال می کند.
  • مشخص می کند که برای اجرای تست ها از تست runner سفارشی استفاده می شود.
  • مشخص می کند که نوع 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...

مثال قبلی نتیجه معیار را بین 0.6 میلی‌ثانیه تا 1.4 میلی‌ثانیه نشان می‌دهد تا الگوریتم مرتب‌سازی را در فهرستی از 1000 مورد اجرا کند. با این حال، اگر تماس شبکه را در معیار قرار دهید، واریانس بین تکرارها بیشتر از زمانی است که خود مرتب‌سازی برای اجرا می‌گذرد، بنابراین نیاز به جداسازی مرتب‌سازی از تماس شبکه است.

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