Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Барлық пайдаланушылар мобильді қосымшаларда жылдам іске қосу және жауап беретін UI қабылдайды. Қолданбаның іске қосылуы ұзақ уақыт алса, пайдаланушы мұңайып, ашулана бастайды. Сіз тұтынушы тәжірибесін оңай бұза аласыз немесе пайдаланушы қолданбаны қолдана бастағанға дейін оны толығымен жоғалта аласыз.

Бірде біз Dodo Pizza қолданбасын іске қосу үшін орта есеппен 3 секунд, ал кейбір «бақыттылар» үшін 15-20 секунд қажет екенін анықтадық.

Кесектің астында бақытты аяқталатын оқиға бар: Realm дерекқорының өсуі, жадтың ағып кетуі, біз кірістірілген нысандарды қалай жинақтағанымыз, содан кейін өзімізді біріктіріп, барлығын жөндегеніміз туралы.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі
Мақала авторы: Максим Качинкин — Dodo Pizza компаниясында Android әзірлеушісі.

Қолданба белгішесін басқаннан бастап бірінші әрекеттің onResume() функциясына дейінгі үш секунд - шексіздік. Ал кейбір пайдаланушылар үшін іске қосу уақыты 15-20 секундқа жетті. Бұл қалай мүмкін?

Оқуға уақыты жоқтар үшін өте қысқа түйіндеме
Біздің Realm дерекқорымыз шексіз өсті. Кейбір кірістірілген нысандар жойылмады, бірақ үнемі жинақталады. Қолданбаны іске қосу уақыты біртіндеп артты. Содан кейін біз оны түзеттік және іске қосу уақыты мақсатқа жетті - ол 1 секундтан аз болды және бұдан былай өспейді. Мақалада жағдайды талдау және екі шешім бар - жылдам және қалыпты.

Мәселені іздеу және талдау

Бүгінгі таңда кез келген мобильді қосымша тез іске қосылуы және жауап беруі керек. Бірақ бұл тек мобильді қосымшаға қатысты емес. Пайдаланушының қызметпен және компаниямен өзара әрекеттесу тәжірибесі күрделі нәрсе. Мысалы, біздің жағдайда жеткізу жылдамдығы пицца қызметінің негізгі көрсеткіштерінің бірі болып табылады. Жеткізу жылдам болса, пицца ыстық болады, ал қазір жегісі келетін тұтынушы көп күтпейді. Қолданба үшін, өз кезегінде, жылдам қызмет көрсету сезімін тудыру маңызды, өйткені егер қолданбаны іске қосу үшін бар болғаны 20 секунд қажет болса, онда пиццаны қанша күту керек?

Бастапқыда біз кейде қосымшаны іске қосуға бірнеше секунд кететіндігімен бетпе-бет келдік, содан кейін басқа әріптестерден оның қанша уақытқа созылғаны туралы шағымдарды ести бастадық. Бірақ біз бұл жағдайды дәйекті түрде қайталай алмадық.

Қанша уақыт? Сәйкес Google құжаттамасы, егер қолданбаның салқын басталуы 5 секундтан аз уақытты алса, бұл "қалыпты" деп саналады. Dodo Pizza Android қолданбасы іске қосылды (Firebase көрсеткіштеріне сәйкес _қолданбаны_бастау) сағ суық бастау орта есеппен 3 секундта - олар айтқандай, «Тамаша емес, қорқынышты емес».

