אפליקציות רבות משתמשות ב-Hilt כדי להחדיר התנהגויות שונות לגרסאות build שונות. האפשרות הזו יכולה להיות שימושית במיוחד כשמבצעים בדיקות ביצועים ברמת המיקרו באפליקציה, כי היא מאפשרת להחליף רכיב שעלול להטות את התוצאות. לדוגמה, קטע הקוד הבא מציג מאגר שמאחזר רשימת שמות וממיין אותה:
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()
במהלך בדיקת הביצועים.
באפליקציות מסוימות כבר נעשה שימוש ב-fakes בגרסאות build לניפוי באגים כדי להסיר יחסי תלות לקצה העורפי. עם זאת, צריך לבצע את בדיקת הביצועים בגרסה של ה-build שהכי קרובה לגרסת ה-release. בהמשך המסמך נעשה שימוש במבנה של כמה מודולים עם כמה וריאנטים, כפי שמתואר בקטע הגדרת פרויקט מלאה.
יש שלושה מודולים:
benchmarkable
: מכיל את הקוד לבדיקה.benchmark
: מכיל את קוד ההשוואה.app
: מכיל את שאר קוד האפליקציה.
לכל אחד מהמודולים הקודמים יש וריאנט build בשם benchmark
, לצד הווריאנטים הרגילים debug
ו-release
.
הגדרת מודול מדדי הביצועים
הקוד של קריאת הרשת המזויפת נמצא בקבוצת המקור debug
של המודול benchmarkable
, וההטמעה המלאה של הרשת נמצאת בקבוצת המקור release
של אותו מודול. קובץ הנכס שמכיל את הנתונים שהוחזרו על ידי ההטמעה המזויפת נמצא בקבוצת המקור debug
כדי למנוע התנפחויות של קובצי APK בגרסה ה-build של release
. הווריאנט benchmark
צריך להתבסס על release
ולהשתמש בקבוצת המקור debug
. תצורת ה-build של הווריאנט 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")) } } }
Groovy
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
. מבצעים את השינויים הבאים בהגדרת ה-build:
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 על ה-build.
- מציין שהכלי להרצת בדיקות בהתאמה אישית משמש להרצת הבדיקות.
- מציין שהוריאנט
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
רשום. לאחר מכן, הוא משתמש ב-latch כדי להמתין עד לעדכון של 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.6ms ל-1.4ms להרצת אלגוריתם המיון ברשימת 1,000 פריטים. עם זאת, אם תכללו את קריאת הרשת בבדיקת הביצועים, השונות בין החזרות (iterations) תהיה גדולה יותר מהזמן שלוקח למיון לפעול בעצמו, ולכן צריך לבודד את המיון מקריאת הרשת.
תמיד אפשר לבצע רפאקציה של הקוד כדי שיהיה קל יותר להריץ את המיון בנפרד, אבל אם אתם כבר משתמשים ב-Hilt, תוכלו להשתמש בו כדי להחדיר ערכים מזויפים לצורך בדיקת ביצועים במקום זאת.