ऐप्लिकेशन कोड मेमोरी है

लिखा गया कोड, मेमोरी का इस्तेमाल करने का एक तरीका है. जब आपका ऐप्लिकेशन चलता है, तब उसमें मौजूद हर क्लास, तरीके, और स्ट्रिंग कॉन्स्टेंट को रैम में लोड किया जाना चाहिए. आपके ऐप्लिकेशन का कोड बेस जितना बड़ा होगा, उसे चालू रखने के लिए उतनी ही ज़्यादा मेमोरी की ज़रूरत होगी.

फ़ाइल-बैक मेमोरी और डिमांड पेजिंग

Android, mmap का इस्तेमाल करके, आपके .apk से एक्ज़ीक्यूटेबल कोड लोड करता है. जैसे, .oat या .so फ़ाइलें. इसका मतलब है कि कोड को फ़ाइल में सेव किया गया है.

Android, डिमांड पेजिंग का इस्तेमाल करता है. जब आपका ऐप्लिकेशन शुरू होता है, तो कर्नल पूरे APK को तुरंत रैम में लोड नहीं करता है. इसके बजाय, यह फ़ाइल को सिर्फ़ प्रोसेस के वर्चुअल पता स्पेस में मैप करता है. जब आपका ऐप्लिकेशन चलता है और सीपीयू किसी नए फ़ंक्शन पर जाता है, तो यह "पेज फ़ॉल्ट" को ट्रिगर करता है. कर्नेल थ्रेड को रोकता है. इसके बाद, स्टोरेज से कोड के उस 4 केबी पेज को फ़िज़िकल रैम में पढ़ता है और फिर से एक्ज़ीक्यूशन शुरू करता है.

डिमांड पेजिंग को दिखाने वाला डायग्राम. इसमें दिखाया गया है कि वर्चुअल पेजों को फ़िज़िकल रैम पेजों पर सिर्फ़ तब मैप किया जाता है, जब उन्हें ऐक्सेस किया जाता है

इसका मतलब है कि जिस कोड को पैक किया जाता है, लेकिन कभी एक्ज़ीक्यूट नहीं किया जाता है वह कोड पेजों के लिए फ़िज़िकल मेमोरी का इस्तेमाल नहीं करता है. हालांकि, इस्तेमाल न की गई लाइब्रेरी की वजह से, APK का साइज़ बढ़ जाता है. साथ ही, इससे सिस्टम के इंटरनल मेटाडेटा (जैसे, DEX इंडेक्स और क्लास डिस्क्रिप्टर) के लिए इस्तेमाल की जाने वाली मेमोरी काफ़ी बढ़ सकती है. इस कोड के बारे में जानने के लिए भी, इस मेटाडेटा को पढ़ना ज़रूरी है. इसके अलावा, कई लाइब्रेरी में स्टैटिक इनिशियलाइज़र होते हैं या ऐप्लिकेशन के स्टार्टअप के दौरान, डिपेंडेंसी इंजेक्शन फ़्रेमवर्क से जुड़े होते हैं. इस वजह से, उन्हें किसी भी तरह से रैम में पेज किया जाता है.

पेज को हटाना और पेज लोड होने में लगने वाला समय बढ़ना

फ़ाइल-बैक मेमोरी को स्टोरेज से हमेशा फिर से पढ़ा जा सकता है. इसलिए, कर्नल इन पेजों को "क्लीन" मानता है. जब सिस्टम पर मेमोरी का दबाव पड़ता है, तो कर्नल इन क्लीन कोड पेजों को RAM से हटा देता है, ताकि दूसरी चीज़ों के लिए जगह बनाई जा सके.

अगर आपके ऐप्लिकेशन को बाद में उस कोड को फिर से लागू करने की ज़रूरत पड़ती है, तो सीपीयू में गड़बड़ी होगी. साथ ही, कर्नल को स्टोरेज से पेज को फिर से पढ़ना होगा. आपके ऐप्लिकेशन में जितना ज़्यादा कोड होगा, उसके कोड को हटाए जाने की आशंका उतनी ही ज़्यादा होगी. जब कोई उपयोगकर्ता, दूसरे ऐप्लिकेशन इस्तेमाल करने के बाद आपके बड़े साइज़ वाले ऐप्लिकेशन पर वापस आता है, तो उसे अचानक रुकने और धीमे चलने की समस्या का सामना करना पड़ता है. ऐसा इसलिए होता है, क्योंकि सीपीयू लगातार स्टोरेज से कोड को वापस पेज में लाने का इंतज़ार करता रहता है.

पेज फ़ॉल्ट की लागत: यह डिवाइस की स्टोरेज स्पीड (यूएफ़एस बनाम ईएमएमसी) और कर्नल की स्थिति के आधार पर अलग-अलग होती है. हालांकि, एक बड़े पेज फ़ॉल्ट (स्टोरेज से 4 केबी पढ़ना) की लागत 0.5 मि॰से॰ से 5 मि॰से॰ तक हो सकती है. अगर आपका स्टार्टअप पाथ, बिना ऑप्टिमाइज़ किए गए कोड के 500 अलग-अलग पेजों को छूता है, तो ऐप्लिकेशन के स्टार्टअप टाइम में कई सौ मिलीसेकंड की I/O लेटेन्सी आसानी से आ सकती है.

