La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Ĉiuj uzantoj prenas rapidan lanĉon kaj respondema UI en moveblaj aplikoj por koncedite. Se la aplikaĵo bezonas longan tempon por lanĉi, la uzanto komencas sentiĝi malĝoja kaj kolera. Vi povas facile difekti la klientan sperton aŭ tute perdi la uzanton eĉ antaŭ ol li komencas uzi la aplikaĵon.

Ni iam malkovris, ke la apo Dodo Pizza bezonas averaĝe 3 sekundojn por lanĉi, kaj por iuj "bonŝanculoj" necesas 15-20 sekundoj.

Sub la tranĉo estas rakonto kun feliĉa fino: pri la kresko de la Realm-datumbazo, memorfuĝo, kiel ni amasigis nestitajn objektojn, kaj tiam kuntiriĝis kaj riparis ĉion.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo
Artikolo aŭtoro: Maksim Kaĉinkin — Android-programisto ĉe Dodo Pizza.

Tri sekundoj de klakado sur la aplikaĵikono ĝis onResume() de la unua agado estas senfineco. Kaj por iuj uzantoj, la ektempo atingis 15-20 sekundojn. Kiel ĉi tio eĉ eblas?

Tre mallonga resumo por tiuj, kiuj ne havas tempon por legi
Nia Realm-datumbazo kreskis senfine. Kelkaj nestitaj objektoj ne estis forigitaj, sed estis konstante akumulitaj. La aplikaĵa ektempo iom post iom pliiĝis. Tiam ni riparis ĝin, kaj la ektempo venis al la celo - ĝi fariĝis malpli ol 1 sekundo kaj ne plu pliiĝis. La artikolo enhavas analizon de la situacio kaj du solvojn - rapidan kaj normalan.

Serĉo kaj analizo de la problemo

Hodiaŭ, ajna movebla aplikaĵo devas lanĉi rapide kaj esti respondema. Sed ne temas nur pri la poŝtelefona aplikaĵo. Uzanta sperto de interago kun servo kaj kompanio estas kompleksa afero. Ekzemple, en nia kazo, liverrapideco estas unu el la ŝlosilaj indikiloj por pico-servo. Se livero estas rapida, la pico estos varma, kaj la kliento, kiu volas manĝi nun, ne devos longe atendi. Por la aplikaĵo, siavice, gravas krei la senton de rapida servo, ĉar se la aplikaĵo bezonas nur 20 sekundojn por lanĉi, kiom longe vi devos atendi la picon?

Komence, ni mem renkontis la fakton, ke foje la aplikaĵo daŭris kelkajn sekundojn por lanĉi, kaj poste ni komencis aŭdi plendojn de aliaj kolegoj pri kiom longe ĝi daŭris. Sed ni ne povis konstante ripeti ĉi tiun situacion.

Kiom longe ĝi estas? Laŭ Gugla dokumentaro, se malvarma komenco de aplikaĵo daŭras malpli ol 5 sekundojn, tiam ĉi tio estas konsiderata "kvazaŭ normala". Dodo Pizza Android-apliko lanĉita (laŭ Firebase-metriko _app_komenco) ĉe malvarma starto averaĝe en 3 sekundoj - "Ne bonega, ne terura", kiel oni diras.

Sed tiam komencis aperi plendoj, ke la aplikaĵo prenis tre, tre, tre longan tempon por lanĉi! Komence, ni decidis mezuri kio estas "tre, tre, tre longa". Kaj ni uzis Firebase-spuron por ĉi tio App-komenca spuro.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Ĉi tiu norma spuro mezuras la tempon inter la momento, kiam la uzanto malfermas la aplikaĵon kaj la momento, kiam la onResume() de la unua agado estas efektivigita. En la Firebase Konzolo ĉi tiu metriko nomiĝas _app_start. Montriĝis ke:

  • Komenctempoj por uzantoj super la 95-a percentilo estas preskaŭ 20 sekundoj (kelkaj eĉ pli longaj), malgraŭ la meza malvarma ektempo estas malpli ol 5 sekundoj.
  • La ektempo ne estas konstanta valoro, sed kreskas laŭlonge de la tempo. Sed foje estas gutoj. Ni trovis ĉi tiun ŝablonon kiam ni pliigis la skalon de analizo al 90 tagoj.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Venis al la menso du pensoj:

  1. Io likas.
  2. Ĉi tiu "io" estas rekomencigita post liberigo kaj poste likas denove.