Бірақ содан кейін қосымшаны іске қосу өте, өте, өте ұзақ уақытты алды деген шағымдар пайда бола бастады! Алдымен біз «өте, өте, өте ұзақ» дегенді өлшеуді шештік. Біз бұл үшін Firebase ізін қолдандық Қолданбаның басталу ізі.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Бұл стандартты жол пайдаланушы қолданбаны ашқан сәт пен бірінші әрекеттің onResume() орындалу сәті арасындағы уақытты өлшейді. Firebase консолінде бұл көрсеткіш _app_start деп аталады. Анықталғандай:

  • Орташа суық іске қосу уақыты 95 секундтан аз болғанымен, 20-процентильден жоғары пайдаланушылар үшін іске қосу уақыты шамамен 5 секундты құрайды (кейбіреулері одан да ұзақ).
  • Іске қосу уақыты тұрақты мән емес, уақыт өте келе өседі. Бірақ кейде тамшылар болады. Біз бұл үлгіні талдау ауқымын 90 күнге дейін ұлғайтқанда таптық.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Басыма екі ой келді:

  1. Бірдеңе ағып жатыр.
  2. Бұл «бірдеңе» шығарылғаннан кейін қалпына келтіріледі, содан кейін қайтадан ағып кетеді.

«Мүмкін, дерекқорға қатысты бірдеңе шығар» деп ойладық және біз дұрыс болдық. Біріншіден, біз дерекқорды кэш ретінде пайдаланамыз, тасымалдау кезінде оны тазартамыз. Екіншіден, дерекқор қолданба іске қосылғанда жүктеледі. Барлығы бір-біріне сәйкес келеді.

Realm дерекқорында не дұрыс емес?

Біз дерекқордың мазмұны қолданбаның қызмет ету мерзімі ішінде, бірінші орнатудан бастап және одан әрі белсенді пайдалану кезінде қалай өзгеретінін тексере бастадық. Realm дерекқорының мазмұнын арқылы көре аласыз Стето немесе файлды ашу арқылы толығырақ және анық Realm Studio. Чтобы посмотреть содержимое базы через ADB, копируем файл базы Realm:

adb exec-out run-as ${PACKAGE_NAME} cat files/${DB_NAME}

Әр түрлі уақытта мәліметтер қорының мазмұнын қарастыра отырып, біз белгілі бір типтегі объектілердің саны үнемі өсіп келе жатқанын білдік.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі
Суретте екі файлға арналған Realm Studio фрагменті көрсетілген: сол жақта - орнатудан біраз уақыттан кейін қолданба базасы, оң жақта - белсенді пайдаланудан кейін. Объектілердің саны көп екенін көруге болады ImageEntity и MoneyType айтарлықтай өсті (скриншот әр түрдегі нысандардың санын көрсетеді).

Дерекқордың өсуі мен іске қосу уақыты арасындағы байланыс

Дерекқордың бақыланбайтын өсуі өте нашар. Бірақ бұл қолданбаны іске қосу уақытына қалай әсер етеді? Мұны ActivityManager арқылы өлшеу өте оңай. Android 4.4 нұсқасынан бастап logcat журналды Displayed жолымен және уақытпен көрсетеді. Бұл уақыт қолданба іске қосылған сәттен бастап әрекетті көрсетудің соңына дейінгі аралыққа тең. Осы уақыт ішінде келесі оқиғалар орын алады:

  • Процесті бастаңыз.
  • Объектілерді инициализациялау.
  • Іс-әрекеттерді құру және инициализациялау.
  • Макет құру.
  • Қолданбаны көрсету.

Бізге жарасады. АДБ-ны -S және -W жалауларымен іске қоссаңыз, іске қосу уақытымен ұзартылған нәтиже алуға болады:

adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN

Егер сіз оны сол жерден ұстасаңыз grep -i WaitTime уақытта осы метриканы жинауды автоматтандыруға және нәтижелерді көрнекі түрде қарауға болады. Төмендегі график қолданбаны іске қосу уақытының қолданбаны суық іске қосу санына тәуелділігін көрсетеді.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Сонымен бірге деректер қорының көлемі мен өсуі арасындағы байланыстың бірдей сипаты болды, ол 4 МБ-тан 15 МБ-қа дейін өсті. Жалпы алғанда, уақыт өте келе (суық старттардың өсуімен) қолданбаны іске қосу уақыты да, дерекқордың көлемі де өсті. Біздің қолымызда гипотеза бар. Енді тәуелділікті растау ғана қалды. Сондықтан біз «ағып кетуді» жоюды және бұл ұшыруды тездететінін білуді шештік.

