Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Watumiaji wote wanakubali uzinduzi wa haraka na kiolesura sikivu katika programu za simu. Ikiwa programu inachukua muda mrefu kuzindua, mtumiaji huanza kujisikia huzuni na hasira. Unaweza kuharibu uzoefu wa mteja kwa urahisi au kupoteza kabisa mtumiaji hata kabla ya kuanza kutumia programu.

Tuligundua mara moja kwamba programu ya Dodo Pizza inachukua sekunde 3 kuzindua kwa wastani, na kwa baadhi ya "waliobahatika" inachukua sekunde 15-20.

Chini ya sehemu hiyo kuna hadithi iliyo na mwisho mzuri: juu ya ukuaji wa hifadhidata ya Realm, uvujaji wa kumbukumbu, jinsi tulivyokusanya vitu vilivyowekwa kwenye kiota, kisha tukajivuta pamoja na kurekebisha kila kitu.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu
Mwandishi wa makala: Maxim Kachinkin β€” Msanidi programu wa Android katika Dodo Pizza.

Sekunde tatu kutoka kwa kubofya ikoni ya programu hadi onResume() ya shughuli ya kwanza ni infinity. Na kwa watumiaji wengine, wakati wa kuanza ulifikia sekunde 15-20. Hii inawezekana vipi?

Muhtasari mfupi sana kwa wale ambao hawana wakati wa kusoma
Hifadhidata yetu ya Realm ilikua bila kikomo. Baadhi ya vitu vilivyowekwa kiota havikufutwa, lakini vilikusanywa kila mara. Muda wa kuanzisha programu uliongezeka polepole. Kisha tukaiweka, na wakati wa kuanza ulikuja kwa lengo - ikawa chini ya sekunde 1 na haikuongezeka tena. Nakala hiyo ina uchambuzi wa hali na suluhisho mbili - moja ya haraka na ya kawaida.

Tafuta na uchambuzi wa shida

Leo, programu yoyote ya simu lazima ianzishwe haraka na iwe sikivu. Lakini si tu kuhusu programu ya simu. Uzoefu wa mtumiaji wa mwingiliano na huduma na kampuni ni jambo ngumu. Kwa mfano, kwa upande wetu, kasi ya utoaji ni moja ya viashiria muhimu vya huduma ya pizza. Ikiwa utoaji ni wa haraka, pizza itakuwa moto, na mteja ambaye anataka kula sasa hatalazimika kusubiri kwa muda mrefu. Kwa maombi, kwa upande wake, ni muhimu kuunda hisia ya huduma ya haraka, kwa sababu ikiwa maombi inachukua sekunde 20 tu ili kuzindua, basi utahitaji muda gani kusubiri pizza?

Mwanzoni, sisi wenyewe tulikabiliwa na ukweli kwamba wakati mwingine maombi yalichukua sekunde chache kuzindua, na kisha tukaanza kusikia malalamiko kutoka kwa wenzetu wengine kuhusu muda gani ilichukua. Lakini hatukuweza kurudia hali hii mara kwa mara.

Ni ndefu kiasi gani? Kulingana na Nyaraka za Google, ikiwa kuanza kwa baridi kwa maombi huchukua chini ya sekunde 5, basi hii inachukuliwa "kama kawaida". Programu ya Android ya Dodo Pizza imezinduliwa (kulingana na vipimo vya Firebase _programu_anza) katika kuanza kwa baridi kwa wastani katika sekunde 3 - "Sio nzuri, sio mbaya," kama wanasema.

Lakini basi malalamiko yakaanza kuonekana kwamba maombi yalichukua muda mrefu sana kuanzishwa! Kuanza, tuliamua kupima "sana, sana, ndefu sana" ni. Na tulitumia ufuatiliaji wa Firebase kwa hili Ufuatiliaji wa kuanza kwa programu.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Ufuatiliaji huu wa kawaida hupima muda kati ya wakati mtumiaji anafungua programu na wakati onResume() ya shughuli ya kwanza inapotekelezwa. Katika Firebase Console kipimo hiki kinaitwa _app_start. Ilibadilika kuwa:

  • Nyakati za kuanza kwa watumiaji zaidi ya asilimia 95 ni karibu sekunde 20 (baadhi hata zaidi), licha ya muda wa wastani wa kuanza kwa baridi kuwa chini ya sekunde 5.
  • Wakati wa kuanza sio thamani ya mara kwa mara, lakini inakua kwa muda. Lakini wakati mwingine kuna matone. Tulipata muundo huu tulipoongeza kipimo cha uchanganuzi hadi siku 90.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Mawazo mawili yalikuja akilini:

  1. Kitu kinavuja.
  2. "Kitu" hiki kinawekwa upya baada ya kutolewa na kisha kuvuja tena.

