Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Alle gebruikers aanvaar vinnige bekendstelling en responsiewe UI in mobiele toepassings as vanselfsprekend. As die toepassing lank neem om te begin, begin die gebruiker hartseer en kwaad voel. Jy kan maklik die kliënt-ervaring bederf of die gebruiker heeltemal verloor selfs voordat hy die toepassing begin gebruik.

Ons het eenkeer ontdek dat die Dodo Pizza-toepassing gemiddeld 3 sekondes neem om te begin, en vir sommige "gelukkiges" neem dit 15-20 sekondes.

Onder die snit is 'n storie met 'n gelukkige einde: oor die groei van die Realm-databasis, 'n geheuelek, hoe ons geneste voorwerpe opgehoop het, en dan onsself bymekaar getrek en alles reggemaak het.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het
Artikel skrywer: Maxim Kachinkin — Android-ontwikkelaar by Dodo Pizza.

Drie sekondes vanaf die klik op die toepassingsikoon tot onResume() van die eerste aktiwiteit is oneindig. En vir sommige gebruikers het die opstarttyd 15-20 sekondes bereik. Hoe is dit selfs moontlik?

'n Baie kort opsomming vir diegene wat nie tyd het om te lees nie
Ons Realm-databasis het eindeloos gegroei. Sommige geneste voorwerpe is nie uitgevee nie, maar is voortdurend opgehoop. Die begintyd van die toepassing het geleidelik toegeneem. Toe het ons dit reggemaak, en die opstarttyd het by die teiken gekom - dit het minder as 1 sekonde geword en nie meer toegeneem nie. Die artikel bevat 'n ontleding van die situasie en twee oplossings - 'n vinnige een en 'n normale een.

Soek en ontleding van die probleem

Vandag moet enige mobiele toepassing vinnig begin en reageer. Maar dit gaan nie net oor die mobiele toepassing nie. Gebruikerservaring van interaksie met 'n diens en 'n maatskappy is 'n komplekse ding. Byvoorbeeld, in ons geval is afleweringspoed een van die sleutelaanwysers vir pizzadiens. As aflewering vinnig is, sal die pizza warm wees, en die klant wat nou wil eet, hoef nie lank te wag nie. Vir die toepassing is dit op sy beurt belangrik om die gevoel van vinnige diens te skep, want as die toepassing net 20 sekondes neem om te begin, hoe lank sal jy dan moet wag vir die pizza?

Aanvanklik het ons self gekonfronteer met die feit dat die toepassing soms 'n paar sekondes geneem het om te begin, en dan het ons klagtes van ander kollegas begin hoor oor hoe lank dit geneem het. Maar ons was nie in staat om hierdie situasie konsekwent te herhaal nie.

Hoe lank is dit? Volgens Google dokumentasie, as 'n koue begin van 'n toediening minder as 5 sekondes neem, word dit as "asof normaal" beskou. Dodo Pizza Android-toepassing is bekendgestel (volgens Firebase-statistieke _app_begin) by koue begin gemiddeld in 3 sekondes - "Nie wonderlik nie, nie vreeslik nie," soos hulle sê.

Maar toe begin klagtes verskyn dat die toepassing baie, baie, baie lank geneem het om te begin! Om mee te begin, het ons besluit om te meet wat "baie, baie, baie lank" is. En ons het Firebase-spoor hiervoor gebruik App begin spoor.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Hierdie standaardspoor meet die tyd tussen die oomblik dat die gebruiker die toepassing oopmaak en die oomblik dat die onResume() van die eerste aktiwiteit uitgevoer word. In die Firebase-konsole word hierdie maatstaf _app_start genoem. Dit het geblyk dat:

  • Opstarttye vir gebruikers bo die 95ste persentiel is byna 20 sekondes (sommige selfs langer), ten spyte daarvan dat die mediaan koue opstarttyd minder as 5 sekondes is.
  • Die begintyd is nie 'n konstante waarde nie, maar groei mettertyd. Maar soms is daar druppels. Ons het hierdie patroon gevind toe ons die skaal van analise tot 90 dae verhoog het.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Twee gedagtes het by my opgekom:

  1. Iets lek.
  2. Hierdie "iets" word na vrylating teruggestel en lek dan weer.