Дерекқордың шексіз өсуінің себептері

«Ағып кетуді» жоймас бұрын, олардың бірінші кезекте неліктен пайда болғанын түсіну керек. Ол үшін Realm деген не екенін еске түсірейік.

Realm – реляциялық емес мәліметтер базасы. Ол Android жүйесіндегі қанша ORM реляциялық дерекқорлары сипатталғанына ұқсас нысандар арасындағы қатынастарды сипаттауға мүмкіндік береді. Сонымен қатар, Realm нысандарды ең аз түрлендірулер мен салыстырулармен тікелей жадта сақтайды. Бұл сізге дискіден деректерді өте жылдам оқуға мүмкіндік береді, бұл Realm күші және оны неліктен жақсы көреді.

(Осы мақаланың мақсаттары үшін бұл сипаттама бізге жеткілікті болады. Сіз Realm туралы көбірек оқи аласыз құжаттама немесе олардың ішінде академиясы).

Көптеген әзірлеушілер реляциялық дерекқорлармен көбірек жұмыс істеуге дағдыланған (мысалы, SQL-пен ORM дерекқорлары қақпақ астында). Және деректерді каскадты жою сияқты нәрселер жиі берілген сияқты көрінеді. Бірақ патшалықта емес.

Айтпақшы, каскадты жою мүмкіндігі ұзақ уақыт бойы сұралды. Бұл қайта қарау и басқа, онымен байланысты, белсенді түрде талқыланды. Жақында орындалатын шығар деген сезім болды. Бірақ содан кейін бәрі күшті және әлсіз сілтемелерді енгізуге аударылды, бұл да бұл мәселені автоматты түрде шешеді. Бұл тапсырмада өте белсенді және белсенді болды сұрауды тарту, ол қазір ішкі қиындықтарға байланысты уақытша тоқтатылды.

Каскадты жоюсыз деректердің ағуы

Егер жоқ каскадты жоюға сенсеңіз, деректер қалай ағып кетеді? Егер сізде кірістірілген Realm нысандары болса, оларды жою керек.
Нақты (дерлік) мысалды қарастырайық. Бізде объект бар CartItemEntity:

@RealmClass
class CartItemEntity(
 @PrimaryKey
 override var id: String? = null,
 ...
 var name: String = "",
 var description: String = "",
 var image: ImageEntity? = null,
 var category: String = MENU_CATEGORY_UNKNOWN_ID,
 var customizationEntity: CustomizationEntity? = null,
 var cartComboProducts: RealmList<CartProductEntity> = RealmList(),
 ...
) : RealmObject()

Себеттегі өнімнің әртүрлі өрістері бар, оның ішінде суреті де бар ImageEntity, теңшелген ингредиенттер CustomizationEntity. Сондай-ақ, арбадағы өнім өз өнімдерінің жиынтығы бар комбинация болуы мүмкін RealmList (CartProductEntity). Барлық тізімделген өрістер аймақ нысандары болып табылады. Егер біз бірдей идентификатормен жаңа нысанды (copyToRealm() / copyToRealmOrUpdate()) енгізсек, онда бұл нысан толығымен қайта жазылады. Бірақ барлық ішкі нысандар (сурет, customizationEntity және cartComboProducts) ата-анамен байланысын жоғалтады және дерекқорда қалады.

Олармен байланыс үзілгендіктен, біз енді оларды оқымаймыз немесе жоймаймыз (егер біз оларға нақты қол жеткізбесе немесе «кестені» толығымен тазаламайынша). Біз мұны «жадтың ағып кетуі» деп атадық.

Біз Realm-пен жұмыс істегенде, біз барлық элементтерді нақты қарап шығуымыз керек және мұндай әрекеттердің алдында барлығын анық жоюымыз керек. Мұны, мысалы, келесідей жасауға болады:

