שחרור אובייקט בקוד של האפליקציה או סגירת זרם זמני לא תמיד מחזירים זיכרון RAM פיזי למערכת ההפעלה. מקציבים מקוריים בהתאמה אישית, סביבות זמן ריצה לאיסוף אשפה וטבלאות גיבוב (hash) עם כתובות פתוחות, שומרים לעיתים קרובות על זירות של זיכרון וירטואלי שמגובות על ידי דפים פיזיים, הרבה אחרי שהנתונים הלוגיים שבתוכם נעלמים. באופן דומה, קריאה או מיפוי זיכרון של ארכיוני נכסים גדולים מאכלסים את מטמון הדפים של הליבה, שנשאר מחויב לקבוצת בקרת הזיכרון (memcg) של האפליקציה.
בדף הזה מוסבר איך להשתמש בקריאות המערכת madvise ו-posix_fadvise כדי להחזיר למערכת ההפעלה דפים אנונימיים ודפים מגובים בקבצים שלא נמצאים בשימוש, תוך שמירה על שלמות הקצאות הכתובות הווירטואליות ותיאורי הקבצים, ואיך לאמת את ההשפעה ב-adb shell וב-Perfetto.
למה הקצאה לוגית שומרת על זיכרון RAM פיזי
ליבת Linux עוקבת אחרי הזיכרון הפיזי ברמת הפירוט של דפי החומרה, שהם בגודל 4KB או 16KB במכשירי Android. כשבתהליך מסוים מוקצה אזור של מרחב כתובות וירטואליות באמצעות mmap, הליבה יוצרת אזור זיכרון וירטואלי (VMA) בלי להקצות זיכרון RAM פיזי. הקרנל מקצה דף פיזי ומחייב עליו רק כשהתהליך נוגע בדף הזה בפעם הראשונה. בזיכרון אנונימי, קריאה ראשונה ממפה דף אפס משותף, כך שרק כתיבה מקצה זיכרון RAM.
אחרי שדף נטען ל-RAM, הליבה לא יודעת אם הבייטים בתוך הדף עדיין מייצגים אובייקטים פעילים בזמן הריצה של האפליקציה. יש שני דפוסים נפוצים שגורמים לדפים פיזיים להישאר בזיכרון אחרי שהאפליקציה מסיימת להשתמש בהם:
- זירות ומאגרים אנונימיים: מקציבים מותאמים אישית (כמו מקציבי slab, pool או frame-arena), סביבות זמן ריצה מנוהלות עם מרחבי ערימה לא ניידים וטבלאות גיבוב גדולות שומרים טווחי כתובות וירטואליות רציפות עם
mmapבאמצעותMAP_PRIVATE | MAP_ANONYMOUS. כשהאפליקציה מאפסת זירה, משחררת בלוק בחזרה למאגר מותאם אישית או מנקה טבלת גיבוב (hash) על ידי איפוס המטא-נתונים שלה, הדפים הבסיסיים נשארים מלוכלכים. בלי רמז מפורש לליבה, הדפים האלה ממשיכים להיספר כחלק מגודל קבוצת הדפים בזיכרון (RSS) וגודל קבוצת הדפים היחסי בזיכרון (PSS) האנונימיים של התהליך, וכחלק מהחיוב שלmemory.currentcgroup. כשיש עומס על הזיכרון, הליבה דוחסת אותם ל-ZRAM, שם הם עדיין תופסים שטח החלפה שמשויך ל-memory.swap.current. - מטמון דפים מגובה קבצים מסטרימינג של נכסים: כשהאפליקציה קוראת חבילות נכסים גדולות, ארכיונים ברמה, מאגרי מדיה או מודלים של למידת מכונה באמצעות
read,preadאוmmapמגובה קבצים, הליבה שומרת במטמון כל דף שנקרא במטמון הדפים ומחייבת את הדפים האלה במונהfileבקובץmemory.statשל ה-cgroup של התהליך. אם האפליקציה שלכם מבטלת את הדחיסה של הנכס הזה או מעתיקה אותו למאגר נתונים אנונימי בזמן הריצה, או מעלה אותו ל-GPU, שמירת דפי הקובץ הגולמי במטמון הדפים משכפלת את הנכס ב-RAM.
פרסום דפים אנונימיים
קריאת המערכת madvise נותנת לליבה (kernel) מידע על האופן שבו התהליך צפוי להשתמש בטווח של כתובות וירטואליות. במיפויים אנונימיים פרטיים (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 פועל על דפים שלמים. היא דורשת כתובת התחלה שתואמת לדף, ומעגלת את האורך עד לגבול הדף הבא. אם משחררים טווח בתוך זירה משותפת שבה בייטים סמוכים עדיין מכילים אובייקטים פעילים, עיגול הטווח כלפי חוץ יכול לאפס הקצאות פעילות שחולקות את דפי הגבול.
כדי לתמוך במכשירים עם דפים בגודל 4KB ו-16KB, תמיד צריך לשלוח שאילתה לגבי גודל הדף בזמן הריצה באמצעות 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, צריך לתייג את ה-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: הנחיה לליבה (kernel) שלא תהיה גישה לטווח הבייטים שצוין בעתיד הקרוב. הקרנל מפנה מיד דפים נקיים ולא ממופים שנשמרו במטמון בטווח הזה, שמיושר לפי דפים, ממטמון הדפים. כך הוא מקטין את המונהfileב-memory.statואת החיוב שלmemory.currentב-cgroup. -
POSIX_FADV_SEQUENTIAL: מכפיל את חלון ה-readahead של הליבה (kernel) לתיאור הקובץ, ומשפר את התפוקה כשמעבירים בסטרימינג ארכיון גדול מההתחלה ועד הסוף. כדאי לשלב את זה עם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)בטווח הכתובות הווירטואליות הממופות כדי לבטל את המיפוי של הדפים האלה מטבלאות הדפים של התהליך (RssFileטיפות). - צריך להפעיל את
posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED)בהיסט הקובץ המתאים כדי שהקרנל יפנה גם את הדפים הנקיים שלא מופו מהמטמון של הדפים (fileב-memory.statטיפות).
הנחיות לגבי דפי ייעוץ
כדאי ליישם את השיטות האלה כשמשלבים את madvise ו-posix_fadvise במקצים, בטועני נכסים ובמטפלים במחזור החיים:
- הפעלת קריאות באצווה לאזורים גסים: כל קריאה ל-
madviseנועלת את מפת הזיכרון של התהליך (mmap_lock), עוברת על טבלאות דפים ומבטלת את הרשומות התואמות של מאגר הנתונים הזמני לתרגום כתובות (TLB) בליבות מעבד אחרות (עם הוראות תחזוקה של TLB בשידור ב-Arm ופסיקות בין מעבדים ב-x86). אל תקראו ל-madviseבכל הקצאה קטנה. לשלב טווחי זיכרון פנויים ולייעץ לליבה בחלקים של 64KB עד 1MB או יותר, או לטאטא טווחי זיכרון פנויים במהלך גבולות של מסגרות ומעברים ברקע (onStopו-TRIM_MEMORY_UI_HIDDEN). - החזרת קיבולת ריקה בטבלאות גיבוב (hash) ובמאגרים: כשמנקים טבלת גיבוב גדולה, מאגר אובייקטים או מאגר נתונים זמני אחרי מעבר בין סצנות או סנכרון נתונים, צריך לכווץ את ההקצאה של הגיבוי או להנפיק
MADV_DONTNEEDבמערך הגיבוי שנוקה. אם שומרים טבלה בנפח 32MB עם כתובות פתוחות ומוקצים לה מצבות, כל 32MB של הדפים הפיזיים יחויבו בתהליך שלכם. - Flush dirty file data before evicting it:
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 ב-AOSP, שנותנת לזירה שלה את השם memorylab-arena. מחליפים את com.example.app ואת memorylab-arena בשם החבילה ובשם הזירה. לפני שקוראים ל-madvise עם MADV_DONTNEED בזירת 64MB שנגעו בה, מריצים:
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 נשאר 65,536KB (64MB), מה שמאשר שטווח הכתובות הווירטואליות עדיין שמור, והערכים של RSS, PSS ו-private dirty יורדים ל-0KB.
בודקים את memory.current ואת memory.stat ב-cgroup
כדי לאמת גם את החזרת הדפים האנונימיים וגם את פינוי המטמון של הדפים שלא מופו (שלא מופיעים בטבלאות הדפים של התהליך אם קוראים אותם באמצעות pread), בודקים את הספרייה cgroup v2 של התהליך. ב-Android, לכל תהליך של אפליקציה יש cgroup משלו (למשל /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
'
לפני שחרור של זירת זיכרון אנונימית בנפח 64MB ופינוי של קובץ נכס בנפח 128MB שממופה בזיכרון ממטמון הדפים:
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 יורד ב-64MB ו-file יורד ב-128MB, מה שמפחית את memory.current בכ-192MB בלי לסגור את תיאור הקובץ או לבטל את המיפוי של מרחב הכתובות הווירטואליות של הזירה. הערך של memory.swap.current כמעט לא משתנה, כי כמעט אף אחד מדפי הזירה לא הועבר ל-ZRAM. הערכים שלכם תלויים במכשיר ובכמות הקובץ שהליבה כבר פינתה בעצמה. למידע נוסף על האופן שבו הליבה מחייבת ומחזירה זיכרון לכל cgroup, ראו החזרת זיכרון: פינוי והחלפה.
ניתוח של החזרת דפים ב-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באזורי heap או בזירות אנונימיות מלוכלכות. -
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 (בכרטיסייה Query (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 יורד ב-63MB ו-mem.rss.file יורד ב-128MB מהשיא שלהם, ו-mem.virt מסתיים בשיא שלו כי ההזמנות הווירטואליות לא השתנו.
פתרון בעיות של דפים שהוסרו מקבצים
האירועים mm_filemap_add_to_page_cache ו-mm_filemap_delete_from_page_cache מזהים קבצים לפי מספר inode (i_ino) ולא לפי נתיב. כדי לוודא שהדפים שהוסרו שייכים לקובץ שציינתם, סופרים את ההסרות לכל inode:
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 דפים בנפח 4KB), כי kswapd כבר פינה חלק מהקובץ בגלל עומס בזיכרון לפני שהאפליקציה קראה אותו בחזרה וקראה ל-posix_fadvise.
במאמר פתרון תקלות ב-inodes ואז בקבצים מופיע סקריפט שפותר הרבה inodes בבת אחת.
← אינטראקציה בין kswapd ל-lmkd | ↑ למעלה | תהליך ברקע של Lean →