Compiler Explorer की मदद से कोड के साइज़ के बारे में जानना

आपका Java या Kotlin कोड, नेटिव मशीन कोड (और इस तरह मेमोरी बाइट) में कैसे बदलता है, यह समझने के लिए Compiler Explorer का इस्तेमाल किया जा सकता है.

Android के लिए सहायता सीधे तौर पर Godbolt में बनाई गई है. इससे आपको यह देखने में मदद मिलती है कि Android टूलचेन (D8, R8, और dex2oat) के अलग-अलग हिस्से, आपके सोर्स कोड को कैसे बदलते हैं.

Android के साथ Compiler Explorer का इस्तेमाल कैसे करें

  1. godbolt.org पर जाएं.
  2. भाषा के ड्रॉपडाउन (सबसे ऊपर बाएं कोने में) से, Android Java या Android Kotlin चुनें.
  3. कंपाइलर ड्रॉपडाउन (कोड पैन में सबसे ऊपर दाईं ओर) में, अलग-अलग टूल में से कोई एक चुना जा सकता है:
    • d8: इसमें Dalvik bytecode (.dex) दिखता है. यह आपके ओरिजनल कोड के सबसे करीब होता है और इसे पढ़ना आसान होता है.
    • r8: इससे पता चलता है कि R8 ऑप्टिमाइज़र, आपके बाइटकोड को कैसे छोटा और ऑप्टिमाइज़ करता है.
    • dex2oat: यह फ़ाइनल ARM64 मशीन कोड दिखाता है, जो डिवाइस पर काम करता है. यहां आपको मेमोरी पर पड़ने वाले असर की सही जानकारी दिखेगी. हर निर्देश के लिए 4 बाइट. dex2oat अलग-अलग आईएसए को टारगेट कर सकता है, लेकिन मोबाइल फ़ोन के लिए ARM64 सबसे आम है.
  4. सोर्स<>आउटपुट हाइलाइट करना: कोड की किसी लाइन पर कर्सर घुमाने से, उससे जुड़े बाइटकोड या मशीन कोड के निर्देश हाइलाइट हो जाएंगे. इससे, किसी स्टेटमेंट के असर को आसानी से ट्रैक किया जा सकेगा.
  5. ऑप्टिमाइज़ेशन पाइपलाइन: डिसअसेंबली व्यू में, नया जोड़ें... पर क्लिक किया जा सकता है -> ऑप्ट पाइपलाइन. इससे आपको कंपाइलर के इंटरनल चरणों को देखने की अनुमति मिलती है. आपके पास यह देखने का विकल्प होता है कि हर चरण में इंटरनल रिप्रेजेंटेशन (आईआर) कैसे बदलता है.जैसे, "इनलाइनर (पहले)" और "इनलाइनर (बाद में)" चरणों के बीच. ऐसा तब होता है, जब आईआर को फ़ाइनल ARM64 मशीन कोड में बदला जाता है.

Compiler Explorer के यूज़र इंटरफ़ेस (यूआई) का स्क्रीनशॉट. इसमें एक सैंपल प्रोग्राम और उसके dex2oat आउटपुट की डिसअसेंबली और ऑप्टिमाइज़ेशन पाइपलाइन दिखाई गई है. साथ ही, इनलाइनिंग का चरण दिखाया गया है

याददाश्त के लिए यह क्यों ज़रूरी है

आपको dex2oat में ARM64 ISA को टारगेट करने वाला हर निर्देश, आपके ऐप्लिकेशन की एक्ज़ीक्यूटेबल (.odex या .oat) फ़ाइल में 4 बाइट लेता है.

अलग-अलग भाषाओं की सुविधाओं का इस्तेमाल करने वाला कोड डालें और कंपाइलर के आउटपुट की जांच करें:

  • ऐरे ऐक्सेस बनाम सूची के इटरेटर:
    • int[] पर एक सामान्य ऐरे लूप को ~10 निर्देशों (~40 बाइट) में कंपाइल किया जा सकता है.
    • List पर foreach लूप, Iterator का इस्तेमाल करता है. इस वजह से, 30 से 40 निर्देश (~160 बाइट) मिल सकते हैं. ऐसा इसलिए होता है, क्योंकि इसमें अतिरिक्त तरीके से कॉल (hasNext(), next()) किए जाते हैं और इटरेटर ऑब्जेक्ट को भी असाइन किया जाता है.
    • R8 ऑप्टिमाइज़ेशन: सही शर्तों के तहत (जैसे, जब List को ArrayList के तौर पर इस्तेमाल किया जाता है), R8 ऑप्टिमाइज़र, foreach लूप को इंडेक्स किए गए सामान्य लूप में बदल सकता है.इससे इटरेटर ओवरहेड खत्म हो जाता है. साथ ही, कोड का साइज़ और रनटाइम मेमोरी चर्न, दोनों कम हो जाते हैं.
  • वर्चुअल तरीके से फ़ंक्शन कॉल करना: इसमें ऑब्जेक्ट की क्लास लोड करना, vtable में तरीका ढूंढना, और फिर ब्रांचिंग करना शामिल है. आम तौर पर, इसमें चार से पांच निर्देश (~20 बाइट) लगते हैं.
  • डायरेक्ट/स्टैटिक कॉल: इनका अनुवाद अक्सर एक bl (लिंक वाला ब्रांच) निर्देश (4 बाइट) में होता है.
  • Kotlin Lambdas: पूरी तरह से अनाम क्लास और अतिरिक्त ब्रिज मेथड जनरेट कर सकते हैं. इससे, सामान्य फ़ंक्शनल ब्लॉक के लिए सैकड़ों बाइट का कोड और मेटाडेटा ओवरहेड जुड़ जाता है.

