ओएस को मेमोरी पेज वापस करना

अपने ऐप्लिकेशन के कोड में किसी ऑब्जेक्ट को खाली करने या कुछ समय के लिए स्ट्रीम को बंद करने से, ऑपरेटिंग सिस्टम को हमेशा फ़िज़िकल रैम वापस नहीं मिलती. कस्टम नेटिव ऐलोकेटर, गार्बेज-कलेक्टेड रनटाइम, और ओपन-ऐड्रेस हैश टेबल अक्सर वर्चुअल मेमोरी अरेना को बनाए रखते हैं. ये अरेना, लॉजिकल डेटा के खत्म होने के बाद भी लंबे समय तक फ़िज़िकल पेजों पर बने रहते हैं. इसी तरह, बड़ी ऐसेट आर्काइव को पढ़ने या मेमोरी-मैप करने से, कर्नल पेज कैश भर जाता है. यह आपके ऐप्लिकेशन के मेमोरी कंट्रोल ग्रुप (memcg) से चार्ज होता रहता है.

इस पेज पर, madvise और posix_fadvise सिस्टम कॉल का इस्तेमाल करने का तरीका बताया गया है. इससे, इस्तेमाल नहीं किए गए ऐसे पेजों को ओएस को वापस किया जा सकता है जिनकी पहचान छिपाई गई है और जो फ़ाइल पर आधारित हैं. साथ ही, वर्चुअल पते के रिज़र्वेशन और फ़ाइल डिस्क्रिप्टर को पहले जैसा ही रखा जा सकता है. इसके अलावा, adb shell और Perfetto में इसके असर की पुष्टि करने का तरीका भी बताया गया है.

लॉजिकल डीएलोकेशन के बाद भी फ़िज़िकल रैम क्यों बची रहती है

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

जब किसी पेज को रैम में फ़ॉल्ट किया जाता है, तो कर्नेल को यह नहीं पता होता कि उस पेज में मौजूद बाइट अब भी आपके ऐप्लिकेशन के रनटाइम में लाइव ऑब्जेक्ट को दिखाती हैं या नहीं. ऐप्लिकेशन का इस्तेमाल खत्म होने के बाद, फ़िज़िकल पेज को मेमोरी में बनाए रखने के लिए, दो सामान्य पैटर्न इस्तेमाल किए जाते हैं:

  • बिना सोर्स फ़ाइल वाली मेमोरी के अरीना और पूल: कस्टम नेटिव ऐलोकेटर (जैसे, स्लैब, पूल या फ़्रेम-अरीना ऐलोकेटर), नॉन-मूविंग हीप स्पेस के साथ मैनेज किए जाने वाले रनटाइम, और बड़ी हैश टेबल, mmap का इस्तेमाल करके MAP_PRIVATE | MAP_ANONYMOUS के साथ वर्चुअल पते की रेंज को रिज़र्व करती हैं. जब आपका ऐप्लिकेशन किसी अरीना को रीसेट करता है, किसी ब्लॉक को कस्टम पूल में वापस भेजता है या मेटाडेटा को शून्य करके हैश टेबल को मिटाता है, तो मौजूदा पेज डर्टी ही रहते हैं. कर्नल को साफ़ तौर पर हिंट दिए बिना, वे पेज आपकी प्रोसेस के बिना सोर्स फ़ाइल वाले रेज़िडेंट सेट साइज़ (आरएसएस) और प्रोपोर्शनल सेट साइज़ (पीएसएस) और उसके memory.current सीजीग्रुप चार्ज में गिने जाते रहेंगे. मेमोरी कम होने पर, कर्नल उन्हें ZRAM में कंप्रेस कर देता है. यहां वे अब भी स्वैप स्पेस का इस्तेमाल करते हैं, जिसका शुल्क memory.swap.current से लिया जाता है.
  • ऐसेट स्ट्रीमिंग से फ़ाइल-बैक्ड पेज कैश मेमोरी: जब आपका ऐप्लिकेशन read, pread या फ़ाइल-बैक्ड mmap का इस्तेमाल करके, बड़े ऐसेट बंडल, लेवल संग्रह, मीडिया कंटेनर या मशीन लर्निंग मॉडल को पढ़ता है, तो कर्नेल, पेज कैश मेमोरी में पढ़े गए हर पेज को कैश मेमोरी में सेव करता है. साथ ही, उन पेजों को आपकी प्रोसेस के cgroup की memory.stat फ़ाइल में file काउंटर पर चार्ज करता है. अगर आपका ऐप्लिकेशन उस ऐसेट को डीकंप्रेस करता है या किसी अज्ञात रनटाइम बफ़र में कॉपी करता है या उसे जीपीयू पर अपलोड करता है, तो पेज कैश मेमोरी में रॉ फ़ाइल पेजों को रखने से, रैम में ऐसेट का डुप्लीकेट बन जाता है.