"Labda kitu na hifadhidata," tulifikiria, na tulikuwa sawa. Kwanza, tunatumia hifadhidata kama kache; wakati wa uhamiaji tunaifuta. Pili, hifadhidata hupakiwa wakati programu inapoanza. Kila kitu kinafaa pamoja.

Ni nini kibaya na hifadhidata ya Realm

Tulianza kuangalia jinsi yaliyomo kwenye hifadhidata yanabadilika katika maisha ya programu, kutoka kwa usakinishaji wa kwanza na zaidi wakati wa matumizi amilifu. Unaweza kutazama yaliyomo kwenye hifadhidata ya Realm kupitia stetho au kwa undani zaidi na kwa uwazi kwa kufungua faili kupitia Studio ya Realm. Ili kuona yaliyomo kwenye hifadhidata kupitia ADB, nakili faili ya hifadhidata ya Realm:

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

Baada ya kuangalia yaliyomo kwenye hifadhidata kwa nyakati tofauti, tuligundua kuwa idadi ya vitu vya aina fulani inaongezeka kila wakati.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu
Picha inaonyesha kipande cha Realm Studio kwa faili mbili: upande wa kushoto - msingi wa programu muda baada ya usakinishaji, upande wa kulia - baada ya matumizi ya kazi. Inaweza kuonekana kuwa idadi ya vitu ImageEntity ΠΈ MoneyType imekua kwa kiasi kikubwa (picha ya skrini inaonyesha idadi ya vitu vya kila aina).

Uhusiano kati ya ukuaji wa hifadhidata na wakati wa kuanza

Ukuaji wa hifadhidata usiodhibitiwa ni mbaya sana. Lakini hii inaathirije wakati wa kuanza kwa programu? Ni rahisi sana kupima hii kupitia ActivityManager. Tangu Android 4.4, logcat huonyesha logi yenye mfuatano Ulioonyeshwa na wakati. Muda huu ni sawa na muda kutoka wakati programu inapozinduliwa hadi mwisho wa uwasilishaji wa shughuli. Katika kipindi hiki, matukio yafuatayo hutokea:

  • Anza mchakato.
  • Uanzishaji wa vitu.
  • Uundaji na uanzishaji wa shughuli.
  • Kuunda mpangilio.
  • Utoaji wa maombi.

Inatufaa. Ukiendesha ADB na -S na -W bendera, unaweza kupata matokeo yaliyopanuliwa na wakati wa kuanza:

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

Ukiinyakua kutoka hapo grep -i WaitTime kwa wakati, unaweza kuhariri mkusanyiko wa kipimo hiki kiotomatiki na kutazama matokeo. Grafu iliyo hapa chini inaonyesha utegemezi wa muda wa kuanza kwa programu kwa idadi ya kuanza kwa baridi kwa programu.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Wakati huo huo, kulikuwa na hali sawa ya uhusiano kati ya ukubwa na ukuaji wa database, ambayo ilikua kutoka 4 MB hadi 15 MB. Kwa jumla, zinageuka kuwa baada ya muda (na ukuaji wa baridi huanza), muda wa uzinduzi wa maombi na ukubwa wa hifadhidata uliongezeka. Tuna hypothesis mikononi mwetu. Sasa kilichobaki ni kuthibitisha utegemezi. Kwa hiyo, tuliamua kuondoa "uvujaji" na kuona ikiwa hii itaharakisha uzinduzi.

Sababu za ukuaji usio na mwisho wa hifadhidata

Kabla ya kuondoa "uvujaji", inafaa kuelewa kwa nini walionekana hapo kwanza. Ili kufanya hivyo, hebu tukumbuke Ulimwengu ni nini.

Realm ni hifadhidata isiyo ya uhusiano. Inakuruhusu kuelezea uhusiano kati ya vitu kwa njia sawa na hifadhidata ngapi za uhusiano za ORM kwenye Android zimefafanuliwa. Wakati huo huo, Realm huhifadhi vitu moja kwa moja kwenye kumbukumbu na kiwango kidogo zaidi cha mabadiliko na michoro. Hii inakuwezesha kusoma data kutoka kwa diski haraka sana, ambayo ni nguvu ya Realm na kwa nini inapendwa.

(Kwa madhumuni ya makala haya, maelezo haya yatatutosha. Unaweza kusoma zaidi kuhusu Realm katika hali nzuri nyaraka au katika zao chuo kikuu).

Watengenezaji wengi wamezoea kufanya kazi zaidi na hifadhidata za uhusiano (kwa mfano, hifadhidata za ORM zilizo na SQL chini ya kofia). Na mambo kama vile kufuta data mara nyingi huonekana kama jambo fulani. Lakini si katika Ufalme.