val entity = realm.where(CartItemEntity::class.java).equalTo("id", id).findFirst()
if (first != null) {
 deleteFromRealm(first.image)
 deleteFromRealm(first.customizationEntity)
 for(cartProductEntity in first.cartComboProducts) {
   deleteFromRealm(cartProductEntity)
 }
 first.deleteFromRealm()
}
// и потом уже сохраняем

Егер сіз мұны жасасаңыз, онда бәрі қажет болғандай жұмыс істейді. Бұл мысалда кескін, customizationEntity және cartComboProducts ішінде басқа кірістірілген Realm нысандары жоқ деп есептейміз, сондықтан басқа кірістірілген циклдар мен жоюлар жоқ.

«Жылдам» шешім

Біз шешкен бірінші нәрсе - ең жылдам өсіп келе жатқан нысандарды тазалау және бұл біздің бастапқы мәселемізді шеше алатынын көру үшін нәтижелерді тексеру. Біріншіден, ең қарапайым және интуитивті шешім жасалды, атап айтқанда: әрбір нысан өз балаларын жоюға жауапты болуы керек. Мұны істеу үшін біз кірістірілген Realm нысандарының тізімін қайтаратын интерфейсті енгіздік:

interface NestedEntityAware {
 fun getNestedEntities(): Collection<RealmObject?>
}

Біз оны Realm нысандарында іске асырдық:

@RealmClass
class DataPizzeriaEntity(
 @PrimaryKey
 var id: String? = null,
 var name: String? = null,
 var coordinates: CoordinatesEntity? = null,
 var deliverySchedule: ScheduleEntity? = null,
 var restaurantSchedule: ScheduleEntity? = null,
 ...
) : RealmObject(), NestedEntityAware {

 override fun getNestedEntities(): Collection<RealmObject?> {
   return listOf(
       coordinates,
       deliverySchedule,
       restaurantSchedule
   )
 }
}

В getNestedEntities біз барлық балаларды тегіс тізім ретінде қайтарамыз. Сондай-ақ әрбір еншілес нысан NestedEntityAware интерфейсін іске асыра алады, мысалы, оның жойылатын ішкі Realm нысандары бар екенін көрсетеді. ScheduleEntity:

@RealmClass
class ScheduleEntity(
 var monday: DayOfWeekEntity? = null,
 var tuesday: DayOfWeekEntity? = null,
 var wednesday: DayOfWeekEntity? = null,
 var thursday: DayOfWeekEntity? = null,
 var friday: DayOfWeekEntity? = null,
 var saturday: DayOfWeekEntity? = null,
 var sunday: DayOfWeekEntity? = null
) : RealmObject(), NestedEntityAware {

 override fun getNestedEntities(): Collection<RealmObject?> {
   return listOf(
       monday, tuesday, wednesday, thursday, friday, saturday, sunday
   )
 }
}

Және т.б., объектілердің ұя салуы қайталануы мүмкін.

Содан кейін біз барлық кірістірілген нысандарды рекурсивті түрде жоятын әдісті жазамыз. Әдіс (кеңейтім ретінде жасалған) deleteAllNestedEntities барлық жоғарғы деңгейдегі нысандар мен әдісті алады deleteNestedRecursively NestedEntityAware интерфейсін пайдаланып барлық кірістірілген нысандарды рекурсивті түрде жояды:

fun <T> Realm.deleteAllNestedEntities(entities: Collection<T>,
 entityClass: Class<out RealmObject>,
 idMapper: (T) -> String,
 idFieldName : String = "id"
 ) {

 val existedObjects = where(entityClass)
     .`in`(idFieldName, entities.map(idMapper).toTypedArray())
     .findAll()

 deleteNestedRecursively(existedObjects)
}

private fun Realm.deleteNestedRecursively(entities: Collection<RealmObject?>) {
 for(entity in entities) {
   entity?.let { realmObject ->
     if (realmObject is NestedEntityAware) {
       deleteNestedRecursively((realmObject as NestedEntityAware).getNestedEntities())
     }
     realmObject.deleteFromRealm()
   }
 }
}

