إنّ تحرير عنصر في رمز تطبيقك أو إغلاق بث مؤقت لا يؤدي دائمًا إلى إعادة ذاكرة الوصول العشوائي (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 للنواة نصائح حول كيفية توقّع العملية
لاستخدام نطاق عناوين افتراضية. بالنسبة إلى عمليات الربط المجهولة الخاصة (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 بالكامل:
- يتم استدعاء
madvise(addr, length, MADV_DONTNEED)في نطاق العناوين الافتراضية المعيّنة لإلغاء تعيين هذه الصفحات من جداول صفحات العملية (RssFiledrops). - يتم استدعاء
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.
يعرض الشكل 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 | ↑ للأعلى | عملية في الخلفية بسيطة →