إرجاع صفحات الذاكرة إلى نظام التشغيل

إنّ تحرير عنصر في رمز تطبيقك أو إغلاق بث مؤقت لا يؤدي دائمًا إلى إعادة ذاكرة الوصول العشوائي (RAM) الفعلية إلى نظام التشغيل. غالبًا ما تحتفظ أدوات التخصيص الأصلية المخصّصة، وأوقات التشغيل التي يتم جمعها من المهملات، وجداول التجزئة المفتوحة العناوين بمساحات الذاكرة الافتراضية التي تظل مدعومة بصفحات فعلية بعد فترة طويلة من اختفاء البيانات المنطقية بداخلها. وبالمثل، فإنّ قراءة أرشيفات مواد العرض الكبيرة أو ربطها بالذاكرة يؤدي إلى ملء ذاكرة التخزين المؤقت لصفحة النواة، والتي تظل مشحونة في مجموعة التحكّم في الذاكرة (memcg) الخاصة بتطبيقك.

توضّح هذه الصفحة كيفية استخدام استدعاءات النظام madvise وposix_fadvise لإعادة الصفحات المجهولة المصدر والصفحات المستندة إلى الملفات غير المستخدَمة إلى نظام التشغيل مع الحفاظ على سلامة حجوزات العناوين الافتراضية وواصفات الملفات، وكيفية التحقّق من التأثير في adb shell وPerfetto.

أسباب احتفاظ إلغاء التخصيص المنطقي بذاكرة الوصول العشوائي (RAM) المادية

تتتبّع نواة Linux الذاكرة الفعلية على مستوى صفحات الأجهزة، والتي تبلغ سعتها 4 كيلوبايت أو 16 كيلوبايت على أجهزة Android. عندما تحجز عملية ما منطقة من مساحة العناوين الافتراضية باستخدام mmap، تنشئ النواة منطقة ذاكرة افتراضية (VMA) بدون تخصيص ذاكرة وصول عشوائي (RAM) فعلية. تخصّص النواة صفحة فعلية وتحتسبها فقط عندما تلمس العملية تلك الصفحة لأول مرة. بالنسبة إلى الذاكرة المجهولة، يتم ربط صفحة صفرية مشتركة عند القراءة الأولى، لذا يتم تخصيص ذاكرة الوصول العشوائي (RAM) عند الكتابة فقط.

بعد تحميل الصفحة في ذاكرة RAM، لا يعرف النواة ما إذا كانت وحدات البايت داخل هذه الصفحة لا تزال تمثّل عناصر نشطة في وقت تشغيل تطبيقك. هناك نمطان شائعان يؤديان إلى بقاء الصفحات الفعلية في الذاكرة بعد انتهاء تطبيقك من استخدامها:

  • المساحات المخصّصة والمجمّعة المجهولة: تخصّص أدوات التخصيص المدمجة المخصّصة (مثل أدوات تخصيص الألواح أو المجمّعات أو المساحات المخصّصة للإطارات) وأوقات التشغيل المُدارة التي تتضمّن مساحات تخزين مؤقتة غير متحركة وجداول التجزئة الكبيرة نطاقات عناوين افتراضية متجاورة باستخدام mmap من خلال MAP_PRIVATE | MAP_ANONYMOUS. عندما يعيد تطبيقك ضبط ساحة، أو يحرر كتلة مرة أخرى إلى مجموعة مخصصة، أو يمسح جدول تجزئة عن طريق تصفير بياناته الوصفية، تظل الصفحات الأساسية غير نظيفة. بدون تلميح صريح إلى النواة، يستمر احتساب هذه الصفحات ضمن حجم مجموعة البيانات الدائمة (RSS) وحجم المجموعة النسبي (PSS) المجهولَين للعملية ورسوم مجموعة التحكّم (cgroup) في memory.current. وعندما يكون هناك ضغط على الذاكرة، تضغطها النواة في ZRAM، حيث تظل تشغل مساحة إبدال يتم احتسابها ضمن memory.swap.current.
  • ذاكرة التخزين المؤقت للصفحات المستندة إلى الملفات من تدفق مواد العرض: عندما يقرأ تطبيقك حِزم مواد عرض كبيرة أو أرشيفات مستويات أو حاويات وسائط أو نماذج تعلُّم الآلة باستخدام read أو pread أو mmap المستندة إلى الملفات، تخزِّن النواة مؤقتًا كل صفحة مقروءة في ذاكرة التخزين المؤقت للصفحات وتحتسب هذه الصفحات في عدّاد file في ملف memory.stat لمجموعة التحكم (cgroup) الخاصة بالعملية. إذا كان تطبيقك يفك ضغط مادة العرض هذه أو ينسخها إلى مخزن مؤقت مجهول الهوية لوقت التشغيل أو يحمّلها إلى وحدة معالجة الرسومات، فإنّ الاحتفاظ بصفحات الملفات الأولية في ذاكرة التخزين المؤقت للصفحات يؤدي إلى تكرار مادة العرض في ذاكرة الوصول العشوائي.
