OS-এ মেমরি পৃষ্ঠা ফিরিয়ে দিন

আপনার অ্যাপের কোডে কোনও অবজেক্ট ফ্রি করা বা সাময়িক স্ট্রিম বন্ধ করার অর্থ সবসময় অপারেটিং সিস্টেমে ফিজিক্যাল RAM ফিরিয়ে দেওয়া নয়। কাস্টম নেটিভ অ্যালোকেটর, গার্বেজ-কালেক্টেড রানটাইম এবং ওপেন-অ্যাড্রেসড হ্যাশ টেবিল প্রায়ই ভার্চুয়াল মেমরি এরিনা রিটেন করে যা লজিক্যাল ডেটা মুছে যাওয়ার অনেক পরেও ফিজিক্যাল পেজ দ্বারা ব্যাক-আপ করা থাকে। একইভাবে, বড় অ্যাসেট আর্কাইভ পড়া বা মেমরি-ম্যাপিং করার ফলে কার্নেল পৃষ্ঠা ক্যাশে পপুলেট হয়, যা আপনার অ্যাপের মেমরি কন্ট্রোল গ্রুপে (memcg) চার্জ করা থাকে।

ভার্চুয়াল অ্যাড্রেস রিজার্ভেশন ও ফাইল ডেসক্রিপটর অক্ষত রেখে, অব্যবহৃত বেনামী ও ফাইল-ব্যাক করা পৃষ্ঠা OS-এ ফিরিয়ে দিতে, কীভাবে madvise ও posix_fadvise সিস্টেম কল ব্যবহার করতে হয় এবং adb shell ও Perfetto-তে এর প্রভাব কীভাবে যাচাই করতে হয়, তা এই পৃষ্ঠায় ব্যাখ্যা করা হয়েছে।

কেন লজিক্যাল ডিলোক্যাশন ফিজিক্যাল RAM ধরে রাখে

Linux কার্নেল হার্ডওয়্যার পেজের গ্র্যানুলারিটিতে ফিজিক্যাল মেমরি ট্র্যাক করে, যা Android ডিভাইসে ৪ কেবি বা ১৬ কেবি হয়। যখন কোনও প্রসেস mmap-এর মাধ্যমে ভার্চুয়াল অ্যাড্রেস স্পেসের কোনও অঞ্চল রিজার্ভ করে, তখন কার্নেল ফিজিক্যাল RAM বরাদ্দ না করেই ভার্চুয়াল মেমরি এরিয়া (VMA) তৈরি করে। প্রসেসটি প্রথমবার সেই পৃষ্ঠাটি স্পর্শ করলেই কার্নেল একটি ফিজিক্যাল পৃষ্ঠা বরাদ্দ করে এবং চার্জ করে। বেনামী মেমরির জন্য, প্রথম রিড একটি শেয়ার করা জিরো পৃষ্ঠা ম্যাপ করে, তাই শুধুমাত্র একটি রাইট RAM বরাদ্দ করে।