Compiler Explorer का इस्तेमाल करके, यह देखा जा सकता है कि भाषा की बेहतर सुविधाओं (जैसे, Kotlin लैम्डा, स्ट्रीम एपीआई या जेनेरिक का ज़्यादा इस्तेमाल) से, आपके ऐप्लिकेशन के कंपाइल किए गए फ़ाइनल साइज़ पर क्या असर पड़ता है. साथ ही, यह भी देखा जा सकता है कि R8 जैसे ऑप्टिमाइज़र, कुछ मामलों में भाषा के ऐब्स्ट्रैक्शन की लागत को कैसे कम कर सकते हैं. इस टूल की मदद से, ऐप्लिकेशन को डिज़ाइन और लागू करने के दौरान, सोच-समझकर फ़ैसले लिए जा सकते हैं.

आम तौर पर, आपके ऐप्लिकेशन के कोड में ज़्यादा जटिलता होने पर, मेमोरी का इस्तेमाल ज़्यादा होता है. इसके उलट, आसान कोड या R8 की मदद से आसान बनाए गए कोड से, सीपीयू के निर्देशों और स्टोरेज और रैम में बाइट के तौर पर कम जगह घेरने वाला कोड बनता है.

meminfo और showmap की मदद से, कोड के असर का आकलन करना

Android के स्टैंडर्ड मेमोरी टूल का इस्तेमाल करके, यह देखा जा सकता है कि आपके ऐप्लिकेशन का कोड कितनी मेमोरी इस्तेमाल कर रहा है.

dumpsys meminfo

adb shell dumpsys meminfo <package> चलाने पर, ऐप्लिकेशन की खास जानकारी सेक्शन में मौजूद कोड कैटगरी में, कोड से जुड़ी मेमोरी की खास जानकारी मिलती है:

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     3244
         Native Heap:     5412
                Code:    24512  # <--- Sum of .so, .dex, .oat, .art, etc.

showmap

ज़्यादा विस्तृत व्यू के लिए, showmap का इस्तेमाल करें. इससे उन फ़ाइलों के बारे में पता चलता है जिन्हें मेमोरी में मैप किया जा रहा है.

adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"

आपको अपने ऐप्लिकेशन के कंपाइल किए गए कोड के लिए ये एंट्री दिखेंगी:

   size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
  12288     8192     8192     8192        0        0        0        0        0 /data/app/.../base.odex

डेड कोड और R8

हर तरीके को लागू करने के लिए मेमोरी की ज़रूरत होती है. इसलिए, अगर किसी ऐप्लिकेशन में गैर-ज़रूरी लाइब्रेरी या इनिशियलाइज़ेशन मौजूद हैं, तो इससे स्टार्टअप परफ़ॉर्मेंस और मेमोरी के इस्तेमाल पर बुरा असर पड़ सकता है.

इसलिए, R8 (ProGuard) जैसे टूल बहुत ज़रूरी हैं. R8, आपके ऐप्लिकेशन के बाइटकोड का विश्लेषण करता है. साथ ही, उन क्लास या तरीकों को हटा देता है जिन्हें कभी कॉल नहीं किया जाता ("डेड कोड स्ट्रिपिंग").

प्रैक्टिकल: ब्लोट की लागत

कोड के साइज़ का असर दिखाने के लिए, एक ऐसे एक्सपेरिमेंट पर विचार करें जिसमें एक ऐप्लिकेशन के दो बिल्ड की तुलना की जा रही है. इस ऐप्लिकेशन में जनरेट की गई 300 क्लास हैं. हर क्लास में 500 तरीके हैं:

  • CodeBloat (Unoptimized): The standard, unoptimized build containing all generated classes and unique strings.
  • CodeBloatOptimized: यह वही सोर्स कोड है, लेकिन इसे R8 की श्रिंकिंग सुविधा चालू करके कंपाइल किया गया है.

1. पहले से कंपाइल करने की सुविधा (एओटी)

फ़ाइल-बैक मेमोरी का ज़्यादा से ज़्यादा फ़ायदा पाने के लिए, हम cmd package compile टूल का इस्तेमाल करेंगे. इससे ऐप्लिकेशन को .oat फ़ाइलों में पहले से ही कंपाइल किया जा सकेगा.

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized

कृपया ध्यान दें कि यह एक बनावटी उदाहरण है. आम तौर पर, ऐप्लिकेशन speed-profile कंपाइलेशन मोड का इस्तेमाल करते हैं. इसके बारे में यहां ज़्यादा जानें.

2. लॉन्च करना और तुलना करना

ट्रूली कोल्ड स्टार्ट देखने के लिए, सिस्टम को स्टोरेज से कोड पढ़ना होगा. इसलिए, हम हर ऐप्लिकेशन को लॉन्च करने से पहले, कर्नल के पेज कैश को हटा देंगे. इसके लिए, रूट ऐक्सेस की ज़रूरत होती है.

बिना ऑप्टिमाइज़ किए गए ऐप्लिकेशन को लॉन्च करें:

adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat

अब ऑप्टिमाइज़ किए गए ऐप्लिकेशन के लिए भी यही तरीका अपनाएं:

adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
नतीजे

अगर App Summary सेक्शन में कोड लाइन को देखा जाए, तो आपको बहुत बड़ा अंतर दिखेगा:

  • ऑप्टिमाइज़ नहीं किया गया Code: ~30,000 केबी (30 एमबी)
  • ऑप्टिमाइज़ किया गया Code: ~2,000 केबी (2 एमबी)

R8 ने यह तय किया कि उन क्लास में मौजूद 500 तरीके कभी भी किसी काम के नहीं थे. doSomething() तरीका सिर्फ़ method0() को कॉल करता है और नतीजों को अनदेखा किया जाता है. इसलिए, R8 ने आर्टिफ़िशियल इंटेलिजेंस से जनरेट किए गए लगभग सभी कोड को फ़ाइनल APK से हटा दिया.

3. Perfetto में असर देखना

कोड ब्लोट का असर, ऐप्लिकेशन के लोड होने के शुरुआती चरण में साफ़ तौर पर दिखता है. खास तौर पर, मुख्य थ्रेड पर bindApplication स्लाइस और madvising से शुरू होने वाले नेस्ट किए गए स्लाइस ढूंढें. इनसे पता चलता है कि सिस्टम, APK और उसके कंपाइल किए गए कोड (.odex) से फ़ाइलें लोड करने की तैयारी कर रहा है.

इंटरैक्टिव कोल्ड स्टार्ट के दौरान, सिस्टम इन फ़ाइलों से mmap() और madvise() कोड और अन्य डेटा को लोड करेगा. यह डेटा, ऐप्लिकेशन को लोड और चलाने के लिए ज़रूरी होता है. madvising स्लाइस में "size=" के बाद मौजूद वैल्यू से पता चलता है कि कितना डेटा लोड करना है. ऐप्लिकेशन के कोड को पहले से फ़ेच करने की सुविधा इसलिए दी जाती है, ताकि ऐप्लिकेशन को तेज़ी से चालू किया जा सके.

तुलना से पता चलता है कि बड़े साइज़ वाले ऐप्लिकेशन के मामले में, स्टोरेज से रैम में लोड किए जाने वाले ऐप्लिकेशन कोड की मात्रा बहुत ज़्यादा थी. इस वजह से, ऐप्लिकेशन को शुरू होने में ज़्यादा समय लगा. इसके अलावा, बड़े साइज़ वाले ऐप्लिकेशन के शुरू होने के ट्रेस से पता चलता है कि सेकंडरी DEX फ़ाइलों को लोड करने के लिए स्लाइस (classes2.dex, classes3.dex) का इस्तेमाल किया गया था. इन स्लाइस को बड़े साइज़ वाले ऐप्लिकेशन में "स्पिल" करना पड़ा, क्योंकि ये एक DEX फ़ाइल में फ़िट नहीं हो सकते थे.

तुलना (Pixel 10a पर कोल्ड स्टार्ट)
मेट्रिक ऑप्टिमाइज़ नहीं किया गया (CodeBloat) ऑप्टिमाइज़ किया गया (CodeBloatOptimized)
base.odex madvise size ~7.9 एमबी (2.0 मि॰से॰) ~16 केबी (0.003 मि॰से॰)
base.apk madvise size ~2.4 एमबी (2.4 मि॰से॰) ~4 केबी (0.001 मि॰से॰)
classes2.dex madvise size ~7.3 एमबी (8.6 मि॰से॰) लागू नहीं
classes3.dex madvise size ~7.3 एमबी (8.0 मि॰से॰) लागू नहीं
कुल madvising अवधि ~21 मि॰से॰ ~0.004 मि॰से॰
ऐप्लिकेशन लोड होने की परफ़ॉर्मेंस ऑप्टिमाइज़ नहीं की गई है

Perfetto के यूज़र इंटरफ़ेस (यूआई) का स्क्रीनशॉट. इसमें com.android.codebloat प्रोसेस के साथ-साथ प्राइमरी और सेकंडरी DEX फ़ाइलों के लिए madvising स्लाइस दिखाए गए हैं