"Verŝajne io kun la datumbazo," ni pensis, kaj ni pravis. Unue, ni uzas la datumbazon kiel kaŝmemoron; dum migrado ni purigas ĝin. Due, la datumbazo estas ŝarĝita kiam la aplikaĵo komenciĝas. Ĉio kongruas.

Kio malbonas kun la Realm-datumbazo

Ni komencis kontroli kiel la enhavo de la datumbazo ŝanĝiĝas dum la vivo de la aplikaĵo, ekde la unua instalado kaj plu dum aktiva uzo. Vi povas vidi la enhavon de la Realm-datumbazo per Stetono aŭ pli detale kaj klare malfermante la dosieron per Realm Studio. Por vidi la enhavon de la datumbazo per ADB, kopiu la datumbazan dosieron de Realm:

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

Rigardante la enhavon de la datumbazo en malsamaj tempoj, ni eksciis, ke la nombro da objektoj de certa tipo konstante pliiĝas.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo
La bildo montras fragmenton de Realm Studio por du dosieroj: maldekstre - la aplikaĵa bazo iom da tempo post instalado, dekstre - post aktiva uzo. Oni povas vidi ke la nombro da objektoj ImageEntity и MoneyType signife kreskis (la ekrankopio montras la nombron da objektoj de ĉiu tipo).

Rilato inter datumbaza kresko kaj ektempo

Nekontrolita datumbaza kresko estas tre malbona. Sed kiel ĉi tio influas la tempon de lanĉo de la aplikaĵo? Estas sufiĉe facile mezuri ĉi tion per ActivityManager. Ekde Android 4.4, logcat montras la protokolon kun la ĉeno Montrita kaj la horo. Ĉi tiu tempo egalas al la intervalo de la momento, kiam la aplikaĵo estas lanĉita ĝis la fino de la agado-prezento. Dum tiu tempo okazas la sekvaj eventoj:

  • Komencu la procezon.
  • Inicialigo de objektoj.
  • Kreado kaj komencado de agadoj.
  • Kreante aranĝon.
  • Aplika bildigo.

konvenas al ni. Se vi rulas ADB kun la flagoj -S kaj -W, vi povas akiri plilongigitan eligon kun la ektempo:

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

Se vi kaptas ĝin de tie grep -i WaitTime tempo, vi povas aŭtomatigi la kolekton de ĉi tiu metriko kaj vide rigardi la rezultojn. La ĉi-suba grafiko montras la dependecon de la tempo de lanĉo de la aplikaĵo de la nombro da malvarmaj lanĉoj de la aplikaĵo.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Samtempe, ekzistis la sama naturo de la rilato inter la grandeco kaj kresko de la datumbazo, kiu kreskis de 4 MB ĝis 15 MB. Entute, rezultas, ke laŭlonge de la tempo (kun la kresko de malvarmaj startoj), kaj la tempo de lanĉo de aplikaĵo kaj la grandeco de la datumbazo pliiĝis. Ni havas hipotezon sur niaj manoj. Nun restis nur konfirmi la dependecon. Tial ni decidis forigi la "likojn" kaj vidi ĉu ĉi tio akcelos la lanĉon.

Kialoj por senfina datumbaza kresko

Antaŭ ol forigi "likojn", indas kompreni, kial ili unue aperis. Por fari tion, ni memoru, kio estas Realm.

Realm estas ne-rilata datumbazo. Ĝi permesas vin priskribi rilatojn inter objektoj simile al kiom da ORM-rilataj datumbazoj pri Android estas priskribitaj. En la sama tempo, Realm stokas objektojn rekte en memoro kun la malplej kvanto da transformoj kaj mapadoj. Ĉi tio permesas vin legi datumojn de la disko tre rapide, kio estas la forto de Realm kaj kial ĝi estas amata.

(Por la celoj de ĉi tiu artikolo, ĉi tiu priskribo sufiĉos por ni. Vi povas legi pli pri Realm en la malvarmeta dokumentado aŭ en ilia akademio).

Multaj programistoj kutimas labori pli kun interrilataj datumbazoj (ekzemple, ORM-datumbazoj kun SQL sub la kapuĉo). Kaj aferoj kiel kaskada forigo de datumoj ofte ŝajnas donitaj. Sed ne en Sfero.

