Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Allir notendur taka hraðræsingu og móttækilegt notendaviðmót í farsímaforritum sem sjálfsögðum hlut. Ef það tekur langan tíma að ræsa forritið byrjar notandinn að verða leiður og reiður. Þú getur auðveldlega spillt upplifun viðskiptavina eða alveg misst notandann jafnvel áður en hann byrjar að nota forritið.

Við uppgötvuðum einu sinni að Dodo Pizza appið tekur 3 sekúndur að ræsa að meðaltali og fyrir suma „heppna“ tekur það 15-20 sekúndur.

Fyrir neðan klippið er saga með ánægjulegum endi: um vöxt Realm gagnagrunnsins, minnisleka, hvernig við söfnuðum hreiðrum hlutum og tókum okkur svo saman og laguðum allt.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu
Höfundur greinar: Maxim Kachinkin — Android verktaki hjá Dodo Pizza.

Þrjár sekúndur frá því að smella á forritatáknið til onResume() fyrstu aðgerðarinnar er óendanlegt. Og fyrir suma notendur náði ræsingartíminn 15-20 sekúndur. Hvernig er þetta jafnvel hægt?

Örstutt samantekt fyrir þá sem hafa ekki tíma til að lesa
Realm gagnagrunnurinn okkar stækkaði endalaust. Sumum hreiðruðum hlutum var ekki eytt, heldur safnaðist stöðugt upp. Ræsingartími forritsins jókst smám saman. Síðan laguðum við það og ræsingartíminn kom að markmiðinu - hann varð innan við 1 sekúnda og jókst ekki lengur. Greinin inniheldur greiningu á aðstæðum og tvær lausnir - fljótleg og eðlileg.

Leit og greining á vandamálinu

Í dag verður hvaða farsímaforrit sem er verður að ræsa hratt og vera móttækilegt. En þetta snýst ekki bara um farsímaforritið. Upplifun notenda af samskiptum við þjónustu og fyrirtæki er flókinn hlutur. Til dæmis, í okkar tilviki, er afhendingarhraði einn af lykilvísunum fyrir pizzuþjónustu. Ef afgreiðslan er hröð verður pizzan heit og viðskiptavinurinn sem vill borða núna þarf ekki að bíða lengi. Fyrir forritið er aftur á móti mikilvægt að skapa tilfinningu fyrir hraðri þjónustu, því ef forritið tekur aðeins 20 sekúndur að ræsa, hversu lengi þarftu þá að bíða eftir pizzunni?

Í fyrstu stóðum við sjálf frammi fyrir því að stundum tók forritið nokkrar sekúndur að ræsa, og síðan fórum við að heyra kvartanir frá öðrum samstarfsmönnum um hversu langan tíma það tók. En við gátum ekki endurtekið þetta ástand stöðugt.

Hversu langt er það? Samkvæmt Google skjöl, ef kaldræsing forrits tekur minna en 5 sekúndur, þá er þetta talið „eins og eðlilegt“. Dodo Pizza Android app opnað (samkvæmt Firebase mæligildum _app_start) kl köld byrjun að meðaltali á 3 sekúndum - „Ekki frábært, ekki hræðilegt,“ eins og sagt er.

En svo fóru að birtast kvartanir um að forritið tæki mjög, mjög, mjög langan tíma að ræsa! Til að byrja með ákváðum við að mæla hvað „mjög, mjög, mjög langt“ er. Og við notuðum Firebase trace fyrir þetta Upphafsspor apps.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Þessi staðlaða rakning mælir tímann frá því augnabliki sem notandinn opnar forritið og þar til onResume() fyrstu aðgerðarinnar er framkvæmd. Í Firebase Console er þessi mælikvarði kallaður _app_start. Það kom í ljós að:

  • Ræsingartími fyrir notendur yfir 95. hundraðshlutanum er næstum 20 sekúndur (sumir jafnvel lengri), þrátt fyrir að miðgildi kaldræsingartímans sé innan við 5 sekúndur.
  • Ræsingartíminn er ekki stöðugt gildi heldur vex með tímanum. En stundum koma dropar. Við fundum þetta mynstur þegar við hækkuðum mælikvarða greiningar í 90 daga.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Tvær hugsanir komu upp í hugann:

  1. Eitthvað lekur.
  2. Þetta „eitthvað“ er endurstillt eftir útgáfu og lekur síðan aftur.