ऐप्लिकेशन को लोड करने की परफ़ॉर्मेंस को ऑप्टिमाइज़ किया गया

Perfetto के यूज़र इंटरफ़ेस (यूआई) का स्क्रीनशॉट. इसमें com.android.codebloat.optimized प्रोसेस को एक छोटी सी madvising स्लाइस के साथ दिखाया गया है

कोड ब्लोट का असर, ऐप्लिकेशन के साइज़, उपयोगकर्ता के डिवाइस की विशेषताओं, और सिस्टम लोड के हिसाब से अलग-अलग होता है.

लोडिंग के विश्लेषण के लिए PerfettoSQL

अपने ट्रेस से इन मेट्रिक को एक्सट्रैक्ट करने के लिए, इन क्वेरी का इस्तेमाल किया जा सकता है.

1. ऐप्लिकेशन स्टार्टअप की अवधि

इससे यह पता चलता है कि किसी ऐप्लिकेशन की ऐक्टिविटी लॉन्च होने से लेकर, ऐक्टिविटी के पहले फ़्रेम के दिखने तक कितना समय लगता है.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';

देखें: ऐप्लिकेशन के स्टार्टअप की अलग-अलग स्थितियों के बारे में जानकारी

किसी ऐप्लिकेशन के स्टार्टअप में लगने वाला समय, इस गाइड में बताई गई बातों के अलावा कई अन्य बातों पर भी निर्भर करता है!

2. madvising साइज़ और अवधि की जानकारी निकालना

इस क्वेरी में, ऊपर दिए गए madvising हिस्से को ज़ूम इन किया गया है.

INCLUDE PERFETTO MODULE slices.with_context;

SELECT
  name,
  dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
  AND name LIKE 'madvising %';
3. मुख्य थ्रेड की स्थिति का ब्रेकडाउन (हर स्थिति के हिसाब से कुल अवधि)

इस क्वेरी से पता चलता है कि ऐप्लिकेशन की मुख्य थ्रेड ने अलग-अलग स्थितियों में कितना समय बिताया.

SELECT
  p.name AS process_name,
  state,
  sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
GROUP BY p.name, state;

ऐप्लिकेशन के स्टार्टअप के दौरान, सिर्फ़ मुख्य थ्रेड की स्थितियों को देखने के लिए क्वेरी को बेहतर बनाया जा सकता है.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT
  p.name AS process_name,
  ts.state,
  -- Calculate only the duration that falls within the startup window
  SUM(
    MAX(0,
      MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
    )
  ) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
  -- Only select thread states that overlap with the startup interval
  AND ts.ts + ts.dur > s.ts
  AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;

इससे कुछ दिलचस्प समस्याएं सामने आ सकती हैं. जैसे:

  • ऐप्लिकेशन के रन करने के लिए ज़्यादा समय उपलब्ध है, लेकिन वह रन नहीं हो रहा है (R): इससे पता चलता है कि सीपीयू के इस्तेमाल की वजह से, ऐप्लिकेशन को शुरू होने में समय लगा. इसका मतलब है कि ऐप्लिकेशन का मुख्य थ्रेड रन नहीं हो सका, क्योंकि अन्य थ्रेड (ऐसा हो सकता है कि वे अन्य ऐप्लिकेशन के हों) सीपीयू का इस्तेमाल कर रहे थे.
  • इंटरप्ट की जा सकने वाली स्लीप (D) में ज़्यादा समय बिताया गया: आम तौर पर, इससे पता चलता है कि I/O धीमा है या मेमोरी पर दबाव है. इस वजह से, ऐप्लिकेशन शुरू नहीं हो पा रहा है.
  • स्लीपिंग (S) में ज़्यादा समय बिताया गया: इसका मतलब है कि मुख्य थ्रेड, अन्य थ्रेड के काम पूरा करने का इंतज़ार कर रही थी. कभी-कभी इससे ऐप्लिकेशन के स्टार्टअप पाथ में लॉक कंटेंशन का पता चलता है. इसका मतलब है कि मुख्य थ्रेड को किसी ऐसी एक्सक्लूसिव रिसॉर्स पर ब्लॉक कर दिया गया था जिस पर ऐप्लिकेशन की किसी दूसरी थ्रेड का कब्ज़ा था.
4. फ़ाइल के साथ जुड़ी ज़्यादा से ज़्यादा मेमोरी (आरएसएस फ़ाइल)

यह मेट्रिक, इस बात से काफ़ी हद तक जुड़ी होती है कि ऐप्लिकेशन स्टार्टअप के समय कितना कोड और डेटा लोड करता है. "ब्लॉटेड" ऐप्लिकेशन का मतलब है कि ऐप्लिकेशन में ज़रूरत से ज़्यादा फ़ंक्शन शामिल हैं. ऐसे ऐप्लिकेशन का स्कोर ज़्यादा होगा. इससे सिस्टम पर मेमोरी का दबाव बढ़ेगा. इस वजह से, ऐप्लिकेशन को शुरू होने में समय लग सकता है. ऐसा इसलिए होता है, क्योंकि सिस्टम को मेमोरी के अनुरोधों को पूरा करने में परेशानी होती है. इसके अलावा, सिस्टम, ऐप्लिकेशन को शुरू करने के बजाय, सीपीयू के समय को अन्य प्रोसेस से मेमोरी वापस पाने में लगाता है, ताकि ऐप्लिकेशन की शुरुआती ज़रूरतों को पूरा किया जा सके.