RAM-এ কোনও পৃষ্ঠা ফল্ট হয়ে গেলে, কার্নেল বুঝতে পারে না যে সেই পৃষ্ঠার মধ্যে থাকা বাইটগুলি এখনও আপনার অ্যাপের রানটাইমে লাইভ অবজেক্টকে উপস্থাপন করছে কিনা। আপনার অ্যাপ ব্যবহার করা শেষ করার পরে দুটি সাধারণ প্যাটার্ন ফিজিক্যাল পৃষ্ঠা রেসিডেন্ট রাখে:

  • বেনামী এরিনা ও পুল: কাস্টম নেটিভ অ্যালোকেটর (যেমন স্ল্যাব, পুল বা ফ্রেম-এরিনা অ্যালোকেটর), নন-মুভিং হিপ স্পেস সহ ম্যানেজ করা রানটাইম এবং বড় হ্যাশ টেবিল mmap ব্যবহার করে MAP_PRIVATE | MAP_ANONYMOUS-এর সাথে সংলগ্ন ভার্চুয়াল অ্যাড্রেস রেঞ্জ রিজার্ভ করে। আপনার অ্যাপ কোনও এরিনা রিসেট করলে, কাস্টম পুলে ব্লক ফ্রি করলে অথবা মেটাডেটা জিরো করে হ্যাশ টেবিল মুছে দিলে, অন্তর্নিহিত পৃষ্ঠাগুলি ডার্টি থেকে যায়। কার্নেলকে স্পষ্ট ইঙ্গিত না দিলে, সেইসব পৃষ্ঠা আপনার প্রসেসের বেনামী রেসিডেন্ট সেট সাইজ (RSS) ও প্রোপোরশনাল সেট সাইজ (PSS) এবং তার memory.current cgroup চার্জের দিকে গণনা করা চালিয়ে যায়। মেমরি প্রেসারের অধীনে, কার্নেল সেগুলিকে ZRAM-এ কম্প্রেস করে, যেখানে সেগুলি এখনও memory.swap.current-এ চার্জ করা সোয়াপ স্পেস দখল করে।
  • অ্যাসেট স্ট্রিমিং থেকে ফাইল-ব্যাক করা পৃষ্ঠা ক্যাশে: আপনার অ্যাপ যখন read, pread বা ফাইল-ব্যাক করা mmap ব্যবহার করে বড় অ্যাসেট বান্ডেল, লেভেল আর্কাইভ, মিডিয়া কন্টেনার বা মেশিন লার্নিং মডেল পড়ে, তখন কার্নেল পৃষ্ঠা ক্যাশেতে প্রতিটি পড়া পৃষ্ঠা ক্যাশে করে এবং আপনার প্রসেসের cgroup-এর memory.stat ফাইলে file কাউন্টারে সেই পৃষ্ঠাগুলি চার্জ করে। আপনার অ্যাপ যদি সেই অ্যাসেটকে কোনও অ্যানোনিমাস রানটাইম বাফারে ডিকম্প্রেস বা কপি করে অথবা GPU-তে আপলোড করে, তাহলে পেজ ক্যাশে র ফাইল পেজ রাখলে RAM-এ অ্যাসেটের ডুপ্লিকেট তৈরি হয়।
ভার্চুয়াল রেঞ্জ ও ফাইল ডেসক্রিপটর অক্ষত রেখে, madvise-এর মাধ্যমে রিলিজ করা ডার্টি এরিনা পৃষ্ঠা এবং posix_fadvise-এর মাধ্যমে
       উচ্ছেদ করা ক্যাশে করা অ্যাসেট পৃষ্ঠা।
ছবি ১. ভার্চুয়াল এরিনা ও পৃষ্ঠা ক্যাশে থেকে ফিজিক্যাল পৃষ্ঠা পুনরুদ্ধার করা।

পরিচয় গোপন রেখে পৃষ্ঠা প্রকাশ করুন

madvise সিস্টেম কল কার্নেলকে পরামর্শ দেয় যে আপনার প্রসেস কীভাবে ভার্চুয়াল অ্যাড্রেস রেঞ্জ ব্যবহার করতে চায়। ব্যক্তিগত বেনামী ম্যাপিংয়ের (MAP_PRIVATE | MAP_ANONYMOUS) জন্য, MADV_DONTNEED পাস করলে কার্নেলকে নির্দেশ দেওয়া হয় যাতে নির্দিষ্ট পৃষ্ঠা-সারিবদ্ধ রেঞ্জের ব্যাক-আপ নেওয়া ফিজিক্যাল পৃষ্ঠা এবং সোয়্যাপ করা যেকোনও ZRAM এন্ট্রি অবিলম্বে বাতিল করা হয়।

MADV_DONTNEED রিটার্ন সহ madvise-এর পরে:

  • ভার্চুয়াল অ্যাড্রেস রেঞ্জ আপনার প্রসেসের অ্যাড্রেস স্পেসে (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 সম্পূর্ণ পৃষ্ঠায় কাজ করে। এটির জন্য পৃষ্ঠা-সারিবদ্ধ শুরুর ঠিকানা প্রয়োজন এবং পরবর্তী পৃষ্ঠার সীমানা পর্যন্ত দৈর্ঘ্য রাউন্ড আপ করে। আপনি যদি শেয়ার করা এরিনার মধ্যে কোনও স্প্যান রিলিজ করেন যেখানে আশেপাশের বাইটে এখনও লাইভ অবজেক্ট থাকে, তাহলে রেঞ্জটি আউটওয়ার্ডে রাউন্ড করলে বাউন্ডারি পৃষ্ঠা শেয়ার করা লাইভ অ্যালোকেশন জিরো আউট হয়ে যেতে পারে।

৪ কেবি ও ১৬ কেবি পৃষ্ঠার ডিভাইস, দুটিকেই সাপোর্ট করার জন্য সবসময় 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-এর মাধ্যমে কাস্টম অ্যানোনিমাস ম্যাপিং তৈরি করার সময়, prctl-এর সাথে VMA লেবেল করুন PR_SET_VMA_ANON_NAME ব্যবহার করে যাতে আপনি 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;
  }
}