„Líklega eitthvað með gagnagrunninn,“ hugsuðum við og við höfðum rétt fyrir okkur. Í fyrsta lagi notum við gagnagrunninn sem skyndiminni; við flutning hreinsum við hann. Í öðru lagi er gagnagrunnurinn hlaðinn þegar forritið byrjar. Allt passar saman.

Hvað er athugavert við Realm gagnagrunninn

Við byrjuðum að athuga hvernig innihald gagnagrunnsins breytist yfir líftíma forritsins, frá fyrstu uppsetningu og áfram við virka notkun. Þú getur skoðað innihald Realm gagnagrunnsins í gegnum stetó eða nánar og greinilega með því að opna skrána í gegnum Realm Studio. Til að skoða innihald gagnagrunnsins í gegnum ADB, afritaðu Realm gagnagrunnsskrána:

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

Eftir að hafa skoðað innihald gagnagrunnsins á mismunandi tímum komumst við að því að hlutum af ákveðinni gerð fjölgar stöðugt.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu
Myndin sýnir brot af Realm Studio fyrir tvær skrár: vinstra megin - forritagrunnurinn nokkru eftir uppsetningu, hægra megin - eftir virka notkun. Það má sjá að fjöldi hluta ImageEntity и MoneyType hefur stækkað verulega (skjáskotið sýnir fjölda hluta af hverri gerð).

Tengsl milli vaxtar gagnagrunns og gangsetningartíma

Óstýrður vöxtur gagnagrunns er mjög slæmur. En hvernig hefur þetta áhrif á ræsingartíma forritsins? Það er frekar auðvelt að mæla þetta í gegnum ActivityManager. Frá Android 4.4 birtir logcat annálinn með strengnum Birt og tímann. Þessi tími er jafn bilinu frá því augnabliki sem forritið er ræst þar til virkni flutnings lýkur. Á þessum tíma eiga sér stað eftirfarandi atburðir:

  • Byrjaðu ferlið.
  • Frumstilling hluta.
  • Stofnun og frumstilling starfsemi.
  • Að búa til skipulag.
  • Umsókn flutningur.

Hentar okkur. Ef þú keyrir ADB með -S og -W fánum, geturðu fengið lengri framleiðsla með ræsingartímanum:

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

Ef þú grípur það þaðan grep -i WaitTime tíma geturðu sjálfvirkt söfnun þessarar mælikvarða og skoðað niðurstöðurnar sjónrænt. Grafið hér að neðan sýnir hversu háð ræsingartími forritsins er á fjölda kaldræsinga forritsins.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Á sama tíma var sama eðlis sambandið milli stærðar og vaxtar gagnagrunnsins, sem stækkaði úr 4 MB í 15 MB. Alls kemur í ljós að með tímanum (með vexti kaldræsinga) jókst bæði ræsingartími forritsins og stærð gagnagrunnsins. Við höfum tilgátu í höndunum. Nú var ekki annað eftir en að staðfesta ósjálfstæði. Þess vegna ákváðum við að fjarlægja „lekann“ og sjá hvort þetta myndi flýta fyrir sjósetningunni.

Ástæður fyrir endalausum gagnagrunnsvexti

Áður en þú fjarlægir „leka“ er þess virði að skilja hvers vegna þeir birtust í fyrsta lagi. Til að gera þetta skulum við muna hvað Realm er.

Realm er gagnagrunnur sem ekki er tengdur. Það gerir þér kleift að lýsa tengslum milli hluta á svipaðan hátt og hversu mörgum ORM tengslagagnagrunnum á Android er lýst. Á sama tíma geymir Realm hluti beint í minni með sem minnstum umbreytingum og kortlagningum. Þetta gerir þér kleift að lesa gögn af disknum mjög fljótt, sem er styrkur Realm og hvers vegna það er elskað.

(Fyrir tilgangi þessarar greinar mun þessi lýsing nægja okkur. Þú getur lesið meira um Realm in the cool skjöl eða í þeirra Háskóli).

Margir forritarar eru vanir að vinna meira með venslagagnagrunna (til dæmis ORM gagnagrunna með SQL undir húddinu). Og hlutir eins og að eyða gögnum í fossi virðast oft sjálfgefið. En ekki í Realm.