Kwa njia, kipengele cha kufuta cascade kimeulizwa kwa muda mrefu. Hii marudio ΠΈ mwingine, iliyohusishwa nayo, ilijadiliwa kikamilifu. Kulikuwa na hisia kwamba hivi karibuni itafanywa. Lakini basi kila kitu kilitafsiriwa katika kuanzishwa kwa viungo vikali na dhaifu, ambavyo pia vinaweza kutatua tatizo hili moja kwa moja. Alikuwa mchangamfu na mwenye bidii katika kazi hii ombi la kuvuta, ambayo imesitishwa kwa sasa kutokana na matatizo ya ndani.

Uvujaji wa data bila kufutwa kwa kasi

Je, ni kwa jinsi gani data huvuja ikiwa unategemea ufutaji usiokuwepo? Ikiwa umeweka vipengee vya Realm, basi lazima vifutwe.
Wacha tuangalie (karibu) mfano halisi. Tuna kitu 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()

Bidhaa kwenye gari ina nyanja tofauti, pamoja na picha ImageEntity, viungo vilivyobinafsishwa CustomizationEntity. Pia, bidhaa katika gari inaweza kuwa combo na seti yake ya bidhaa RealmList (CartProductEntity). Sehemu zote zilizoorodheshwa ni vitu vya Realm. Ikiwa tutaingiza kitu kipya (copyToRealm() / copyToRealmOrUpdate()) na kitambulisho sawa, basi kitu hiki kitafutwa kabisa. Lakini vitu vyote vya ndani (picha, customizationEntity na cartComboProducts) vitapoteza muunganisho na mzazi na kubaki kwenye hifadhidata.

Kwa kuwa muunganisho nao umepotea, hatuzisomi tena au kuzifuta (isipokuwa tukizifikia kwa uwazi au kufuta "meza" nzima). Tuliita hii "uvujaji wa kumbukumbu".

Tunapofanya kazi na Realm, lazima tupitie vipengele vyote kwa uwazi na kufuta kila kitu kabla ya shughuli kama hizo. Hii inaweza kufanywa, kwa mfano, kama hii:

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()
}
// ΠΈ ΠΏΠΎΡ‚ΠΎΠΌ ΡƒΠΆΠ΅ сохраняСм

Ikiwa utafanya hivi, basi kila kitu kitafanya kazi kama inavyopaswa. Katika mfano huu, tunadhania kuwa hakuna vipengee vingine vya Realm vilivyowekwa ndani ya picha, customizationEntity, na cartComboProducts, kwa hivyo hakuna vitanzi vingine vilivyowekwa na kufuta.

Suluhisho la "Haraka".

Jambo la kwanza tuliamua kufanya ni kusafisha vitu vinavyokua kwa kasi zaidi na kuangalia matokeo ili kuona ikiwa hii ingesuluhisha shida yetu ya asili. Kwanza, ufumbuzi rahisi zaidi na wa angavu ulifanywa, yaani: kila kitu kinapaswa kuwajibika kwa kuondoa watoto wake. Ili kufanya hivyo, tulianzisha kiolesura ambacho kilirejesha orodha ya vitu vyake vya Realm vilivyowekwa:

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

Na tuliitekeleza katika vitu vyetu vya Ufalme:

@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 tunarudisha watoto wote kama orodha tambarare. Na kila kitu cha mtoto kinaweza pia kutekeleza kiolesura cha NestedEntityAware, kuonyesha kwamba kina vitu vya ndani vya Realm vya kufuta, kwa mfano. 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
   )
 }
}

Na kadhalika, kuota kwa vitu kunaweza kurudiwa.

Kisha tunaandika njia ambayo inafuta tena vitu vyote vilivyowekwa. Njia (iliyotengenezwa kama nyongeza) deleteAllNestedEntities hupata vitu na njia zote za kiwango cha juu deleteNestedRecursively Huondoa kwa kujirudia vitu vyote vilivyowekwa kwa kutumia kiolesura cha 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()
   }
 }
}

Tulifanya hivi na vitu vinavyokua kwa kasi zaidi na kuangalia kilichotokea.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Matokeo yake, vitu hivyo ambavyo tulivifunika na suluhisho hili viliacha kukua. Na ukuaji wa jumla wa msingi ulipungua, lakini haukuacha.

Suluhisho la "kawaida".

Ingawa msingi ulianza kukua polepole zaidi, bado ulikua. Kwa hivyo tulianza kuangalia zaidi. Mradi wetu unatumia kikamilifu uhifadhi wa data katika Realm. Kwa hiyo, kuandika vitu vyote vilivyowekwa kwa kila kitu ni kazi kubwa, pamoja na hatari ya makosa huongezeka, kwa sababu unaweza kusahau kutaja vitu wakati wa kubadilisha msimbo.