يتم إخلاء صفحات الذاكرة المؤقتة التي تم تعديلها باستخدام madvise وصفحات مواد العرض المخزّنة مؤقتًا باستخدام posix_fadvise، بينما يظل النطاق الافتراضي وواصِف الملف كما هما.
الشكل 1: استعادة الصفحات الفعلية من المساحات الافتراضية وذاكرة التخزين المؤقت للصفحات

نشر صفحات مجهولة المصدر

يقدّم استدعاء النظام madvise للنواة نصائح حول كيفية توقّع العملية لاستخدام نطاق عناوين افتراضية. بالنسبة إلى عمليات الربط المجهولة الخاصة (MAP_PRIVATE | MAP_ANONYMOUS)، يؤدي تمرير MADV_DONTNEED إلى توجيه النواة إلى التجاهل الفوري للصفحات الفعلية وأي إدخالات ZRAM تم تبديلها تدعم النطاق المحدد المحاذي للصفحة.

بعد madvise مع MADV_DONTNEED، يتم عرض ما يلي:

  • يظل نطاق العناوين الافتراضية محجوزًا وصالحًا في مساحة عناوين العملية (لا يتغيّر VmSize)، وبالتالي تظل المؤشرات وحدود المساحة سليمة بدون استدعاء munmap أو mmap.
  • يتم إلغاء ربط الصفحات الفعلية وإخلاء مساحتها على الفور، ما يقلّل من RssAnon، Pss، وعداد anon في memory.stat، وmemory.swap.current لأي صفحات كانت في ZRAM.
  • أي قراءة أو كتابة لاحقة لعنوان في هذا النطاق تخطئ في صفحة جديدة مملوءة بالصفر عند الطلب.
علامة النصيحة ربط الأهداف سلوك النواة على 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) ليتوافق مع الأجهزة التي تستخدم صفحات بحجم 4 كيلوبايت و16 كيلوبايت، وتقريب عنوان البدء الداخلي إلى حد الصفحة التالي وعنوان النهاية الداخلي إلى حد الصفحة السابق. لمزيد من الإرشادات حول أدوات تخصيص الذاكرة التي لا تعتمد على حجم الصفحة، يُرجى الاطّلاع على تحسين أدوات تخصيص الذاكرة المخصّصة.

#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، يمكنك تصنيف VMA باستخدام prctl من خلال 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: يوجّه النواة إلى أنّ نطاق البايت المحدّد لن يتم الوصول إليه في المستقبل القريب. يزيل النواة على الفور الصفحات المخزّنة مؤقتًا النظيفة وغير المعيّنة في هذا النطاق المحاذي للصفحة من ذاكرة التخزين المؤقت للصفحة، مما يقلل من عداد file في memory.stat ورسوم memory.current لمجموعة التحكم الخاصة بك.
  • ‫POSIX_FADV_SEQUENTIAL: يضاعف نافذة القراءة المسبقة للنواة لواصف الملف، ما يحسّن معدل النقل عند بث أرشيف كبير من البداية إلى النهاية. يمكنك إقران ذلك مع POSIX_FADV_DONTNEED على الأجزاء اللاحقة أثناء استخدامها لكي يظل حجم ذاكرة التخزين المؤقت للصفحة محدودًا في نافذة منزلقة صغيرة.
  • POSIX_FADV_RANDOM: لإيقاف القراءة المسبقة التخمينية للنواة على واصِف الملف يُستخدم هذا الخيار عند إجراء عمليات بحث متفرقة وغير متجاورة حسب إزاحة البايت في ملف أرشيف أو قاعدة بيانات كبير، بحيث لا تؤدي كل عملية قراءة صغيرة إلى سحب الصفحات المجاورة غير المستخدمة إلى ذاكرة التخزين المؤقت للصفحة. بالنسبة إلى الملفات المعيّنة في الذاكرة، يمكنك استدعاء madvise باستخدام MADV_RANDOM في عملية التعيين بدلاً من ذلك.

يعرض المثال التالي أرشيفًا كبيرًا لمواد العرض في أجزاء، ما يتيح القراءة المسبقة المتسلسلة مسبقًا وإزالة كل نطاق تمت معالجته ومتوافق مع الصفحة من ذاكرة التخزين المؤقت للصفحة:

#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 المستندة إلى الملفات