madvise के साथ रिलीज़ किए गए डर्टी अरीना पेज और posix_fadvise के साथ हटाए गए कैश मेमोरी में सेव किए गए ऐसेट पेज. हालांकि, वर्चुअल रेंज और फ़ाइल डिस्क्रिप्टर में कोई बदलाव नहीं होता.
पहली इमेज. वर्चुअल अरीना और पेज कैश मेमोरी से फ़िज़िकल पेज वापस पाना.

पहचान छिपाकर पेज रिलीज़ करना

madvise सिस्टम कॉल, कर्नेल को सलाह देता है कि आपकी प्रोसेस, वर्चुअल पते की रेंज का इस्तेमाल कैसे करेगी. निजी और बिना सोर्स फ़ाइल वाली मैपिंग (MAP_PRIVATE | MAP_ANONYMOUS) के लिए, MADV_DONTNEED को पास करने से कर्नेल को निर्देश मिलता है कि वह फ़िज़िकल पेजों और स्वैप की गई ZRAM एंट्री को तुरंत खारिज कर दे. ये एंट्री, पेज के हिसाब से अलाइन की गई रेंज के लिए बैकअप के तौर पर काम करती हैं.

madvise के बाद, MADV_DONTNEED के साथ आइटम लौटाने पर:

  • वर्चुअल पते की रेंज, आपकी प्रोसेस के पते की जगह में रिज़र्व और मान्य रहती है (VmSize में कोई बदलाव नहीं होता). इसलिए, पॉइंटर और अरीना की सीमाएं, munmap या mmap को कॉल किए बिना ही बरकरार रहती हैं.
  • फ़िज़िकल पेज तुरंत अनमैप और खाली हो जाते हैं. इससे RssAnon, Pss, memory.stat में anon काउंटर, और ZRAM में मौजूद किसी भी पेज के लिए memory.swap.current कम हो जाता है.
  • उस रेंज में मौजूद किसी पते को पढ़ने या उसमें कुछ लिखने पर, ज़रूरत के हिसाब से शून्य से भरा एक नया पेज जनरेट होता है.
सलाह देने वाला फ़्लैग टारगेट मैपिंग Android पर कर्नेल का काम करने का तरीका
MADV_DONTNEED निजी और पहचान छिपाने की सुविधा (MAP_PRIVATE | MAP_ANONYMOUS) यह फ़िज़िकल पेजों और ZRAM स्वैप स्लॉट को तुरंत खारिज कर देता है. इसके बाद, शून्य से भरे पेजों में ऐक्सेस की गड़बड़ी होती है. RssAnon और memory.current में डिटरमिनिस्टिक गिरावट.
MADV_DONTNEED फ़ाइल-बैक्ड (MAP_PRIVATE या MAP_SHARED) प्रोसेस पेज टेबल से पेजों को अनमैप करता है (RssFile ड्रॉप). MAP_PRIVATE मैपिंग में, कॉपी-ऑन-राइट वाले निजी पेजों को खारिज कर दिया जाता है. इसलिए, उनमें किए गए बदलाव सेव नहीं होते और अगली बार ऐक्सेस करने पर, फ़ाइल को फिर से पढ़ा जाता है. डर्टी MAP_SHARED पेजों को बाद में स्टोरेज में वापस लिखा जाता है. क्लीन पेज, पेज कैश में तब तक रहते हैं, जब तक उन्हें हटाया नहीं जाता.
MADV_FREE निजी और पहचान छिपाने की सुविधा (MAP_PRIVATE | MAP_ANONYMOUS) यह पेजों को लेज़ीली रीयूज़ेबल के तौर पर मार्क करता है. अगर मेमोरी पर कोई दबाव नहीं है, तो कर्नेल, पेजों को RssAnon में बनाए रखता है. साथ ही, उन्हें सिर्फ़ तब वापस लेता है, जब ग्लोबल या memcg को वापस लेने की प्रोसेस चलती है.
MADV_WILLNEED फ़ाइल-बैक्ड या स्वैप किया गया अज्ञात यह फ़ाइल-आधारित मैपिंग के लिए, एसिंक्रोनस रीडअहेड शुरू करता है या स्वैप किए गए बिना सोर्स फ़ाइल वाले पेजों को ZRAM से स्वैप कैश मेमोरी में समय से पहले वापस पढ़ता है.

