Microbenchmark และ 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() ดังที่แสดงในตัวอย่างก่อนหน้านี้มีคำเรียกใช้เครือข่าย อย่างไรก็ตาม Hilt จะแทรกอินสแตนซ์ NetworkDataSource และคุณสามารถแทนที่ด้วยการติดตั้งใช้งานจำลองต่อไปนี้เพื่อทำการเปรียบเทียบ

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

Groovy

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 จะทำการแทรก Dependency ในคลาสทดสอบการเปรียบเทียบ คุณต้องเรียกใช้เมธอด 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 อยู่แล้ว คุณก็ใช้เพื่อแทรกข้อมูลจำลองสำหรับการเปรียบเทียบประสิทธิภาพแทนได้