Cetere, la kaskada forigo-trajto estas delonge demandita. Ĉi tio revizio и alia, asociita kun ĝi, estis aktive diskutita. Estis sento, ke ĝi baldaŭ estos farita. Sed tiam ĉio tradukiĝis al la enkonduko de fortaj kaj malfortaj ligiloj, kiuj ankaŭ aŭtomate solvus ĉi tiun problemon. Estis sufiĉe vigla kaj aktiva en ĉi tiu tasko tiri peton, kiu estas paŭzita nuntempe pro internaj malfacilaĵoj.

Datumoj malfluas sen kaskada forigo

Kiel precize fluas datumoj se vi fidas je neekzistanta kaskada forigo? Se vi havas nestitajn Realm-objektojn, tiam ili devas esti forigitaj.
Ni rigardu (preskaŭ) realan ekzemplon. Ni havas objekton 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()

La produkto en la ĉaro havas malsamajn kampojn, inkluzive de bildo ImageEntity, personecigitaj ingrediencoj CustomizationEntity. Ankaŭ, la produkto en la ĉaro povas esti kombo kun sia propra aro de produktoj RealmList (CartProductEntity). Ĉiuj listigitaj kampoj estas Realm-objektoj. Se ni enmetas novan objekton (copyToRealm() / copyToRealmOrUpdate()) kun la sama identigilo, tiam ĉi tiu objekto estos tute anstataŭita. Sed ĉiuj internaj objektoj (bildo, personigoEntity kaj ĉaroComboProducts) perdos rilaton kun la gepatro kaj restos en la datumbazo.

Ĉar la konekto kun ili estas perdita, ni ne plu legas ilin aŭ forigas ilin (krom se ni eksplicite aliras ilin aŭ purigas la tutan "tabelon"). Ni nomis ĉi tion "memorfuĝoj".

Kiam ni laboras kun Realm, ni devas eksplicite trarigardi ĉiujn elementojn kaj eksplicite forigi ĉion antaŭ tiaj operacioj. Ĉi tio povas esti farita, ekzemple, jene:

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

Se vi faros tion, tiam ĉio funkcios kiel ĝi devus. En ĉi tiu ekzemplo, ni supozas, ke ne ekzistas aliaj nestitaj Realm-objektoj ene de bildo, personigoEntity kaj cartComboProducts, do ne ekzistas aliaj nestitaj bukloj kaj forigoj.

"Rapida" solvo

La unua afero, kiun ni decidis fari, estis purigi la plej rapide kreskantajn objektojn kaj kontroli la rezultojn por vidi ĉu ĉi tio solvos nian originalan problemon. Unue, la plej simpla kaj intuicia solvo estis farita, nome: ĉiu objekto devus esti respondeca por forigi siajn infanojn. Por fari tion, ni enkondukis interfacon kiu resendis liston de siaj nestitaj Realm-objektoj:

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

Kaj ni efektivigis ĝin en niaj Realmobjektoj:

@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 ni resendas ĉiujn infanojn kiel plata listo. Kaj ĉiu infana objekto ankaŭ povas efektivigi la NestedEntityAware-interfacon, indikante, ke ĝi havas internajn Realm-objektojn por forigi, ekzemple 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
   )
 }
}

Kaj tiel plu, la nestado de objektoj povas esti ripetita.

Tiam ni skribas metodon kiu rekursie forigas ĉiujn nestitajn objektojn. Metodo (farita kiel etendaĵo) deleteAllNestedEntities ricevas ĉiujn supernivelajn objektojn kaj metodon deleteNestedRecursively Rekurve forigas ĉiujn nestitajn objektojn uzante la NestedEntityAware-interfacon:

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

Ni faris tion kun la plej rapide kreskantaj objektoj kaj kontrolis kio okazis.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Kiel rezulto, tiuj objektoj, kiujn ni kovris per ĉi tiu solvo, ĉesis kreski. Kaj la ĝenerala kresko de la bazo malrapidiĝis, sed ne ĉesis.

La "normala" solvo

Kvankam la bazo komencis kreski pli malrapide, ĝi ankoraŭ kreskis. Do ni komencis serĉi plu. Nia projekto faras tre aktivan uzon de datuma kaŝmemoro en Realm. Sekve, skribi ĉiujn nestitajn objektojn por ĉiu objekto estas laborintensa, krome la risko de eraroj pliiĝas, ĉar vi povas forgesi specifi objektojn kiam vi ŝanĝas la kodon.

Mi volis certigi, ke mi ne uzas interfacojn, sed ke ĉio funkcias per si mem.