ফাইল-ব্যাকড mmap অঞ্চল রিক্লেম করুন

আপনার অ্যাপ যদি pread-এর মাধ্যমে কোনও বড় অ্যাসেট ফাইল পড়ার পরিবর্তে mmap-এর মাধ্যমে সেটির মেমরি-ম্যাপ করে, তাহলে শুধু POSIX_FADV_DONTNEED-এর সাথে posix_fadvise কল করলে, আপনার প্রসেসের পৃষ্ঠা টেবিলে এখনও ম্যাপ করা আছে এমন পৃষ্ঠাগুলি এভিক্ট করা হয় না। সম্পূর্ণ VMA আনম্যাপ না করে শুধু-পড়ার-যোগ্য ফাইল ম্যাপিংয়ের ব্যবহৃত স্প্যান ড্রপ করতে:

  1. আপনার প্রসেসের পৃষ্ঠা সারণী (RssFile ড্রপ) থেকে সেই পৃষ্ঠাগুলি আনম্যাপ করতে ম্যাপ করা ভার্চুয়াল অ্যাড্রেস রেঞ্জে madvise(addr, length, MADV_DONTNEED) কল করুন।
  2. সংশ্লিষ্ট ফাইল অফসেটে posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED) কল করুন যাতে কার্নেলও পেজ ক্যাশে থেকে আনম্যাপ করা ক্লিন পেজগুলি বের করে দেয় (memory.stat ড্রপে file)।

পৃষ্ঠা সংক্রান্ত পরামর্শের নির্দেশিকা অনুসরণ করুন

অ্যালোকেটর, অ্যাসেট লোডার ও লাইফসাইকেল হ্যান্ডলারে madvise ও posix_fadvise ইন্টিগ্রেট করার সময় এইসব পদ্ধতি প্রয়োগ করুন:

  • কোর্স রিজিয়নে ব্যাচ কল: প্রতিটি madvise কল আপনার প্রসেসের মেমরি ম্যাপ লক (mmap_lock) নেয়, পৃষ্ঠা টেবিল ওয়াক করে এবং অন্যান্য CPU কোরে ম্যাচিং ট্রান্সলেশন লুকাসাইড বাফার (TLB) এন্ট্রি অবৈধ করে (Arm-এ ব্রডকাস্ট TLB রক্ষণাবেক্ষণ নির্দেশাবলী এবং x86-এ ইন্টার-প্রসেসর ইন্টারাপ্ট সহ)। প্রতিটি ছোট ডিলোকেশনের জন্য madvise কল করবেন না। ফ্রি স্প্যান একত্রিত করুন এবং কার্নেলকে ৬৪ কেবি থেকে ১ এমবি বা তার বেশি চাঙ্কে পরামর্শ দিন, অথবা ফ্রেমের সীমানা এবং ব্যাকগ্রাউন্ড ট্রানজিশনের (onStop এবং TRIM_MEMORY_UI_HIDDEN) সময় ফ্রি স্প্যান সুইপ করুন।
  • হ্যাশ টেবিল ও পুলে খালি ক্যাপাসিটি রিক্লেম করা: কোনও দৃশ্য ট্রানজিশন বা ডেটা সিঙ্ক করার পরে বড় হ্যাশ টেবিল, অবজেক্ট পুল বা স্টেজিং বাফার খালি করা হলে, হয় এর ব্যাকিং অ্যালোকেশন ছোট করুন অথবা খালি করা ব্যাকিং অ্যারে জুড়ে MADV_DONTNEED ইস্যু করুন। টুম্বস্টোনের সাথে বরাদ্দ করা একটি ৩২ এমবি ওপেন-অ্যাড্রেসড টেবিল রাখলে, আপনার প্রসেসে ফিজিক্যাল পেজের সম্পূর্ণ ৩২ এমবি চার্জ করা থাকে।
  • খারাপ ফাইল ডেটা বের করে দেওয়ার আগে তা ফ্লাশ করুন: POSIX_FADV_DONTNEED শুধুমাত্র ক্লিন পৃষ্ঠা ক্যাশে পৃষ্ঠা বের করে দেয়। আপনার অ্যাপ যদি একটি বড় অস্থায়ী বা ক্যাশে ফাইল লেখে যা এটি অবিলম্বে পড়বে না, তাহলে posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)-এর আগে fdatasync-এ কল করুন যাতে খারাপ পৃষ্ঠাগুলি স্টোরেজে লেখা হয় এবং তারপরে RAM থেকে বাদ দেওয়া হয়।
  • হট শেয়ার করা কোড বা অ্যাসেট এড়িয়ে যাবেন না: DEX, ODEX, VDEX, শেয়ার করা লাইব্রেরি (.so) বা বারবার অ্যাক্সেস করা হয় এমন UI অ্যাসেটে POSIX_FADV_DONTNEED কল করা এড়িয়ে চলুন। অ্যাক্টিভ কোড বা অ্যাসেট পেজ এভিক্ট করলে সিঙ্ক্রোনাস স্টোরেজ রিফল্ট হয় যা থ্রেড স্টল করে এবং I/O প্রেশার বাড়ায়, যেমন প্রেশার স্টল ইনফরমেশন (PSI) থেকে জানা গেছে।