इंटीरियर रेंज को रनटाइम पेज साइज़ के हिसाब से अलाइन करना

madvise, पूरे पेज पर काम करता है. इसके लिए, पेज के हिसाब से अलाइन किए गए शुरुआती पते की ज़रूरत होती है. साथ ही, यह लंबाई को अगले पेज की सीमा तक राउंड अप करता है. अगर किसी शेयर किए गए अरीना में स्पैन को रिलीज़ किया जाता है, जहां आस-पास की बाइट में अब भी लाइव ऑब्जेक्ट मौजूद हैं, तो रेंज को बाहर की ओर राउंड करने से, बाउंड्री पेज शेयर करने वाले लाइव ऐलोकेशन शून्य हो सकते हैं.

4 केबी और 16 केबी वाले पेज साइज़ वाले डिवाइसों के साथ काम करने के लिए, हमेशा sysconf(_SC_PAGESIZE) के साथ रनटाइम पेज साइज़ के बारे में क्वेरी करें. साथ ही, अंदरूनी स्टार्ट अड्रेस को अगले पेज की सीमा तक और अंदरूनी एंड अड्रेस को पिछले पेज की सीमा तक राउंड करें. पेज के साइज़ के हिसाब से काम न करने वाले ऐलोकेटर के बारे में ज़्यादा जानने के लिए, कस्टम मेमोरी का बंटवारा करने वाले ऐलोकेटर को ऑप्टिमाइज़ करना लेख पढ़ें.

#include <sys/mman.h>
#include <unistd.h>
#include <cstdint>

// Reclaims physical pages strictly inside [region_start, region_start + length)
// without disturbing live objects on partially occupied boundary pages.
void ReleaseUnusedArenaSpan(void* region_start, size_t length) {
  const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
  const uintptr_t page_mask = ~(page_size - 1);

  const uintptr_t raw_start = reinterpret_cast<uintptr_t>(region_start);
  const uintptr_t raw_end = raw_start + length;

  // Round start UP and end DOWN so only fully unused pages are discarded.
  const uintptr_t aligned_start = (raw_start + page_size - 1) & page_mask;
  const uintptr_t aligned_end = raw_end & page_mask;

  if (aligned_end > aligned_start) {
    madvise(reinterpret_cast<void*>(aligned_start),
            aligned_end - aligned_start,
            MADV_DONTNEED);
  }
}

mmap की मदद से कस्टम एनॉनिमस मैपिंग बनाते समय, PR_SET_VMA_ANON_NAME का इस्तेमाल करके वीएमए को prctl के साथ लेबल करें, ताकि showmap और /proc/<pid>/smaps में नाम के हिसाब से इसकी रेज़िडेंसी को ट्रैक किया जा सके. dumpsys meminfo Unknown में मौजूद, कस्टम नाम वाले उन क्षेत्रों की गिनती करता है जिनकी पहचान छिपाई गई है. कुछ पुराने कर्नेल, नाम को कॉपी करने के बजाय उसका पॉइंटर सेव रखते हैं. इसलिए, ऐसी स्ट्रिंग पास करें जो मैपिंग के लाइफ़टाइम तक मान्य रहे. जैसे, स्ट्रिंग लिटरल:

#include <sys/mman.h>
#include <sys/prctl.h>

#ifndef PR_SET_VMA
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0
#endif