Við the vegur, Cascade eyðingu eiginleiki hefur verið beðinn um í langan tíma. Þetta endurskoðun и annað, sem tengist henni, var virkur ræddur. Það var tilfinning að það yrði bráðum gert. En þá þýddi allt í innleiðingu sterkra og veikra hlekkja, sem myndi líka sjálfkrafa leysa þetta vandamál. Var nokkuð líflegur og virkur í þessu verkefni draga beiðni, sem hefur verið gert hlé í bili vegna innri erfiðleika.

Gagnaleki án þess að eyða tökum

Hvernig nákvæmlega leka gögn ef þú treystir á að ekki sé til fosseyðing? Ef þú ert með hreiðraða Realm hluti verður að eyða þeim.
Skoðum (næstum) raunverulegt dæmi. Við höfum hlut 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()

Varan í körfunni hefur mismunandi reiti, þar á meðal mynd ImageEntity, sérsniðið hráefni CustomizationEntity. Einnig getur varan í körfunni verið samsett með eigin vörusetti RealmList (CartProductEntity). Allir reitir á lista eru Realm hlutir. Ef við setjum inn nýjan hlut (copyToRealm() / copyToRealmOrUpdate()) með sama auðkenni, þá verður þessum hlut yfirskrifað alveg. En allir innri hlutir (mynd, customizationEntity og cartComboProducts) munu missa tengingu við foreldri og verða áfram í gagnagrunninum.

Þar sem tengingin við þá rofnar, lesum við þau ekki lengur eða eyðum þeim (nema við opnum sérstaklega eða hreinsum alla „töfluna“). Við kölluðum þetta „minnisleka“.

Þegar við vinnum með Realm verðum við beinlínis að fara í gegnum alla þættina og hreinlega eyða öllu fyrir slíkar aðgerðir. Þetta er til dæmis hægt að gera svona:

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

Ef þú gerir þetta, þá mun allt virka eins og það á að gera. Í þessu dæmi gerum við ráð fyrir að það séu engir aðrir hreiðraðir Realm hlutir inni í mynd, customizationEntity og cartComboProducts, svo það eru engar aðrar hreiðrar lykkjur og eyðingar.

„Fljót“ lausn

Það fyrsta sem við ákváðum að gera var að hreinsa upp þá hluti sem vaxa hraðast og athuga niðurstöðurnar til að sjá hvort þetta myndi leysa upprunalega vandamálið okkar. Í fyrsta lagi var einfaldasta og leiðandi lausnin gerð, nefnilega: hver hlutur ætti að bera ábyrgð á að fjarlægja börnin sín. Til að gera þetta kynntum við viðmót sem skilaði lista yfir hreiðra Realm hluti þess:

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

Og við innleiddum það í Realm hlutunum okkar:

@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 við skilum öllum börnum sem flatum lista. Og hver barnhlutur getur líka innleitt NestedEntityAware viðmótið, sem gefur til kynna að hann hafi innri Realm hluti til að eyða, til dæmis 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
   )
 }
}

Og svo framvegis er hægt að endurtaka varp hluta.

Síðan skrifum við aðferð sem eyðir öllum hreiðri hlutum með endurteknum hætti. Aðferð (gerð sem viðbót) deleteAllNestedEntities fær alla efstu hluti og aðferð deleteNestedRecursively Fjarlægir afturkvæmt alla hreiðra hluti með því að nota NestedEntityAware viðmótið:

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

Við gerðum þetta með hlutunum sem vaxa hraðast og athuguðum hvað gerðist.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Þess vegna hættu þessir hlutir sem við huldum með þessari lausn að vaxa. Og heildarvöxtur grunnsins hægði á, en hætti ekki.

"venjulega" lausnin

Þrátt fyrir að grunnurinn hafi farið að vaxa hægar þá stækkaði hann samt. Svo við fórum að leita lengra. Verkefnið okkar nýtir skyndiminni gagna mjög virkan í Realm. Þess vegna er vinnufrekt að skrifa alla hreiðra hluti fyrir hvern hlut, auk þess sem hættan á villum eykst, því þú getur gleymt að tilgreina hluti þegar þú skiptir um kóða.

Ég vildi ganga úr skugga um að ég notaði ekki viðmót, en að allt virkaði af sjálfu sér.