শেল-এ পৃষ্ঠা রিক্লেমেশন যাচাই করুন

showmap এবং আপনার অ্যাপের cgroup v2 অ্যাকাউন্টিং ফাইল ব্যবহার করে madvise এবং posix_fadvise ফিজিক্যাল RAM রিক্লেম করে কিনা তা যাচাই করুন।

showmap-এর সাহায্যে ভার্চুয়াল বনাম ফিজিক্যাল সাইজ পরীক্ষা করা

আপনি PR_SET_VMA_ANON_NAME দিয়ে কোনও বেনামী এরেনার নাম দিলে, showmap সেটির ভার্চুয়াল রিজার্ভেশন (virtual size) এবং রেসিডেন্ট ফিজিক্যাল পেজ (RSS এবং PSS) দুটিই দেখায়। নিচের স্যাম্পেল আউটপুটটি AOSP-তে MemoryLab স্যাম্পেল অ্যাপ থেকে আসে, যা এরেনার নাম memorylab-arena দেয়। com.example.app এবং memorylab-arena-এর জায়গায় আপনার প্যাকেজের নাম এবং এরিনার নাম লিখুন। টাচ করা ৬৪ এমবি এরিনাতে MADV_DONTNEED সহ madvise কল করার আগে, রান করুন:

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 ৬৫৫৩৬ কেবি (৬৪ এমবি) থাকে, যা নিশ্চিত করে যে ভার্চুয়াল অ্যাড্রেস রেঞ্জ এখনও রিজার্ভ করা আছে, যেখানে RSS, PSS এবং private dirty ০ কেবিতে নেমে আসে।

cgroup memory.current এবং memory.stat পরীক্ষা করুন

বেনামী পৃষ্ঠা রিক্লেমেশন এবং আনম্যাপ করা পৃষ্ঠা ক্যাশে এভিকশন (যা pread-এর মাধ্যমে পড়লে প্রসেস পৃষ্ঠা টেবিলে দেখা যায় না) উভয়ই যাচাই করতে, প্রসেসের cgroup v2 ডিরেক্টরি পরীক্ষা করুন। 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
'

৬৪ এমবি বেনামী এরিনা রিলিজ করার আগে এবং পৃষ্ঠা ক্যাশে থেকে ১২৮ এমবি মেমরি-ম্যাপ করা অ্যাসেট ফাইল বের করে দেওয়ার আগে:

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-এর সাইজ ৬৪ এমবি এবং file-এর সাইজ ১২৮ এমবি কমে যায়, যার ফলে ফাইল ডেসক্রিপটর বন্ধ না করে বা এরিনার ভার্চুয়াল অ্যাড্রেস স্পেস আনম্যাপ না করেই memory.current-এর সাইজ প্রায় ১৯২ এমবি কমে যায়। memory.swap.current খুব কমই পরিবর্তন হয়, কারণ এরিনা পৃষ্ঠার প্রায় কোনওটিই ZRAM-এ সোয়্যাপ করা হয়নি। আপনার মান ডিভাইসের উপর নির্ভর করে এবং কার্নেল ইতিমধ্যেই ফাইলের কতটা অংশ নিজে থেকে বের করে দিয়েছে তার উপর নির্ভর করে। কার্নেল কীভাবে চার্জ করে এবং প্রতিটি cgroup-এর জন্য মেমরি রিক্লেম করে সেই সম্পর্কে আরও জানতে, মেমরি রিক্লেম: এভিকশন এবং সোয়াপ দেখুন।

Perfetto-তে পৃষ্ঠা রিক্লেমেশন বিশ্লেষণ করুন

Perfetto আপনাকে একটি ইউনিফায়েড টাইমলাইনে কার্নেল পেজ-টেবিল এবং পেজ-ক্যাশে কাউন্টারের সাথে madvise এবং posix_fadvise কলকে কোরিলেট করতে দেয়।

ট্রেস রেকর্ড করুন