void* ReserveNamedArena(size_t capacity_bytes, const char* arena_name) {
  void* ptr = mmap(nullptr, capacity_bytes, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (ptr != MAP_FAILED) {
    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME,
          reinterpret_cast<unsigned long>(ptr), capacity_bytes,
          reinterpret_cast<unsigned long>(arena_name));
  }
  return ptr;
}

फ़ाइल कैश मेमोरी के पेज हटाएं

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

posix_fadvise सिस्टम कॉल की मदद से, आपका ऐप्लिकेशन कर्नेल को यह जानकारी दे सकता है कि वह किसी खुली हुई फ़ाइल डिस्क्रिप्टर पर, फ़ाइल को किस तरह ऐक्सेस कर सकता है:

  • POSIX_FADV_DONTNEED: यह कर्नेल को निर्देश देता है कि बाइट की तय की गई रेंज को आने वाले समय में ऐक्सेस नहीं किया जाएगा. कर्नेल, पेज कैश मेमोरी से पेज-अलाइन की गई रेंज में मौजूद, साफ़ और मैप नहीं किए गए कैश मेमोरी वाले पेजों को तुरंत हटा देता है. इससे memory.stat में file काउंटर और आपके cgroup का memory.current चार्ज कम हो जाता है.
  • POSIX_FADV_SEQUENTIAL: यह फ़ाइल डिस्क्रिप्टर के लिए, कर्नल की रीडअहेड विंडो को दोगुना कर देता है. इससे, शुरुआत से आखिर तक बड़ी फ़ाइल को स्ट्रीम करते समय, थ्रूपुट बेहतर होता है. इसे ट्रेलिंग चंक पर POSIX_FADV_DONTNEED के साथ जोड़ें, क्योंकि आप उनका इस्तेमाल करते हैं, ताकि पेज कैश मेमोरी फ़ुटप्रिंट एक छोटी स्लाइडिंग विंडो तक सीमित रहे.
  • POSIX_FADV_RANDOM: फ़ाइल डिस्क्रिप्टर पर, अनुमानित कर्नेल रीडअहेड को बंद करता है. इसका इस्तेमाल तब करें, जब किसी बड़ी संग्रह या डेटाबेस फ़ाइल में बाइट ऑफ़सेट के हिसाब से, स्पार्स और नॉन-कॉन्टिग्यूअस लुकअप किए जा रहे हों. इससे हर छोटे रीड के लिए, आस-पास के इस्तेमाल न किए गए पेजों को पेज कैश में नहीं खींचा जाता. मेमोरी-मैप की गई फ़ाइलों के लिए, मैपिंग पर MADV_RANDOM के साथ madvise को कॉल करें.

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

#include <fcntl.h>
#include <unistd.h>
#include <cstdint>
#include <vector>

void StreamAssetArchive(int fd, size_t total_size) {
  const size_t page_size = static_cast<size_t>(sysconf(_SC_PAGESIZE));
  constexpr size_t kChunkSize = 4 * 1024 * 1024;  // 4 MB streaming window
  std::vector<uint8_t> buffer(kChunkSize);

  // Hint sequential access so the kernel prefetches upcoming chunks.
  posix_fadvise(fd, 0, total_size, POSIX_FADV_SEQUENTIAL);

  off_t offset = 0;
  while (offset < static_cast<off_t>(total_size)) {
    ssize_t bytes_read = pread(fd, buffer.data(), buffer.size(), offset);
    if (bytes_read <= 0) break;

    // Decode or upload buffer[0 .. bytes_read) into runtime structures here.

    // Evict full pages that have been completely consumed.
    const size_t aligned_len =
        (static_cast<size_t>(bytes_read) / page_size) * page_size;
    if (aligned_len > 0) {
      posix_fadvise(fd, offset, static_cast<off_t>(aligned_len),
                    POSIX_FADV_DONTNEED);
    }
    offset += bytes_read;
  }
}

फ़ाइल-बैक्ड एममैप रीजन को फिर से हासिल करना