Nilitaka kuhakikisha kuwa sikutumia miingiliano, lakini kila kitu kilifanya kazi peke yake.

Tunapotaka kitu kifanye kazi chenyewe, hatuna budi kutumia kutafakari. Ili kufanya hivyo, tunaweza kupitia kila uwanja wa darasa na kuangalia ikiwa ni kitu cha Realm au orodha ya vitu:

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

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

Ikiwa uga ni RealmModel au RealmList, basi ongeza kipengee cha sehemu hii kwenye orodha ya vitu vilivyowekwa. Kila kitu ni sawa na tulivyofanya hapo juu, hapa tu kitafanywa peke yake. Njia ya kufuta cascade yenyewe ni rahisi sana na inaonekana kama hii:

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

Ugani filterRealmObject huchuja na kupitisha vitu vya Realm pekee. Njia getNestedRealmObjects kupitia kutafakari, hupata vitu vyote vya Realm vilivyowekwa na kuviweka kwenye orodha ya mstari. Kisha tunafanya vivyo hivyo kwa kujirudia. Wakati wa kufuta, unahitaji kuangalia kitu kwa uhalali isValid, kwa sababu inaweza kuwa vitu tofauti vya wazazi vinaweza kuwa na viota vinavyofanana. Ni bora kuepuka hili na kutumia tu kizazi cha id wakati wa kuunda vitu vipya.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Utekelezaji kamili wa mbinu ya 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)
}

Kwa hivyo, katika msimbo wetu wa mteja tunatumia "cascading delete" kwa kila operesheni ya kurekebisha data. Kwa mfano, kwa operesheni ya kuingiza inaonekana kama hii:

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

Mbinu kwanza getManagedEntities inapokea vitu vyote vilivyoongezwa, na kisha njia cascadeDelete Hufuta tena vitu vyote vilivyokusanywa kabla ya kuandika vipya. Tunaishia kutumia njia hii katika programu yote. Uvujaji wa kumbukumbu katika Realm umetoweka kabisa. Baada ya kufanya kipimo sawa cha utegemezi wa wakati wa kuanza kwa idadi ya kuanza kwa baridi ya programu, tunaona matokeo.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Mstari wa kijani kibichi unaonyesha utegemezi wa muda wa kuanza kwa programu kwa idadi ya baridi kuanza wakati wa kufuta kiotomatiki kwa vitu vilivyowekwa.

Matokeo na hitimisho

Hifadhidata ya Realm inayokua kila wakati ilikuwa ikisababisha programu kuzinduliwa polepole sana. Tulitoa sasisho na "cascading delete" yetu ya vitu vilivyowekwa. Na sasa tunafuatilia na kutathmini jinsi uamuzi wetu ulivyoathiri muda wa kuanza maombi kupitia _app_start metric.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Kwa uchanganuzi, tunachukua muda wa siku 90 na kuona: muda wa uzinduzi wa programu, wastani na ule unaoangukia asilimia 95 ya watumiaji, ulianza kupungua na haukupanda tena.

Hadithi ya jinsi ufutaji wa kasino katika Realm ulivyoshinda kwenye uzinduzi wa muda mrefu

Ukiangalia chati ya siku saba, _app_start metriki inaonekana ya kutosha kabisa na ni chini ya sekunde 1.

Inafaa pia kuongeza kuwa, kwa chaguomsingi, Firebase hutuma arifa ikiwa thamani ya wastani ya _app_start inazidi sekunde 5. Walakini, kama tunavyoona, haupaswi kutegemea hii, lakini ingia na uikague kwa uwazi.

Jambo maalum kuhusu hifadhidata ya Realm ni kwamba ni hifadhidata isiyo ya uhusiano. Licha ya urahisi wa matumizi, kufanana kwa ufumbuzi wa ORM na kuunganisha kitu, haina ufutaji wa kasino.

Ikiwa hii haitazingatiwa, basi vitu vilivyowekwa kwenye kiota vitajilimbikiza na "kuvuja." Hifadhidata itakua kila wakati, ambayo itaathiri kupungua au kuanza kwa programu.

Nilishiriki uzoefu wetu juu ya jinsi ya kufanya ufutaji wa haraka wa vitu kwenye Realm, ambao bado haujatoka kwenye boksi, lakini umezungumziwa kwa muda mrefu. wanasema ΠΈ wanasema. Kwa upande wetu, hii iliharakisha sana wakati wa kuanza kwa programu.

Licha ya majadiliano juu ya kuonekana karibu kwa kipengele hiki, kutokuwepo kwa ufutaji wa kasino katika Realm hufanywa na muundo. Ikiwa unaunda programu mpya, basi uzingatie hili. Na ikiwa tayari unatumia Realm, angalia ikiwa una shida kama hizo.

Chanzo: mapenzi.com

Kuongeza maoni