Kiam ni volas, ke io funkciu memstare, ni devas uzi pripensadon. Por fari tion, ni povas trarigardi ĉiun klaskampon kaj kontroli ĉu ĝi estas Realm-objekto aŭ listo de objektoj:

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

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

Se la kampo estas RealmModel aŭ RealmList, tiam aldonu la objekton de ĉi tiu kampo al listo de nestitaj objektoj. Ĉio estas ekzakte sama kiel ni faris supre, nur ĉi tie ĝi estos farita per si mem. La kaskada foriga metodo mem estas tre simpla kaj aspektas jene:

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

Etendo filterRealmObject filtras kaj pasas nur Realm-objektojn. Metodo getNestedRealmObjects tra reflektado, ĝi trovas ĉiujn nestitajn Realm-objektojn kaj metas ilin en linearan liston. Tiam ni faras la samon rekursie. Dum forigo, vi devas kontroli la objekton por valideco isValid, ĉar povas esti ke malsamaj gepatraj objektoj povas havi nestitajn identajn. Pli bone estas eviti ĉi tion kaj simple uzi aŭtomatan generadon de id kiam kreas novajn objektojn.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Plena efektivigo de la metodo 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)
}

Kiel rezulto, en nia klientokodo ni uzas "kascada forigo" por ĉiu datummodifa operacio. Ekzemple, por eniga operacio ĝi aspektas jene:

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

Metodo unue getManagedEntities ricevas ĉiujn aldonitajn objektojn, kaj poste la metodon cascadeDelete Rekurve forigas ĉiujn kolektitajn objektojn antaŭ skribi novajn. Ni finas uzi ĉi tiun aliron tra la aplikaĵo. Memorfuĝoj en Realm tute malaperis. Farinte la saman mezuradon de la dependeco de ektempo de la nombro da malvarmaj ekfunkcioj de la aplikaĵo, ni vidas la rezulton.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

La verda linio montras la dependecon de la aplika tempo de lanĉo de la nombro da malvarmaj ekfunkciigo dum aŭtomata kaskada forigo de nestitaj objektoj.

Rezultoj kaj konkludoj

La ĉiam kreskanta Realm-datumbazo igis la aplikaĵon lanĉi tre malrapide. Ni publikigis ĝisdatigon kun nia propra "kaskada forigo" de nestitaj objektoj. Kaj nun ni kontrolas kaj taksas kiel nia decido influis la aplikaĵan ektempon per la metriko _app_start.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Por analizo, ni prenas tempoperiodon de 90 tagoj kaj vidas: la aplikaĵa lanĉotempo, kaj la meza kaj tiu, kiu falas sur la 95-a procento de uzantoj, komencis malpliiĝi kaj ne plu altiĝas.

La rakonto pri kiel kaskada forigo en Realm venkis super longa lanĉo

Se vi rigardas la septagan diagramon, la metriko _app_start aspektas tute taŭga kaj estas malpli ol 1 sekundo.

Ankaŭ indas aldoni, ke defaŭlte, Firebase sendas sciigojn se la meza valoro de _app_start superas 5 sekundojn. Tamen, kiel ni povas vidi, vi ne devus fidi ĉi tion, sed prefere eniru kaj kontroli ĝin eksplicite.

La speciala afero pri la Realm-datumbazo estas, ke ĝi estas ne-rilata datumbazo. Malgraŭ ĝia facileco de uzo, simileco al ORM-solvoj kaj objektoligado, ĝi ne havas kaskadan forigon.

Se ĉi tio ne estas konsiderata, tiam nestitaj objektoj amasiĝos kaj "forfluos". La datumbazo kreskos konstante, kio siavice influos la malrapidiĝon aŭ ekfunkciigon de la aplikaĵo.

Mi konigis nian sperton pri kiel rapide fari kaskadan forigon de objektoj en Realm, kiu ankoraŭ ne estas el la skatolo, sed pri kiu oni longe parolas. diru и diru. En nia kazo, ĉi tio multe akcelis la aplikaĵan lanĉan tempon.

Malgraŭ la diskuto pri la baldaŭa apero de ĉi tiu funkcio, la foresto de kaskada forigo en Realm estas farita laŭ dezajno. Se vi desegnas novan aplikaĵon, tiam konsideru ĉi tion. Kaj se vi jam uzas Realm, kontrolu ĉu vi havas tiajn problemojn.

fonto: www.habr.com

Aldoni komenton