अगर आपका ऐप्लिकेशन pread से पढ़ने के बजाय, mmap के साथ किसी बड़ी ऐसेट फ़ाइल को मेमोरी-मैप करता है, तो सिर्फ़ POSIX_FADV_DONTNEED के साथ posix_fadvise को कॉल करने से, वे पेज नहीं हटते जो अब भी आपकी प्रोसेस के पेज टेबल में मैप किए गए हैं. पूरे वीएमए को अनमैप किए बिना, रीड-ओनली फ़ाइल मैपिंग के इस्तेमाल किए गए स्पैन को हटाने के लिए:

  1. मैप की गई वर्चुअल पता रेंज पर madvise(addr, length, MADV_DONTNEED) को कॉल करें, ताकि उन पेजों को आपकी प्रोसेस की पेज टेबल (RssFile ड्रॉप) से अनमैप किया जा सके.
  2. संबंधित फ़ाइल ऑफ़सेट पर posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED) को कॉल करें, ताकि कर्नेल, पेज कैश मेमोरी से अनमैप किए गए क्लीन पेजों को भी हटा दे (memory.stat ड्रॉप में file).

पेज पर सलाह देने से जुड़े दिशा-निर्देशों का पालन करना

एलोकेटर, ऐसेट लोडर, और लाइफ़साइकल हैंडलर में madvise और posix_fadvise को इंटिग्रेट करते समय, ये तरीके अपनाएं:

  • बड़े क्षेत्रों में बैच कॉल: हर madvise कॉल, आपकी प्रोसेस का मेमोरी मैप लॉक (mmap_lock) लेता है, पेज टेबल को स्कैन करता है, और अन्य सीपीयू कोर पर मैच होने वाली ट्रांसलेशन लुकअसाइड बफ़र (टीएलबी) एंट्री को अमान्य कर देता है. इसके लिए, Arm पर ब्रॉडकास्ट टीएलबी मेंटेनेंस के निर्देशों और x86 पर इंटर-प्रोसेसर इंटरप्ट का इस्तेमाल किया जाता है. हर छोटे डीएलोकेशन पर madvise को कॉल न करें. खाली स्पैन को एक साथ जोड़ें और कर्नेल को 64 केबी से 1 एमबी या उससे बड़े हिस्सों में सलाह दें. इसके अलावा, फ़्रेम की सीमाओं और बैकग्राउंड ट्रांज़िशन (onStop और TRIM_MEMORY_UI_HIDDEN) के दौरान खाली स्पैन को स्वीप करें.
  • हैश टेबल और पूल में खाली जगह को फिर से इस्तेमाल करना: जब किसी सीन ट्रांज़िशन या डेटा सिंक के बाद, बड़ी हैश टेबल, ऑब्जेक्ट पूल या स्टेजिंग बफ़र को खाली किया जाता है, तो उसके बैकिंग ऐलोकेशन को छोटा करें या खाली किए गए बैकिंग कलेक्शन में MADV_DONTNEED जारी करें. टॉम्बस्टोन के साथ असाइन किए गए 32 एमबी के ओपन-एड्रेस टेबल को बनाए रखने से, 32 एमबी के सभी फ़िज़िकल पेज आपकी प्रोसेस के लिए चार्ज किए जाते हैं.
  • फ़ाइल के डेटा को हटाने से पहले, उसमें मौजूद ग़ैर-ज़रूरी डेटा को हटाना: POSIX_FADV_DONTNEED सिर्फ़ पेज कैश मेमोरी के सही पेजों को हटाता है. अगर आपका ऐप्लिकेशन, बड़ी अस्थायी या कैश मेमोरी वाली फ़ाइल बनाता है, जिसे वह तुरंत नहीं पढ़ेगा, तो posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) से पहले fdatasync को कॉल करें, ताकि डर्टी पेज स्टोरेज में सेव हो जाएं और फिर रैम से हटा दिए जाएं.
  • शेयर किए गए हॉट कोड या ऐसेट को न हटाएं: DEX, ODEX, VDEX, शेयर की गई लाइब्रेरी (.so) या बार-बार ऐक्सेस की जाने वाली यूज़र इंटरफ़ेस (यूआई) ऐसेट पर POSIX_FADV_DONTNEED को कॉल करने से बचें. ऐक्टिव कोड या ऐसेट पेजों को हटाने से, सिंक्रोनस स्टोरेज में गड़बड़ियां फिर से होने लगती हैं. इससे थ्रेड रुक जाते हैं और I/O पर दबाव बढ़ जाता है. इसकी जानकारी, प्रेशर स्टॉल इन्फ़ॉर्मेशन (पीएसआई) से मिलती है.