إذا كان تطبيقك يربط ملف أصول كبيرًا بالذاكرة باستخدام mmap بدلاً من قراءته باستخدام pread، فإنّ استدعاء posix_fadvise باستخدام POSIX_FADV_DONTNEED وحده لا يؤدي إلى إزالة الصفحات التي لا تزال مرتبطة بجداول صفحات العملية. لإسقاط نطاق مستهلك من تعيين ملف للقراءة فقط بدون إلغاء تعيين VMA بالكامل:

  1. يتم استدعاء madvise(addr, length, MADV_DONTNEED) في نطاق العناوين الافتراضية المعيّنة لإلغاء تعيين هذه الصفحات من جداول صفحات العملية (RssFile drops).
  2. يتم استدعاء posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED) على إزاحة الملف المقابلة حتى تطرد النواة أيضًا الصفحات النظيفة غير المعيّنة من ذاكرة التخزين المؤقت للصفحة (file في memory.stat قطرة).

اتّباع إرشادات نصائح الصفحة

يُرجى تطبيق هذه الممارسات عند دمج madvise وposix_fadvise في أدوات التخصيص وأدوات تحميل مواد العرض ومعالِجات مراحل النشاط:

  • تجميع طلبات الاستدعاء في مناطق تقريبية: يتطلّب كل طلب استدعاء madvise قفل خريطة الذاكرة (mmap_lock) للعملية، ويتنقّل بين جداول الصفحات، ويبطل إدخالات مخزن الترجمة المؤقت (TLB) المطابقة على أنوية وحدة المعالجة المركزية الأخرى (باستخدام تعليمات صيانة مخزن الترجمة المؤقت للبث على Arm ومقاطعات المعالجات على x86). لا تستدعِ madvise عند كل عملية إلغاء تخصيص صغيرة. يمكنك دمج المساحات الفارغة وإبلاغ النواة بأجزاء تتراوح بين 64 كيلوبايت و1 ميغابايت أو أكبر، أو يمكنك مسح المساحات الفارغة أثناء حدود الإطارات وعمليات الانتقال في الخلفية (onStop وTRIM_MEMORY_UI_HIDDEN).
  • استعادة السعة الفارغة في جداول التجزئة ومجموعات البيانات: عند محو جدول تجزئة كبير أو مجموعة بيانات أو مخزن مؤقت للتخزين المؤقت بعد انتقال المشهد أو مزامنة البيانات، يمكنك إما تقليص تخصيص النسخ الاحتياطي أو إصدار MADV_DONTNEED عبر مصفوفة النسخ الاحتياطي التي تم محوها. إنّ الاحتفاظ بجدول مفتوح العناوين بسعة 32 ميغابايت مخصّص مع شواهد القبور يؤدي إلى تحميل جميع الصفحات الفعلية بسعة 32 ميغابايت على العملية.
  • محو بيانات الملفات غير النظيفة قبل إزالتها: لا يزيل POSIX_FADV_DONTNEED سوى صفحات ذاكرة التخزين المؤقت النظيفة. إذا كان تطبيقك يكتب ملفًا مؤقتًا أو ملف ذاكرة تخزين مؤقت كبيرًا لن يقرأه مرة أخرى على الفور، يمكنك استدعاء fdatasync قبل posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) حتى تتم كتابة الصفحات غير الصالحة في مساحة التخزين ثم يتم إسقاطها من ذاكرة الوصول العشوائي (RAM).
  • عدم إزالة الرموز أو مواد العرض المشتركة التي يتم الوصول إليها بشكل متكرر: تجنَّب استدعاء POSIX_FADV_DONTNEED في ملفات DEX أو ODEX أو VDEX أو المكتبات المشتركة (.so) أو مواد عرض واجهة المستخدم التي يتم الوصول إليها بشكل متكرر. ويؤدي إخلاء صفحات الرموز البرمجية أو مواد العرض النشطة إلى فرض أخطاء في تخزين البيانات بشكل متزامن، ما يؤدي إلى توقُّف سلاسل المحادثات وزيادة الضغط على عمليات الإدخال والإخراج، كما هو موضّح في معلومات توقُّف الضغط (PSI).

التحقّق من استعادة الصفحة في واجهة المستخدم

يُرجى التأكّد من أنّ madvise وposix_fadvise يستعيدان ذاكرة الوصول العشوائي (RAM) الفعلية باستخدام showmap وملفات تدوين سجلات الاستخدام الخاصة بتطبيقك في cgroup v2.

فحص الحجم الافتراضي مقابل الحجم الفعلي باستخدام showmap

عند تسمية ساحة مجهولة باستخدام PR_SET_VMA_ANON_NAME، يعرض showmap كلاً من الحجز الافتراضي (virtual size) والصفحات الفعلية المقيمة (RSS وPSS). يأتي نموذج الناتج التالي من نموذج تطبيق MemoryLab في مشروع Android المفتوح المصدر (AOSP)، والذي يسمي ساحته 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