"Seker iets met die databasis," het ons gedink, en ons was reg. Eerstens gebruik ons ​​die databasis as 'n kas; tydens migrasie maak ons ​​dit skoon. Tweedens word die databasis gelaai wanneer die toepassing begin. Alles pas inmekaar.

Wat is fout met die Realm-databasis

Ons het begin kyk hoe die inhoud van die databasis verander oor die leeftyd van die toepassing, vanaf die eerste installasie en verder tydens aktiewe gebruik. U kan die inhoud van die Realm-databasis bekyk via steto of in meer besonderhede en duidelik deur die lêer oop te maak via Realm Studio. Kopieer die Realm-databasislêer om die inhoud van die databasis via ADB te sien:

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

Nadat ons op verskillende tye na die inhoud van die databasis gekyk het, het ons uitgevind dat die aantal voorwerpe van 'n sekere tipe voortdurend toeneem.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het
Die prentjie toon 'n fragment van Realm Studio vir twee lêers: aan die linkerkant - die toepassingsbasis 'n rukkie na installasie, aan die regterkant - na aktiewe gebruik. Dit kan gesien word dat die aantal voorwerpe ImageEntity и MoneyType het aansienlik gegroei (die skermkiekie wys die aantal voorwerpe van elke tipe).

Verwantskap tussen databasisgroei en opstarttyd

Onbeheerde databasisgroei is baie sleg. Maar hoe beïnvloed dit die begintyd van die toepassing? Dit is redelik maklik om dit deur die ActivityManager te meet. Sedert Android 4.4, vertoon logcat die logboek met die string Wys en die tyd. Hierdie tyd is gelyk aan die interval vanaf die oomblik dat die toepassing geloods word tot die einde van die aktiwiteitsweergawe. Gedurende hierdie tyd vind die volgende gebeurtenisse plaas:

  • Begin die proses.
  • Inisialisering van voorwerpe.
  • Skep en inisialisering van aktiwiteite.
  • Die skep van 'n uitleg.
  • Toepassingslewering.

Pas ons. As jy ADB met die -S- en -W-vlae laat loop, kan jy uitgebreide uitvoer kry met die opstarttyd:

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

As jy dit van daar af gryp grep -i WaitTime tyd, kan jy die versameling van hierdie metrieke outomatiseer en visueel na die resultate kyk. Die grafiek hieronder toon die afhanklikheid van die toepassing se aanvangstyd van die aantal koue begin van die toepassing.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Terselfdertyd was daar dieselfde aard van die verband tussen die grootte en groei van die databasis, wat van 4 MB tot 15 MB gegroei het. In totaal blyk dit dat beide die toepassing se bekendstellingstyd en die grootte van die databasis met verloop van tyd (met die groei van koue begin) toegeneem het. Ons het 'n hipotese op ons hande. Nou was al wat oorgebly het om die afhanklikheid te bevestig. Daarom het ons besluit om die "lekkasies" te verwyder en te kyk of dit die bekendstelling sou bespoedig.

Redes vir eindelose databasisgroei

Voordat u "lekkasies" verwyder, is dit die moeite werd om te verstaan ​​​​waarom hulle in die eerste plek verskyn het. Om dit te doen, laat ons onthou wat Realm is.

Realm is 'n nie-relasionele databasis. Dit laat jou toe om verhoudings tussen voorwerpe te beskryf op 'n soortgelyke manier as hoeveel ORM-relasionele databasisse op Android beskryf word. Terselfdertyd stoor Realm voorwerpe direk in die geheue met die minste hoeveelheid transformasies en kartering. Dit laat jou toe om data van die skyf baie vinnig te lees, wat Realm se sterkpunt is en hoekom dit geliefd is.

(Vir die doeleindes van hierdie artikel sal hierdie beskrywing vir ons genoeg wees. Jy kan meer oor Realm in die koelte lees dokumentasie of in hul Akademie).

Baie ontwikkelaars is gewoond daaraan om meer met relasionele databasisse te werk (byvoorbeeld ORM-databasisse met SQL onder die kap). En dinge soos die uitwissing van data deurval lyk dikwels na 'n gegewe. Maar nie in die Ryk nie.