शेल में पेज रिक्लेमेशन की पुष्टि करना

पुष्टि करें कि madvise और posix_fadvise, showmap और आपके ऐप्लिकेशन की cgroup v2 अकाउंटिंग फ़ाइलों का इस्तेमाल करके, फ़िज़िकल रैम को फिर से हासिल करते हैं.

showmap की मदद से वर्चुअल और फ़िज़िकल साइज़ की जांच करना

PR_SET_VMA_ANON_NAME का इस्तेमाल करके, किसी अज्ञात अरीना को नाम देने पर, showmap उसके वर्चुअल रिज़र्वेशन (virtual size) और उसके रेज़िडेंट फ़िज़िकल पेज (RSS और PSS), दोनों को दिखाता है. यहां दिया गया सैंपल आउटपुट, एओएसपी में मौजूद MemoryLab के सैंपल ऐप्लिकेशन से लिया गया है. यह ऐप्लिकेशन, अपने अरीना को memorylab-arena नाम देता है. com.example.app और memorylab-arena को अपने पैकेज के नाम और अरीना के नाम से बदलें. madvise को MADV_DONTNEED के साथ कॉल करने से पहले, 64 एमबी के अरेना पर यह चलाएं:

adb shell "showmap \$(pidof com.example.app) | grep -E '^ +virtual|^ +size|memorylab-arena' | head -3"
 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536    65536    65536        0        0        0    65536        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

madvise(arena_ptr, 64 * 1024 * 1024, MADV_DONTNEED) को कॉल करने के बाद, showmap को फिर से चलाएं:

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536        0        0        0        0        0        0        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

virtual size 65536 केबी (64 एमबी) पर बना रहता है. इससे यह पुष्टि होती है कि वर्चुअल पते की रेंज अब भी रिज़र्व है. वहीं, RSS, PSS, और private dirty घटकर 0 केबी हो जाते हैं.

cgroup memory.current और memory.stat की जांच करना

एनॉनिमस पेज रिक्लेमेशन और अनमैप्ड पेज कैश मेमोरी को हटाने की प्रोसेस की पुष्टि करने के लिए, प्रोसेस की cgroup v2 डायरेक्ट्री की जांच करें. अनमैप्ड पेज कैश मेमोरी को हटाने की प्रोसेस, pread से पढ़ने पर प्रोसेस पेज टेबल में नहीं दिखती. Android पर, हर ऐप्लिकेशन प्रोसेस का अपना cgroup होता है. जैसे, डिवाइस के हिसाब से /sys/fs/cgroup/apps/uid_<UID>/pid_<PID>/. यहां दिया गया कमांड, /proc/<PID>/cgroup की 0:: लाइन से डायरेक्ट्री ढूंढता है और काउंटर प्रिंट करता है:

adb shell '
  PID=$(pidof com.example.app)
  CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
  echo "memory.current: $(cat $CG/memory.current)"
  echo "memory.swap.current: $(cat $CG/memory.swap.current)"
  grep -E "^(anon|file) " $CG/memory.stat
'

पेज कैश मेमोरी से 64 एमबी के अज्ञात अरीना को रिलीज़ करने और 128 एमबी की मेमोरी-मैप की गई ऐसेट फ़ाइल को हटाने से पहले:

memory.current: 242962432
memory.swap.current: 16633856
anon 100671488
file 140009472

अरीना और ऐसेट फ़ाइल की मैपिंग पर MADV_DONTNEED के साथ madvise को कॉल करने के तुरंत बाद, ऐसेट फ़ाइल डिस्क्रिप्टर पर POSIX_FADV_DONTNEED के साथ posix_fadvise को कॉल करें:

memory.current: 41766912
memory.swap.current: 16502784
anon 33722368
file 5914624