للتحقّق من استرداد الصفحات المجهولة المصدر وإخلاء ذاكرة التخزين المؤقت للصفحات غير المرتبطة (التي لا تظهر في جداول صفحات العمليات إذا تمت قراءتها باستخدام pread)، يمكنك فحص دليل cgroup v2 الخاص بالعملية. على Android، لكل عملية تطبيق مجموعة تحكّم خاصة بها (مثل /sys/fs/cgroup/apps/uid_<UID>/pid_<PID>/، حسب الجهاز). يعثر الأمر التالي على الدليل من السطر 0:: في /proc/<PID>/cgroup ويطبع العدادات:

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

بعد استدعاء madvise مع MADV_DONTNEED على الساحة وعلى ربط ملف مادة العرض، ثم posix_fadvise مع POSIX_FADV_DONTNEED على واصف ملف مادة العرض:

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

ينخفض حجم anon بمقدار 64 ميغابايت وينخفض حجم file بمقدار 128 ميغابايت، ما يقلل من حجم memory.current بمقدار 192 ميغابايت تقريبًا بدون إغلاق واصِف الملف أو إلغاء ربط مساحة العنوان الافتراضي في الساحة. لا يتغيّر memory.swap.current تقريبًا، لأنّه لم يتم تبديل أي من صفحات الساحة تقريبًا إلى ZRAM. تعتمد القيم على الجهاز ومقدار الملف الذي أزالته النواة من تلقاء نفسها. لمزيد من المعلومات حول كيفية تحصيل النواة للذاكرة واستعادتها لكل مجموعة تحكّم، يُرجى الاطّلاع على استعادة الذاكرة: الإخلاء والمبادلة.

تحليل استرداد الصفحات في Perfetto

تتيح لك أداة Perfetto ربط طلبات madvise وposix_fadvise بعدّادات ذاكرة التخزين المؤقت للصفحات وجدول الصفحات في النواة على مخطط زمني موحّد.

تسجيل تتبُّع

التقِط تتبُّعًا باستخدام linux.process_stats لعدّادات العمليات الدورية وlinux.ftrace لأحداث RSS في الوقت الفعلي وأحداث ذاكرة التخزين المؤقت للصفحات، ثم اسحبه إلى جهاز الكمبيوتر:

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: تنخفض بشكل حاد عند الطابع الزمني الدقيق الذي يستدعي فيه تطبيقك madvise مع MADV_DONTNEED في مناطق الذاكرة المؤقتة أو مناطق الذاكرة المجهولة غير المحفوظة.
  • mem.rss.file: يتم خفض المستوى عندما يستدعي تطبيقك madvise باستخدام MADV_DONTNEED في منطقة mmap مستندة إلى ملف، مصحوبة بسلسلة من أحداث ftrace في mm_filemap_delete_from_page_cache عندما يطرد posix_fadvise باستخدام POSIX_FADV_DONTNEED الصفحات النظيفة من ذاكرة التخزين المؤقت للصفحة.
  • ‫mem.swap: ينخفض عندما يتجاهل MADV_DONTNEED الصفحات المجهولة المصدر التي تم ضغطها سابقًا في ZRAM.
مسارات عدّاد Perfetto لعملية واحدة. يرتفع mem.rss.anon وmem.rss.file عند تعبئة ملف ساحة وملف مادة عرض، وينخفضان بعد madvise وposix_fadvise، بينما يظل mem.swap وmem.virt ثابتَين.
الشكل 2. انخفاض في عدد المشتركين في RSS بعد مكالمات لتقديم نصائح حول الصفحة

يعرض الشكل 2 هذه المسارات لنموذج تطبيق MemoryLab. لاستكشاف طريقة العرض نفسها، افتح نموذج التتبُّع في واجهة مستخدم Perfetto.

الاستعلام عن ذروة استخدام الذاكرة والذاكرة النهائية باستخدام PerfettoSQL

نفِّذ طلب البحث التالي في PerfettoSQL في واجهة مستخدم Perfetto (علامة التبويب طلب البحث (SQL)) لمقارنة القيم القصوى والنهائية لـ 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 الملفات حسب رقم inode (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 فريدة فقط داخل نظام ملفات واحد، لذا عليك حصر البحث في نظام الملفات الذي يحتوي على بيانات تطبيقك:

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

مثال على الناتج لـ inode 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 صفحة) أكبر من الملف نفسه (32,768 صفحة بحجم 4 كيلوبايت)، لأنّ kswapd قد أخلت جزءًا من الملف تحت ضغط الذاكرة قبل أن يقرأه التطبيق مرة أخرى ويستدعي posix_fadvise.

للحصول على نص برمجي يحلّ العديد من أخطاء inodes في آنٍ واحد، يُرجى الاطّلاع على حلّ الأخطاء في inodes، ثم في الملفات.


← التفاعل بين kswapd وlmkd | ↑ للأعلى | عملية في الخلفية بسيطة →