SELECT
  p.name AS process_name,
  max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.name = 'mem.rss.file'
GROUP BY p.name;

एआरटी कंपाइलेशन मोड और मेमोरी

Android Runtime (ART), आपके ऐप्लिकेशन के कोड को कई अलग-अलग मोड में कंपाइल कर सकता है. इन्हें कंपाइलर फ़िल्टर भी कहा जाता है. चुने गए कंपाइलर फ़िल्टर का सीधा असर, आपके ऐप्लिकेशन के मेमोरी फ़ुटप्रिंट पर पड़ता है.

  • verify: ART सिर्फ़ बाइटकोड की पुष्टि करता है. कोई एओटी कंपाइलेशन नहीं किया जाता. कोड को इंटरप्रेटर के ज़रिए लागू किया जाता है या रनटाइम में JIT कंपाइलर से कंपाइल किया जाता है.
    • मेमोरी पर असर: डिस्क पर सबसे छोटा साइज़. नेटिव कोड की मेमोरी के इस्तेमाल को JIT Cache (बिना सोर्स फ़ाइल वाली डर्टी मेमोरी) में पुश किया जाता है.
  • speed: ART, सभी तरीकों का पूरा एओटी कंपाइलेशन करता है.
    • मेमोरी पर असर: सबसे बड़ा .odex साइज़. इससे फ़ाइल-बैक (क्लीन) मेमोरी का ज़्यादा से ज़्यादा इस्तेमाल किया जा सकता है.
  • speed-profile: एआरटी सिर्फ़ उन तरीकों को कंपाइल करता है जिन्हें JIT प्रोफ़ाइल में "हॉट" के तौर पर मार्क किया गया है.
    • मेमोरी पर असर: बैलेंस तरीके से मेमोरी सेव की जाएगी. सिर्फ़ सबसे ज़रूरी कोड को AOT कंपाइल किया जाता है.

सबसे ज़्यादा इस्तेमाल किया जाने वाला फ़िल्टर speed-profile है. इसका इस्तेमाल, उपयोगकर्ता के ऐप्लिकेशन इंस्टॉल करते समय किया जाता है. इसे सिस्टम प्रॉपर्टी pm.dexopt.install और pm.dexopt.bg-dexopt में कॉन्फ़िगर किया जाता है. आम तौर पर, इसे build/make/target/product/runtime_libart.mk में सेट किया जाता है.

कुछ सिस्टम ऐप्लिकेशन, speed कंपाइलेशन का इस्तेमाल करेंगे. साथ ही, सिस्टम इमेज बनाने के बिल्ड प्रोसेस में लगने वाले समय भी कंपाइल करेंगे. verify का इस्तेमाल आम तौर पर, सिर्फ़ डेवलपमेंट के इस्तेमाल के उदाहरणों में किया जाता है.

इस्तेमाल का उदाहरण सामान्य कंपाइलर फ़िल्टर
डेवलेपमेंट verify
सिस्टम इमेज speed
उपयोगकर्ता ऐप्लिकेशन speed-profile

प्रैक्टिकल: कंपाइलेशन मोड और मेमोरी

हम CodeBloat ऐप्लिकेशन का इस्तेमाल करके यह देख सकते हैं कि इन फ़िल्टर का मेमोरी पर क्या असर पड़ता है. इन मेज़रमेंट को फिर से बनाने के लिए:

  1. ऐप्लिकेशन को टारगेट मोड में फिर से कंपाइल करें.
  2. ऐप्लिकेशन को ज़बरदस्ती बंद करें और फिर से चालू करें.
  3. बैकग्राउंड थ्रेड के क्लास को टच करने की प्रोसेस पूरी होने का इंतज़ार करें. इसके लिए, logcat देखें या पांच सेकंड इंतज़ार करें.
  4. adb shell dumpsys meminfo com.android.codebloat रन करें.

मोड: verify (एओटी नहीं है)

adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

verify मोड में, ऐप्लिकेशन की खास जानकारी में यह दिखता है: * कोड पीएसएस: ~8,000 केबी * डैलविक अन्य (जेआईटी): ~25,000 केबी

कोई भी कोड एओटी कंपाइल नहीं किया जाता है. इसलिए, रनटाइम को हॉट तरीकों को JIT कैश में JIT-कंपाइल करना होगा. यह डर्टी ऐनॉनमस मेमोरी (Dalvik Other) के तौर पर दिखता है.

मोड: speed (फ़ुल एओटी)

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

speed मोड में, नतीजे काफ़ी बदल जाते हैं: * कोड पीएसएस: ~24,000 केबी * Dalvik Other (JIT): ~5,000 केबी