Terloops, die kaskade-skrap-funksie word al lank gevra. Hierdie hersiening и 'n ander, wat daarmee gepaardgaan, aktief bespreek is. Daar was 'n gevoel dat dit binnekort gedoen sou word. Maar toe het alles vertaal in die bekendstelling van sterk en swak skakels, wat ook outomaties hierdie probleem sou oplos. Was nogal lewendig en aktief met hierdie taak trek versoek, wat vir nou onderbreek is weens interne probleme.

Datalek sonder uitwissing

Hoe presies lek data as jy staatmaak op 'n nie-bestaande cascading delete? As jy geneste Realm-objekte het, moet hulle uitgevee word.
Kom ons kyk na 'n (amper) werklike voorbeeld. Ons het 'n voorwerp 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()

Die produk in die kar het verskillende velde, insluitend 'n prentjie ImageEntity, pasgemaakte bestanddele CustomizationEntity. Die produk in die wa kan ook 'n kombinasie met sy eie stel produkte wees RealmList (CartProductEntity). Alle gelyste velde is Realm-objekte. As ons 'n nuwe objek (copyToRealm() / copyToRealmOrUpdate()) met dieselfde id invoeg, dan sal hierdie objek heeltemal oorskryf word. Maar alle interne voorwerpe (beeld, customizationEntity en cartComboProducts) sal verbinding met die ouer verloor en in die databasis bly.

Aangesien die verbinding met hulle verlore is, lees ons dit nie meer of vee dit uit nie (tensy ons uitdruklik toegang tot hulle verkry of die hele "tabel" uitvee). Ons het dit "geheuelekkasies" genoem.

Wanneer ons met Realm werk, moet ons eksplisiet deur al die elemente gaan en alles eksplisiet uitvee voor sulke operasies. Dit kan byvoorbeeld so gedoen word:

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()
}
// и потом уже сохраняем

As jy dit doen, sal alles werk soos dit moet. In hierdie voorbeeld neem ons aan dat daar geen ander geneste Realm-objekte binne-in beeld, customizationEntity en cartComboProducts is nie, so daar is geen ander geneste lusse en verwyderings nie.

"Vinnige" oplossing

Die eerste ding wat ons besluit het om te doen, was om die vinnigste groeiende voorwerpe skoon te maak en die resultate na te gaan om te sien of dit ons oorspronklike probleem sou oplos. Eerstens is die eenvoudigste en mees intuïtiewe oplossing gemaak, naamlik: elke voorwerp moet verantwoordelik wees vir die verwydering van sy kinders. Om dit te doen, het ons 'n koppelvlak bekendgestel wat 'n lys van sy geneste Realm-voorwerpe teruggestuur het:

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

En ons het dit in ons Realm-voorwerpe geïmplementeer:

@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 ons gee alle kinders terug as 'n plat lys. En elke kindervoorwerp kan ook die NestedEntityAware-koppelvlak implementeer, wat aandui dat dit interne Realm-objekte het om te verwyder, byvoorbeeld 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
   )
 }
}

En so kan die nes van voorwerpe herhaal word.

Dan skryf ons 'n metode wat alle geneste voorwerpe rekursief uitvee. Metode (gemaak as 'n uitbreiding) deleteAllNestedEntities kry alle top-vlak voorwerpe en metode deleteNestedRecursively Verwyder rekursief alle geneste voorwerpe met behulp van die NestedEntityAware-koppelvlak:

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

Ons het dit met die vinnigste groeiende voorwerpe gedoen en gekyk wat gebeur het.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

As gevolg hiervan het die voorwerpe wat ons met hierdie oplossing bedek het, opgehou groei. En die algehele groei van die basis het verlangsaam, maar het nie opgehou nie.

Die "normale" oplossing

Alhoewel die basis stadiger begin groei het, het dit steeds gegroei. So ons het verder begin soek. Ons projek maak baie aktief gebruik van datakas in Realm. Daarom is die skryf van alle geneste voorwerpe vir elke voorwerp arbeidsintensief, plus die risiko van foute verhoog, want jy kan vergeet om voorwerpe te spesifiseer wanneer jy die kode verander.

Ek wou seker maak dat ek nie koppelvlakke gebruik nie, maar dat alles op sy eie werk.