anon में 64 एमबी और file में 128 एमबी की कमी आती है. इससे फ़ाइल डिस्क्रिप्टर को बंद किए बिना या अरीना के वर्चुअल पते की जगह को अनमैप किए बिना, memory.current में करीब 192 एमबी की कमी आती है. memory.swap.current में शायद ही कोई बदलाव हुआ हो, क्योंकि अरेना के किसी भी पेज को ZRAM में स्वैप नहीं किया गया था. आपकी वैल्यू, डिवाइस और इस बात पर निर्भर करती हैं कि कर्नेल ने फ़ाइल का कितना हिस्सा पहले ही हटा दिया है. कर्नल हर cgroup के लिए मेमोरी को कैसे चार्ज और रिक्लेम करता है, इस बारे में ज़्यादा जानने के लिए, मेमोरी रिक्लेम करना: इविक्शन और स्वैप देखें.

Perfetto में पेज रिक्लेमेशन का विश्लेषण करना

Perfetto की मदद से, madvise और posix_fadvise कॉल को एक ही टाइमलाइन पर, कर्नल पेज-टेबल और पेज-कैश काउंटर के साथ कोरिलेट किया जा सकता है.

ट्रेस रिकॉर्ड करना

समय-समय पर प्रोसेस काउंटर के लिए linux.process_stats और रीयल-टाइम आरएसएस और पेज-कैश इवेंट के लिए linux.ftrace के साथ ट्रेस कैप्चर करें. इसके बाद, इसे अपने कंप्यूटर पर खींचें और छोड़ें:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/page_advice.perfetto-trace <<EOF
buffers: {
    size_kb: 65536
    fill_policy: RING_BUFFER
}
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config {
            scan_all_processes_on_start: true
            proc_stats_poll_ms: 100
        }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "kmem/rss_stat"
            ftrace_events: "filemap/mm_filemap_add_to_page_cache"
            ftrace_events: "filemap/mm_filemap_delete_from_page_cache"
        }
    }
}
duration_ms: 20000
EOF
adb pull /data/misc/perfetto-traces/page_advice.perfetto-trace

Perfetto यूज़र इंटरफ़ेस (यूआई) में ट्रैक की जांच करना

Perfetto यूज़र इंटरफ़ेस (यूआई) में ट्रेस खोलें. इसके बाद, अपने ऐप्लिकेशन के प्रोसेस ग्रुप को बड़ा करें और इन काउंटर ट्रैक को पिन करें, ताकि वे टाइमलाइन में सबसे ऊपर रहें:

  • mem.virt: MADV_DONTNEED कॉल के दौरान इसमें कोई बदलाव नहीं होता. इससे यह पुष्टि होती है कि वर्चुअल पता रिज़र्वेशन (mmap) में कोई बदलाव नहीं हुआ है.
  • mem.rss.anon: यह उस टाइमस्टैंप पर तेज़ी से कम होता है जहां आपका ऐप्लिकेशन, डर्टी एनॉनिमस अरीना या हीप रीजन पर MADV_DONTNEED के साथ madvise को कॉल करता है.
  • mem.rss.file: यह तब कम होता है, जब आपका ऐप्लिकेशन madvise को MADV_DONTNEED के साथ कॉल करता है. ऐसा फ़ाइल-बैक्ड mmap रीजन पर होता है. साथ ही, mm_filemap_delete_from_page_cache ftrace इवेंट की संख्या अचानक बढ़ जाती है. ऐसा तब होता है, जब posix_fadvise, POSIX_FADV_DONTNEED के साथ पेज कैश मेमोरी से क्लीन पेजों को हटा देता है.
  • mem.swap: यह तब कम होता है, जब MADV_DONTNEED उन अज्ञात पेजों को खारिज कर देता है जिन्हें पहले ZRAM में कंप्रेस किया गया था.
Perfetto काउंटर, एक प्रोसेस के लिए ट्रैक करता है. mem.rss.anon और
       mem.rss.file, एरीना और ऐसेट फ़ाइल के पॉप्युलेट होने पर बढ़ जाते हैं और
       madvise और posix_fadvise के बाद कम हो जाते हैं. वहीं, mem.swap और
       mem.virt में कोई बदलाव नहीं होता.
दूसरी इमेज. पेज से जुड़ी सलाह वाले कॉल के बाद, आरएसएस फ़ीड की संख्या में गिरावट.

दूसरी इमेज में, MemoryLab के सैंपल के तौर पर मिले ऐप्लिकेशन के लिए ये ट्रैक दिखाए गए हैं. इस व्यू को एक्सप्लोर करने के लिए, Perfetto यूज़र इंटरफ़ेस (यूआई) में सैंपल ट्रेस खोलें.