ऐप्लिकेशन के कोड को अब .odex फ़ाइल से clean file-backed memory के तौर पर मैप किया गया है. इससे JIT कैश पर दबाव कम होता है. साथ ही, मेमोरी को "डर्टी" रैम के तौर पर "स्टक" होने के बजाय, दबाव में निकालने के लिए उपलब्ध कराया जाता है.

मोड: speed-profile (चुनिंदा एओटी)

मॉडर्न ऐप्लिकेशन, baseline.prof बेसलाइन प्रोफ़ाइल को बंडल कर सकते हैं. ART इसका इस्तेमाल, सिर्फ़ उस कोड को चुनिंदा तौर पर कंपाइल करने के लिए करता है जिसकी ज़रूरत तेज़ी से और कम मेमोरी में स्टार्टअप के लिए होती है.

इस अभ्यास में, हम ऐप्लिकेशन के स्टार्टअप क्लास की सूची बनाने के लिए, एक बेसलाइन प्रोफ़ाइल बनाएंगे. हालांकि, कंपाइलर को बाहरी सोर्स से भी प्रोफ़ाइलें मिल सकती हैं. जैसे, ऐप्लिकेशन स्टोर ("क्लाउड प्रोफ़ाइलें") से. इससे ऐप्लिकेशन के लिए, क्राउडसोर्स की गई JIT प्रोफ़ाइलें मिल सकती हैं. भले ही, डेवलपर ने जनरेट की गई बेसलाइन प्रोफ़ाइल को बंडल किया हो या नहीं.

डिवाइस पर प्रोफ़ाइलें जनरेट करना और उनका इस्तेमाल करना

speed-profile के असर को देखने के लिए, डिवाइस पर अपनी प्रोफ़ाइल जनरेट करें:

  1. रीसेट करें और शुरू करें:

    adb shell am force-stop com.android.codebloat
    
  2. इंटरैक्ट करें: ऐप्लिकेशन शुरू करें और उसे स्टार्टअप सीक्वेंस चलाने दें.

  3. डंप प्रोफ़ाइल:

    adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)
    

    (इससे ऐप्लिकेशन को अपनी मौजूदा प्रोफ़ाइल को डिस्क पर सेव करने के लिए मजबूर किया जाता है).

  4. ऐप्लिकेशन इंस्टॉल करने की प्रोफ़ाइल:

    adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \
    /data/misc/profiles/ref/com.android.codebloat/primary.prof
    
  5. कंपाइल करें:

    adb shell cmd package compile -m speed-profile -f com.android.codebloat
    

फिर से लॉन्च करने पर, आपको बैलेंस दिखेगा: कोड PSS, speed (जैसे, ~16,000 केबी) से कम होगा. ऐसा इसलिए, क्योंकि सिर्फ़ "हॉट" स्टार्टअप के तरीकों को कंपाइल किया गया था. बाकी को इंटरप्रेटर या JIT के ज़रिए हैंडल किया जाएगा. हालांकि, ऐसा सिर्फ़ तब होगा, जब उनका इस्तेमाल किया जाएगा.

देखें:

कंपाइल किए गए कोड के बारे में ज़्यादा जानकारी

अगर आपको यह देखना है कि एआरटी कौनसे निर्देश जनरेट कर रहा है, तो art/DISASSEMBLY_GUIDE.md पर जाएं.

इसमें इनके इस्तेमाल के बारे में पूरी जानकारी दी गई है:

  • oatdump: किसी मौजूदा .odex फ़ाइल में ARM64 निर्देश देखने के लिए.
  • dex2oat: वर्बोस डीबग फ़्लैग के साथ कंपाइलेशन को सिम्युलेट करने के लिए.

एक्सरसाइज़: कोड इनलाइन करना

कंपाइल किए गए कोड का साइज़ अचानक बढ़ने की एक वजह, मेथड इनलाइनिंग है. कंपाइलर, अक्सर कॉल किए जाने वाले छोटे तरीके के मुख्य हिस्से को सीधे तौर पर कॉल करने वालों में कॉपी करने का फ़ैसला कर सकता है.

हमारे CodeBloat ऐप्लिकेशन में, जनरेट की गई हर क्लास में मौजूद doSomething() मैथड, सिर्फ़ method0() को कॉल करता है. speed मोड में कंपाइल करने पर, ART का ऑप्टिमाइज़िंग कंपाइलर, method0() को doSomething() में इनलाइन कर देगा.

एक्सरसाइज़: अपने डिवाइस पर oatdump का इस्तेमाल करके इसकी पुष्टि करें:

# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk

adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]

# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
                  --class-filter=com.android.codebloat.GeneratedClass0

आउटपुट में doSomething तरीका ढूंढें. अगर इसे इनलाइन किया गया था, तो आपको method0 को टारगेट करने वाले bl निर्देश के बजाय, doSomething में सीधे तौर पर लंबी स्ट्रिंग वाले कॉन्स्टेंट को लोड करने के निर्देश दिखेंगे.

