Microbenchmark และ Hilt

แอปจำนวนมากใช้ Hilt เพื่อแทรกลักษณะการทำงานที่แตกต่างกันลงในตัวแปรของบิลด์ที่แตกต่างกัน ซึ่งจะเป็นประโยชน์อย่างยิ่งเมื่อทำการเปรียบเทียบแบบไมโครกับแอปของคุณ เนื่องจากจะทำให้ คุณจะสลับส่วนประกอบที่สามารถบิดเบือนผลลัพธ์ได้ ตัวอย่างเช่น URL ต่อไปนี้ ข้อมูลโค้ดจะแสดงที่เก็บซึ่งดึงข้อมูลและจัดเรียงรายชื่อดังนี้

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

ข้อมูลโหลดจากชิ้นงานโดยใช้การเรียก I/O ที่มีความยาวแปรผัน อย่างไรก็ตาม การดำเนินการนี้จะทำระหว่างการเริ่มต้นและจะไม่ก่อให้เกิดความผิดปกติใดๆ เมื่อมีการเรียก getPeople() ระหว่างการเปรียบเทียบ

แอปบางแอปใช้ของปลอมอยู่แล้วในเวอร์ชันการแก้ไขข้อบกพร่องเพื่อนำทรัพยากร Dependency ของแบ็กเอนด์ออก อย่างไรก็ตาม คุณจำเป็นต้องเปรียบเทียบบิลด์ที่ใกล้เคียงกับบิลด์ เท่าที่จะเป็นไปได้ ส่วนที่เหลือของเอกสารนี้ใช้โครงสร้างหลายโมดูลและหลายตัวแปร ตามที่อธิบายไว้ในการตั้งค่าโปรเจ็กต์แบบเต็ม

มี 3 โมดูล ดังนี้

  • 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
  • เพิ่มทรัพยากร Dependency ที่จำเป็น

คุณต้องเปลี่ยน 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 ดำเนินการ Dependency Injection ในคลาสทดสอบการเปรียบเทียบ คุณจะต้องเรียกใช้ inject() ของกฎ Hilt ในฟังก์ชัน @Before เพื่อดำเนินการ ก่อนดำเนินการทดสอบแต่ละครั้ง

ตัวเปรียบเทียบจะหยุดช่วงเวลาชั่วคราวในขณะที่ผู้สังเกตการณ์ LiveData ที่ลงทะเบียนแล้ว จากนั้นจะใช้สลักเพื่อรอจนกว่าจะอัปเดต LiveData แล้ว จนเสร็จสิ้น เมื่อการจัดเรียงทํางานในช่วงเวลาต่อไปนี้ ระบบจะเรียกใช้ peopleRepository.update() และเมื่อ LiveData ได้รับการอัปเดต ระยะเวลาของการจัดเรียงจะรวมอยู่ในเวลาเปรียบเทียบ

เรียกใช้ Microbenchmark

ทำการเปรียบเทียบกับ ./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 อยู่แล้ว คุณก็สามารถใช้ Hilt เพื่อแทรกข้อมูลปลอมสำหรับ การเปรียบเทียบแทน