Біз мұны ең жылдам өсетін нысандармен жасадық және не болғанын тексердік.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Нәтижесінде біз осы шешіммен жапқан нысандар өсуді тоқтатты. Ал базаның жалпы өсуі баяулады, бірақ тоқтамады.

«Қалыпты» шешім

База баяу өсе бастағанымен, ол әлі де өсті. Сондықтан біз одан әрі қарай бастадық. Біздің жоба Realm жүйесінде деректерді кэштеуді белсенді түрде пайдаланады. Сондықтан әрбір нысан үшін барлық кірістірілген нысандарды жазу көп еңбекті қажет етеді, сонымен қатар қателер қаупі артады, өйткені кодты өзгерту кезінде нысандарды көрсетуді ұмытып кетуге болады.

Мен интерфейстерді пайдаланбағаныма, бірақ бәрі өздігінен жұмыс істейтініне көз жеткізгім келді.

Біз бір нәрсенің өздігінен жұмыс істеуін қаласақ, біз рефлексияны қолдануымыз керек. Мұны істеу үшін біз әрбір сынып өрісін аралап, оның Realm нысаны немесе нысандар тізімі екенін тексере аламыз:

RealmModel::class.java.isAssignableFrom(field.type)

RealmList::class.java.isAssignableFrom(field.type)

Егер өріс RealmModel немесе RealmList болса, осы өрістің нысанын кірістірілген нысандар тізіміне қосыңыз. Барлығы жоғарыда істегеніміздей, тек мұнда ол өздігінен орындалады. Каскадты жою әдісінің өзі өте қарапайым және келесідей көрінеді:

fun <T : Any> Realm.cascadeDelete(entities: Collection<T?>) {
 if(entities.isEmpty()) {
   return
 }

 entities.filterNotNull().let { notNullEntities ->
   notNullEntities
       .filterRealmObject()
       .flatMap { realmObject -> getNestedRealmObjects(realmObject) }
       .also { realmObjects -> cascadeDelete(realmObjects) }

   notNullEntities
       .forEach { entity ->
         if((entity is RealmObject) && entity.isValid) {
           entity.deleteFromRealm()
         }
       }
 }
}

Кеңейтім filterRealmObject тек Realm нысандарын сүзеді және өткізеді. Әдіс getNestedRealmObjects рефлексия арқылы ол барлық кірістірілген Realm нысандарын тауып, оларды сызықтық тізімге қояды. Содан кейін біз бірдей нәрсені рекурсивті түрде жасаймыз. Жою кезінде объектінің жарамдылығын тексеру керек isValid, себебі әртүрлі тектік нысандарда бірдей кірістірілген нысандар болуы мүмкін. Бұған жол бермеу және жаңа нысандарды жасау кезінде идентификаторды автоматты түрде жасауды пайдалану жақсы.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

getNestedRealmObjects әдісінің толық орындалуы

private fun getNestedRealmObjects(realmObject: RealmObject) : List<RealmObject> {
 val nestedObjects = mutableListOf<RealmObject>()
 val fields = realmObject.javaClass.superclass.declaredFields

// Проверяем каждое поле, не является ли оно RealmModel или списком RealmList
 fields.forEach { field ->
   when {
     RealmModel::class.java.isAssignableFrom(field.type) -> {
       try {
         val child = getChildObjectByField(realmObject, field)
         child?.let {
           if (isInstanceOfRealmObject(it)) {
             nestedObjects.add(child as RealmObject)
           }
         }
       } catch (e: Exception) { ... }
     }

     RealmList::class.java.isAssignableFrom(field.type) -> {
       try {
         val childList = getChildObjectByField(realmObject, field)
         childList?.let { list ->
           (list as RealmList<*>).forEach {
             if (isInstanceOfRealmObject(it)) {
               nestedObjects.add(it as RealmObject)
             }
           }
         }
       } catch (e: Exception) { ... }
     }
   }
 }

 return nestedObjects
}

private fun getChildObjectByField(realmObject: RealmObject, field: Field): Any? {
 val methodName = "get${field.name.capitalize()}"
 val method = realmObject.javaClass.getMethod(methodName)
 return method.invoke(realmObject)
}