Wanneer ons wil hê iets moet op sy eie werk, moet ons refleksie gebruik. Om dit te doen, kan ons deur elke klasveld gaan en kyk of dit 'n Realm-objek of 'n lys van voorwerpe is:

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

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

As die veld 'n RealmModel of RealmList is, voeg dan die voorwerp van hierdie veld by 'n lys geneste voorwerpe. Alles is presies dieselfde as wat ons hierbo gedoen het, net hier sal dit vanself gedoen word. Die kaskade-skrapmetode self is baie eenvoudig en lyk soos volg:

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

Uitbreiding filterRealmObject filter net Realm-voorwerpe uit en gee dit deur. Metode getNestedRealmObjects deur refleksie vind dit alle geneste Realm-objekte en plaas dit in 'n lineêre lys. Dan doen ons dieselfde ding rekursief. Wanneer u uitvee, moet u die voorwerp vir geldigheid nagaan isValid, want dit kan wees dat verskillende ouerobjekte geneste identiese kan hê. Dit is beter om dit te vermy en bloot outo-generering van ID te gebruik wanneer nuwe voorwerpe geskep word.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Volledige implementering van die getNestedRealmObjects-metode

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

Gevolglik gebruik ons ​​in ons kliëntkode "cascading delete" vir elke datamodifikasiebewerking. Byvoorbeeld, vir 'n invoegbewerking lyk dit soos volg:

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

Metode eerste getManagedEntities ontvang alle bygevoegde voorwerpe, en dan die metode cascadeDelete Vee rekursief alle versamelde voorwerpe uit voordat nuwes geskryf word. Ons gebruik uiteindelik hierdie benadering regdeur die toepassing. Geheuelekkasies in Realm is heeltemal weg. Nadat ons dieselfde meting uitgevoer het van die afhanklikheid van opstarttyd van die aantal koue begin van die toepassing, sien ons die resultaat.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Die groen lyn wys die afhanklikheid van die toepassing se opstarttyd van die aantal koue begin tydens outomatiese kaskade-uitwissing van geneste voorwerpe.

Resultate en gevolgtrekkings

Die steeds groeiende Realm-databasis het die toepassing baie stadig laat begin. Ons het 'n opdatering vrygestel met ons eie "cascading delete" van geneste voorwerpe. En nou monitor en evalueer ons hoe ons besluit die toepassing se begintyd beïnvloed het deur die _app_start-metriek.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

Vir ontleding neem ons 'n tydperk van 90 dae en sien: die toepassing se bekendstellingstyd, beide die mediaan en dit wat op die 95ste persentiel van gebruikers val, het begin afneem en nie meer styg nie.

Die verhaal van hoe deurlopende verwydering in Realm se lang bekendstelling gewen het

As jy na die sewe-dag-grafiek kyk, lyk die _app_start-metriek heeltemal voldoende en is dit minder as 1 sekonde.

Dit is ook die moeite werd om by te voeg dat Firebase by verstek kennisgewings stuur as die mediaanwaarde van _app_start 5 sekondes oorskry. Soos ons kan sien, moet jy egter nie hierop staatmaak nie, maar eerder ingaan en dit uitdruklik nagaan.

Die spesiale ding van die Realm-databasis is dat dit 'n nie-relasionele databasis is. Ten spyte van die gebruiksgemak, ooreenkoms met ORM-oplossings en objekkoppeling, het dit nie kaskade-uitwissing nie.

As dit nie in ag geneem word nie, sal geneste voorwerpe ophoop en "weglek". Die databasis sal voortdurend groei, wat weer die verlangsaming of opstart van die toepassing sal beïnvloed.

Ek het ons ervaring gedeel oor hoe om vinnig 'n kaskade-uitvee van voorwerpe in Realm te doen, wat nog nie uit die boks is nie, maar oor 'n lang tyd gepraat word hulle sê и hulle sê. In ons geval het dit die opstarttyd van die toepassing aansienlik versnel.

Ten spyte van die bespreking oor die naderende verskyning van hierdie kenmerk, word die afwesigheid van kaskade-skrap in Realm deur ontwerp gedoen. As jy 'n nuwe toepassing ontwerp, neem dit dan in ag. En as jy reeds Realm gebruik, kyk of jy sulke probleme het.

Bron: will.com

Voeg 'n opmerking