পর্যায়ক্রমিক প্রসেস কাউন্টারের জন্য linux.process_stats এবং রিয়েল-টাইম RSS ও পৃষ্ঠা-ক্যাশে ইভেন্টের জন্য 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 UI-তে ট্র্যাকগুলি পরীক্ষা করুন

Perfetto UI-তে ট্রেস খুলুন, আপনার অ্যাপের প্রসেস গ্রুপ বড় করুন এবং নিম্নলিখিত কাউন্টার ট্র্যাক পিন করুন যাতে সেগুলি টাইমলাইনের সবচেয়ে উপরে থাকে:

  • mem.virt: MADV_DONTNEEDটি কল জুড়ে ফ্ল্যাট থাকে, যা নিশ্চিত করে যে ভার্চুয়াল অ্যাড্রেস রিজার্ভেশন (mmap) অপরিবর্তিত রয়েছে।
  • mem.rss.anon: ঠিক সেই টাইমস্ট্যাম্পে দ্রুত কমে যায় যেখানে আপনার অ্যাপ ডার্টি অ্যানোনিমাস এরিনা বা হিপ অঞ্চলে MADV_DONTNEED সহ madvise কল করে।
  • mem.rss.file: আপনার অ্যাপ যখন ফাইল-ব্যাকড mmap অঞ্চলে MADV_DONTNEED সহ madvise কল করে, তখন ধাপগুলি কমে যায়, সাথে mm_filemap_delete_from_page_cache ftrace ইভেন্টের একটি বার্স্ট হয় যখন POSIX_FADV_DONTNEED সহ posix_fadvise পৃষ্ঠা ক্যাশে থেকে ক্লিন পৃষ্ঠাগুলি সরিয়ে দেয়।
  • mem.swap: MADV_DONTNEED যখন আগে ZRAM-এ কম্প্রেস করা বেনামী পৃষ্ঠা বাতিল করে দেয় তখন কমে যায়।
একটি প্রসেসের জন্য Perfetto কাউন্টার ট্র্যাক। mem.rss.anon এবং
       mem.rss.file স্টেপ আপ হয় যখন একটি এরিনা এবং একটি অ্যাসেট ফাইল পপুলেট হয় এবং
       madvise এবং posix_fadvise-এর পরে স্টেপ ব্যাক ডাউন হয়, যখন mem.swap এবং
       mem.virt ফ্ল্যাট থাকে।
ছবি ২. পৃষ্ঠা সংক্রান্ত পরামর্শের জন্য কল করার পরে RSS ড্রপ করা।

ছবি ২-এ MemoryLab স্যাম্পেল অ্যাপের জন্য এইসব ট্র্যাক দেখানো হয়েছে। একই ভিউ এক্সপ্লোর করতে, Perfetto UI-তে স্যাম্পেল ট্রেস খুলুন।

PerfettoSQL-এর মাধ্যমে পিক ও ফাইনাল মেমরি কোয়েরি করুন

আপনার টার্গেট প্রসেসের জন্য mem.virt, mem.rss.anon, mem.rss.file এবং mem.swap-এর পিক ও ফাইনাল ভ্যালু তুলনা করতে Perfetto UI-তে (কোয়েরি (SQL) ট্যাব) নিম্নলিখিত PerfettoSQL কোয়েরি রান করুন:

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;

ছবি ২-এ ট্রেসের জন্য স্যাম্পেল আউটপুট:

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 MB এবং mem.rss.file-এর পিক থেকে 128 MB কমে যায়, যদিও 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;

ছবি ২-এ ট্রেসের জন্য স্যাম্পেল আউটপুট:

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 হল একই অ্যাপ ডেটা ডিরেক্টরির দুটি নাম। অ্যাসেট ফাইলের (৩৫,৭৮১ পৃষ্ঠা) জন্য ইভিকশন সংখ্যা ফাইলের (৪ কেবিতে ৩২,৭৬৮ পৃষ্ঠা) থেকে বেশি, কারণ অ্যাপটি এটি আবার পড়ার এবং posix_fadvise কল করার আগে kswapd মেমরি প্রেসারের অধীনে ফাইলের অংশটি আগেই ইভিক্ট করে দিয়েছে।

একসাথে অনেক আইনোড সমাধান করে এমন স্ক্রিপ্টের জন্য, আইনোড এবং তারপর ফাইলে ত্রুটি সমাধান করা দেখুন।


← kswapd ও lmkd ইন্টার‍্যাকশন | ↑ উপরে | ব্যাকগ্রাউন্ড প্রসেস কমানো →