ऑप्टिमाइज़ेशन (सीएफ़जी) को विज़ुअलाइज़ करना

कंपाइलर ने मेथड को कब इनलाइन करने का फ़ैसला किया, यह देखने के लिए कंट्रोल फ़्लो ग्राफ़ (सीएफ़जी) जनरेट किया जा सकता है. इससे ऑप्टिमाइज़ेशन पाइपलाइन के हर चरण में कोड की स्थिति दिखती है. साथ ही, कंपाइलर के इंटरमीडिएट रिप्रेजेंटेशन (आईआर) पर हर ट्रांसफ़ॉर्मेशन तब तक दिखता है, जब तक कोड को टारगेट आईएसए (जैसे, ARM64) में नहीं बदल दिया जाता.

  1. डंप फ़्लैग के साथ dex2oat चलाएं: --verbose-methods फ़्लैग का इस्तेमाल करके, आउटपुट को कुछ खास तरीकों तक सीमित करें. ऐसा न करने पर, बड़े ऐप्लिकेशन के लिए .cfg फ़ाइल का साइज़ कई गीगाबाइट तक बढ़ सकता है.

    # Substitution of actual paths required:
    adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \
                        --oat-file=/data/local/tmp/dump.odex \
                        --compiler-filter=speed \
                        --dump-cfg=/data/local/tmp/codebloat.cfg \
                        --verbose-methods=doSomething
    
  2. पुल और देखें: .cfg फ़ाइल को अपने वर्कस्टेशन पर पुल करें और इसे IR Hydra से खोलें.

  3. इनलाइनर ढूंढें: IR Hydra में, कंपाइलेशन आर्टफ़ैक्ट लोड करें और doSomething खोजें. Inliner पास से पहले और बाद में, कोड के प्रज़ेंटेशन की तुलना करें. method0 से मिले निर्देशों को कॉलर में मर्ज करने पर, आपको ग्राफ़ बढ़ता हुआ दिखेगा.

इसके अलावा, ऊपर दिए गए सेक्शन में बताए गए तरीके से, Compiler Explorer में Opt Pipeline टूल का इस्तेमाल करें. साथ ही, Inliner पास में किए गए बदलाव को देखने के लिए, मिलता-जुलता कोड डालें.

एक्सरसाइज़: वोलेटाइल फ़ील्ड और मेमोरी बैरियर

MemoryLab ऐप्लिकेशन में, mGarbageSink फ़ील्ड को volatile के तौर पर मार्क किया गया है. इससे यह पक्का होता है कि कंपाइलर, हमारे गार्बेज कलेक्शन को ऑप्टिमाइज़ न करे.

public volatile byte[] mGarbageSink;

ARM64 डिसअसेंबली में, आपको दिखेगा कि इस फ़ील्ड में हर स्टोर के साथ मेमोरी बैरियर (dmb ish) होता है या Load-Acquire/Store-Release निर्देशों (ldar/stlr) का इस्तेमाल किया जाता है. इससे यह पक्का होता है कि थ्रेड दिखे, लेकिन हर ऐक्सेस में कुछ अतिरिक्त निर्देश जुड़ जाते हैं. इससे कोड का साइज़, सामान्य फ़ील्ड की तुलना में थोड़ा बढ़ जाता है.

एक्सरसाइज़: डिसअसेंबली में, फ़ील्ड ऐक्सेस और उनसे जुड़ी मेमोरी बैरियर ढूंढें.

एक्सरसाइज़: निलंबित करने से जुड़ी जांचें

अगर आपको generateAllocationChurn में दिए गए लूप की तरह किसी लूप को अलग-अलग हिस्सों में बांटना है, तो आपको लूप के आखिर में यह निर्देश दिखेगा:

ldr x21, [x21]

यह निलंबन की पुष्टि करने की प्रोसेस है. ART इसका इस्तेमाल, Garbage Collector को थ्रेड को सुरक्षित तरीके से रोकने की अनुमति देने के लिए करता है. रजिस्टर x21 आम तौर पर खुद को पॉइंट करता है. जब GC को थ्रेड को निलंबित करने की ज़रूरत होती है, तो वह मेमोरी लोकेशन को "पॉइज़न" करता है. जब थ्रेड अगली बार उस ldr को एक्ज़ीक्यूट करेगा, तब एक गड़बड़ी ट्रिगर होगी. रनटाइम इस गड़बड़ी को पकड़ लेगा और इसका इस्तेमाल करके थ्रेड को निलंबित स्थिति में बदल देगा.

यह पैटर्न हर लूप में और हर तरीके की शुरुआत में दोहराया जाता है. इससे आपके ऐप्लिकेशन के कुल कोड का साइज़ बढ़ जाता है.

एक्सरसाइज़: डिसअसेंबली के तरीके में, निलंबित करने की सभी जांचों का पता लगाएं. साथ ही, उन्हें ओरिजनल सोर्स कोड से जोड़ने की कोशिश करें.


← WebView | ↑ ऊपर जाएं | Threads →