Þegar við viljum að eitthvað virki af sjálfu sér verðum við að nota ígrundun. Til að gera þetta getum við farið í gegnum hvert bekkjarsvæði og athugað hvort það sé Realm hlutur eða listi yfir hluti:

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

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

Ef reiturinn er RealmModel eða RealmList, bætið þá hlut þessa svæðis við lista yfir hreiðra hluti. Allt er nákvæmlega eins og við gerðum hér að ofan, aðeins hér verður það gert af sjálfu sér. Cascade eyðingaraðferðin sjálf er mjög einföld og lítur svona út:

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

Framlenging filterRealmObject síar út og sendir aðeins Realm hluti. Aðferð getNestedRealmObjects með speglun finnur það alla hreiðra Realm hluti og setur þá á línulegan lista. Þá gerum við það sama afturkvæmt. Þegar þú eyðir þarftu að athuga hvort hluturinn sé réttmætur isValid, vegna þess að það getur verið að mismunandi yfirhlutir geti verið með hreiðra eins. Það er betra að forðast þetta og nota einfaldlega sjálfvirka gerð auðkennis þegar þú býrð til nýja hluti.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Full útfærsla á getNestedRealmObjects aðferðinni

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

Fyrir vikið notum við í kóða viðskiptavinar okkar „eyðingu í falli“ fyrir hverja gagnabreytingaaðgerð. Til dæmis, fyrir innsetningaraðgerð lítur hún svona út:

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

Aðferð fyrst getManagedEntities tekur við öllum hlutum sem bætt er við og síðan aðferðina cascadeDelete Eyðir afturkvæmt öllum söfnuðum hlutum áður en nýir eru skrifaðir. Við notum þessa nálgun í gegnum umsóknina. Minnislekar í Realm eru alveg horfin. Eftir að hafa framkvæmt sömu mælingu á háð ræsingartíma á fjölda kaldræsinga forritsins sjáum við niðurstöðuna.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Græna línan sýnir hversu háð ræsingartími forritsins er á fjölda kaldræsinga við sjálfvirka eyðingu í hlaupi á hreiðri hlutum.

Niðurstöður og niðurstöður

Sívaxandi Realm gagnagrunnurinn olli því að forritið ræsti mjög hægt. Við gáfum út uppfærslu með okkar eigin "fallandi eyðingu" af hreiðri hlutum. Og nú fylgjumst við með og metum hvernig ákvörðun okkar hafði áhrif á ræsingartíma forritsins í gegnum _app_start mæligildið.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Til greiningar tökum við 90 daga tímabil og sjáum: ræsingartími forritsins, bæði miðgildi og það sem fellur á 95. hundraðshluta notenda, byrjaði að minnka og hækkar ekki lengur.

Sagan af því hvernig eyðing fossa í Realm vann langa kynningu

Ef þú horfir á sjö daga töfluna lítur _app_start mæligildið alveg fullnægjandi út og er innan við 1 sekúnda.

Það er líka þess virði að bæta við að sjálfgefið sendir Firebase tilkynningar ef miðgildi _app_start fer yfir 5 sekúndur. Hins vegar, eins og við sjáum, ættir þú ekki að treysta á þetta, heldur frekar að fara inn og athuga það beinlínis.

Það sérstaka við Realm gagnagrunninn er að hann er gagnagrunnur sem ekki er tengdur. Þrátt fyrir auðveld notkun þess, líkt við ORM lausnir og tengingar við hluti, þá hefur það ekki fosseyðingu.

Ef þetta er ekki tekið með í reikninginn munu hreiður hlutir safnast upp og „leka í burtu“. Gagnagrunnurinn mun stækka stöðugt, sem aftur mun hafa áhrif á hægagang eða gangsetningu forritsins.

Ég deildi reynslu okkar af því hvernig hægt er að eyða hlutum fljótt í sviðum í Realm, sem er ekki enn komið út úr kassanum, en hefur verið talað um í langan tíma segðu и segðu. Í okkar tilviki flýtti þetta mjög ræsingartíma forritsins.

Þrátt fyrir umræðuna um yfirvofandi útlit þessa eiginleika, þá er fjarvera fosseyðingar í Realm gerð með hönnun. Ef þú ert að hanna nýtt forrit skaltu taka tillit til þess. Og ef þú ert nú þegar að nota Realm, athugaðu hvort þú hafir slík vandamál.

Heimild: www.habr.com

Bæta við athugasemd