PerfettoSQL की मदद से, पीक और फ़ाइनल मेमोरी के बारे में क्वेरी करना

Perfetto यूज़र इंटरफ़ेस (क्वेरी (एसक्यूएल) टैब) में, यह PerfettoSQL क्वेरी चलाएं. इससे, टारगेट प्रोसेस के लिए mem.virt, mem.rss.anon, mem.rss.file, और mem.swap की पीक और फ़ाइनल वैल्यू की तुलना की जा सकेगी:

WITH counters AS (
  SELECT t.name AS counter_name, c.ts, c.value
  FROM counter c
  JOIN process_counter_track t ON c.track_id = t.id
  JOIN process p USING (upid)
  WHERE p.name = 'com.example.app'
    AND t.name IN ('mem.virt', 'mem.rss.anon', 'mem.rss.file', 'mem.swap')
)
SELECT
  counter_name,
  CAST(MAX(value) / 1048576 AS INT) AS peak_mb,
  CAST((
    SELECT value FROM counters AS last
    WHERE last.counter_name = counters.counter_name
    ORDER BY ts DESC LIMIT 1
  ) / 1048576 AS INT) AS final_mb
FROM counters
GROUP BY counter_name
ORDER BY counter_name;

इमेज 2 में ट्रेस के लिए सैंपल आउटपुट:

counter_name  peak_mb  final_mb
mem.rss.anon      111        48
mem.rss.file      212        84
mem.swap           36        36
mem.virt        13952     13952

mem.rss.anon में 63 एमबी और mem.rss.file में 128 एमबी की गिरावट आती है. वहीं, mem.virt अपने पीक पर खत्म होता है, क्योंकि वर्चुअल रिज़र्वेशन में कोई बदलाव नहीं होता.

फ़ाइलों से हटाए गए पेजों की समस्या हल करना

mm_filemap_add_to_page_cache और mm_filemap_delete_from_page_cache इवेंट, फ़ाइलों की पहचान पाथ के बजाय आईनोड नंबर (i_ino) से करते हैं. यह पुष्टि करने के लिए कि हटाए गए पेज आपकी सुझाई गई फ़ाइल से जुड़े हैं, हर आईनोड के हिसाब से हटाए गए पेजों की संख्या गिनें:

SELECT
  EXTRACT_ARG(arg_set_id, 'i_ino') AS inode,
  COUNT(*) AS evicted_pages
FROM ftrace_event
WHERE name = 'mm_filemap_delete_from_page_cache'
GROUP BY inode
ORDER BY evicted_pages DESC
LIMIT 5;

इमेज 2 में ट्रेस के लिए सैंपल आउटपुट:

inode     evicted_pages
9761              35781
76792192            767
126                 668
0                   612
11587712            514

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

INODE=<inode from the query>
adb shell "find /data -xdev -inum $INODE 2>/dev/null"

आईनोड 9761 के लिए आउटपुट का सैंपल:

/data/user/0/com.android.memorylab/files/asset_pack.bin
/data/data/com.android.memorylab/files/asset_pack.bin

दोनों पाथ एक ही फ़ाइल को रेफ़र करते हैं, क्योंकि /data/user/0 और /data/data, एक ही ऐप्लिकेशन डेटा डायरेक्ट्री के दो नाम हैं. ऐसेट फ़ाइल (35,781 पेज) के लिए, निकाले गए पेज की संख्या, फ़ाइल के साइज़ (4 केबी पर 32,768 पेज) से ज़्यादा है. ऐसा इसलिए है, क्योंकि ऐप्लिकेशन के फ़ाइल को फिर से पढ़ने और posix_fadvise को कॉल करने से पहले ही, kswapd ने मेमोरी के दबाव की वजह से फ़ाइल के कुछ हिस्से को निकाल दिया था.

एक साथ कई आईनोड की गड़बड़ियां ठीक करने वाली स्क्रिप्ट के लिए, आईनोड और फिर फ़ाइलों की गड़बड़ियां ठीक करना देखें.


← kswapd और lmkd इंटरैक्शन | ↑ ऊपर | बैकग्राउंड में चल रही प्रोसेस को कम करना →