Нәтижесінде біз клиенттік кодымызда деректерді өзгертудің әрбір операциясы үшін «каскадты жоюды» қолданамыз. Мысалы, кірістіру операциясы үшін ол келесідей көрінеді:

override fun <T : Entity> insert(
 entityInformation: EntityInformation,
 entities: Collection<T>): Collection<T> = entities.apply {
 realmInstance.cascadeDelete(getManagedEntities(entityInformation, this))
 realmInstance.copyFromRealm(
     realmInstance
         .copyToRealmOrUpdate(this.map { entity -> entity as RealmModel }
 ))
}

Бірінші әдіс getManagedEntities барлық қосылған нысандарды, содан кейін әдісті қабылдайды cascadeDelete Жаңаларын жазбас бұрын барлық жиналған нысандарды рекурсивті түрде жояды. Біз осы тәсілді қолданбаның барлық барысында қолданамыз. Realm жүйесінде жадтың ағуы толығымен жойылды. Іске қосу уақытының қолданбаны суық іске қосу санына тәуелділігін бірдей өлшеуді жүргізгеннен кейін біз нәтижені көреміз.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Жасыл жол кірістірілген нысандарды автоматты түрде каскадты жою кезінде қолданбаны іске қосу уақытының суық іске қосулар санына тәуелділігін көрсетеді.

Нәтижелер мен қорытындылар

Тұрақты өсіп келе жатқан Realm дерекқоры қолданбаның өте баяу іске қосылуына себеп болды. Біз кірістірілген нысандарды «каскадты жою» арқылы жаңартуды шығардық. Енді біз _app_start метрикасы арқылы шешіміміздің қолданбаны іске қосу уақытына қалай әсер еткенін бақылап, бағалаймыз.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Талдау үшін біз 90 күндік уақыт кезеңін аламыз және көреміз: қолданбаны іске қосу уақыты, медиана және пайдаланушылардың 95-процентильіне түсетін уақыт азая бастады және енді көтерілмейді.

Патшалықтағы каскадты жоюдың ұзақ іске қосу арқылы қалай жеңетіні туралы ертегі

Жеті күндік диаграмманы қарасаңыз, _app_start көрсеткіші толығымен сәйкес және 1 секундтан аз болып көрінеді.

_app_start медианалық мәні 5 секундтан асатын болса, Firebase әдепкі бойынша хабарландыруларды жіберетінін де айта кеткен жөн. Дегенмен, көріп отырғанымыздай, сіз бұған сенбеуіңіз керек, керісінше кіріп, оны анық түрде тексеріңіз.

Realm дерекқорының ерекшелігі - бұл реляциялық емес дерекқор. Пайдаланудың қарапайымдылығына, ORM шешімдеріне және объектілерді байланыстыруға ұқсастығына қарамастан, оның каскадты жою мүмкіндігі жоқ.

Егер бұл ескерілмесе, онда кірістірілген нысандар жиналып, «ағып кетеді». Деректер базасы үнемі өседі, бұл өз кезегінде қолданбаның баяулауына немесе іске қосылуына әсер етеді.

Мен әлі қораптан шықпаған, бірақ көптен бері айтылып келе жатқан Realm-тегі нысандарды каскадты жоюды қалай тез орындауға болатындығы туралы тәжірибемізбен бөлістім. олар айтады и олар айтады. Біздің жағдайда бұл қолданбаны іске қосу уақытын айтарлықтай жылдамдатты.

Бұл мүмкіндіктің жақын арада пайда болуы туралы талқылауға қарамастан, Realm жүйесінде каскадты жоюдың болмауы дизайн арқылы жүзеге асырылады. Егер сіз жаңа қолданбаны әзірлеп жатсаңыз, оны ескеріңіз. Егер сіз Realm қолданбасын пайдаланып жатсаңыз, сізде осындай мәселелер бар-жоғын тексеріңіз.

Ақпарат көзі: www.habr.com

пікір қалдыру