ב-Android בגרסה 17 ואילך, הפלטפורמה מנהלת את הזיכרון של תהליכי המערכת באמצעות שני מנגנונים משלימים של Linux cgroup v2, שפועלים לצד מגביל הזיכרון הכללי של האפליקציה:
- רצפות הגנה של daemons של המערכת (
SystemMemoryBudgetManagerעםmemory.min): שמירות cgroup v2memory.minבעלות כוונון עצמי (מוגדרות ל-90% מערך העבודה המיושב של כל daemon ומוגבלות באופן גלובלי ל-10% מסך ה-RAM הפיזי) מגנות על daemons אינטראקטיביים קריטיים ותהליכי מעטפת (system_server,surfaceflinger, הגרפיקהcomposerHAL,audioserver,cameraserver,netd,SystemUIומפעיל הבית) תחת לחץ זיכרון, ומחליפות הצמדת קבצים סטטיתPinnerServicemlock. - מגבלות הזיכרון של אפליקציות המערכת (
<memory-budget>עםmemory.high): מגבלות גמישות שלmemory.highב-cgroup v2 (בדרך כלל 30MB עד 190MB) שמותאמות לעומס העבודה ומוצהרות בקובצי המניפסט של אפליקציות מערכת שפועלות ברקע ובאופן קבוע (Bluetooth, Nfc, CalendarProvider, Traceur,MediaProvider, :PhotoPicker, NetworkStack, AppSearchו-TeleService) מחזירות באופן יזום את המטמון של דפי קבצים קרים ואת ה-heap האנונימי אחרי פרץ פעילות, לפני שנוצר עומס זיכרון גלובלי.
בטבלה הבאה מוצגת השוואה בין שני המנגנונים:
| המאפיין | רמות הגנה של דימון המערכת | מגבלות הזיכרון של אפליקציות המערכת |
|---|---|---|
| שירות פלטפורמה | SystemMemoryBudgetManager |
MemoryLimiter |
| בקרת Cgroup v2 | סף תחתון קשיח להגנה על המותג: memory.min |
תקרת החזרת נפח אחסון רכה של memory.high |
| תהליכי יעד | ממשק משתמש של מעטפת ודמונים אינטראקטיביים שקריטיים לזמן האחזור (system_server, surfaceflinger, composer HAL, audioserver, cameraserver, netd, SystemUI ואפליקציית Home) |
אפליקציות מערכת קבועות וללא ממשק משתמש, ספקים לניהול גישה ושירותים שפועלים ברקע (כמו Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch ו-TeleService) |
| מודל המידות | כוונון עצמי בזמן הריצה (90% מקבוצת העבודה שהוגדרה, מוגבל באופן גלובלי ל-10% מסך ה-RAM) עם אפס טבלאות כוונון לכל מכשיר | מאפייני XML של מניפסט <memory-budget> דקלרטיביים שמכוילים לפי רמת RAM, מצב תהליך וספירת פיקסלים בתצוגה |
| היתרון העיקרי | מונעת קפיצות בממשק המשתמש האינטראקטיבי והשהיות בהתעוררות כשיש עומס על הזיכרון, תוך החלפת רשימות קבצים סטטיות של PinnerService mlock |
מפנה באופן קבוע מטמון קבצים חד-פעמיים לא פעילים ו-heap לאחר פרץ פעילות, כדי להשאיר יותר זיכרון RAM פיזי לאפליקציות בחזית ולאפליקציות שנשמרו במטמון |
מידע נוסף על התחביר של <memory-budget>, מאפייני XML, ממשקי API של זמן ריצה וחשבונאות זיכרון של מערכת ההפעלה זמין במאמרים הגדרת תקציבי זיכרון של אפליקציות והבנה ופתרון בעיות בזיכרון של Android.
הגנה על תהליכי רקע אינטראקטיביים קריטיים באמצעות SystemMemoryBudgetManager
SystemMemoryBudgetManager שומר על התהליכים שנמצאים ישירות בנתיב האינטראקטיבי של המשתמש ב-RAM הפיזי בזמן לחץ על הזיכרון. במקום להצמיד רשימה סטטית של קובצי APK, ODEX ו-VDEX לזיכרון באמצעות mlock (כמו ש-PinnerService עושה עבור config_defaultPinnerServiceFiles ו-config_pinnerHomePinBytes), SystemMemoryBudgetManager מקצה רמות מינימום דינמיות של memory.min ב-cgroup v2 לתהליכים הבאים:
- פרוסת מערכת ליבה (
/sys/fs/cgroup/system):system_server,surfaceflinger, הגרפיקהcomposerHAL,audioserver,cameraserver, ו-netd, וגם כל מערכת מקורית ספציפית לגורם צורה או תהליכי רקע של ספקים שמפורטים ב-config_systemMemoryBudgetAdditionalSystemProcesses(לדוגמה,android.hardware.perceptionhostו-auracamerahalבמכשירי XR). - פרוסת אפליקציה אינטראקטיבית (
/sys/fs/cgroup/apps): התהליך הראשי של חבילת SystemUI שהוגדרה (config_systemUi) והתהליך הראשי של בעל התפקיד "דף הבית" של המשתמש הנוכחי.
כדי להבטיח כיסוי בכל גורמי הצורה, צריך להגן על בעל התפקיד Home לצד config_systemUi. במכשירים שבהם config_systemUi היא חבילת תאימות קלה (כמו Wear OS, שבה תצוגת השעון, ההתראות וכרטיסי המידע פועלים בתוך אפליקציית Home), המעטפת שגלויה למשתמשים עדיין מקבלת הגנה מלאה על קבוצת העבודה. אם כוונת המשתמש של דף הבית מפוענחת לחבילה config_systemUi, התהליך מוגן פעם אחת. רק התהליך הראשי של כל חבילה (processName == packageName) מקבל רצפת הגנה, כך שתהליכי משנה (כמו :screenshot) לא צורכים תקציב הגנה.
מדיניות כוונון עצמי והקצאת cgroup בשלוש רמות
מכיוון שקבוצת העבודה של דימון אינטראקטיבי משתנה מאוד בין גורמי צורה וקיבולות RAM (לדוגמה, SystemUI משתמש בכ-45MB בשעון חכם, 60MB בטלפון של 6GB ו-180MB בטלפון של 16GB), טבלאות חיפוש סטטיות לכל מכשיר הן שבירות. במקום זאת, SystemMemoryBudgetManager מפיק את ערכי המינימום ישירות מהטביעה של כל תהליך בזיכרון הפיזי, באמצעות שלושה קבועים:
| פרמטר מדיניות | ערך ברירת המחדל | התנהגות |
|---|---|---|
| הגבלת תהליכים | 90% מה-memory.current שהוסדר, פחות מטמון קבצים לא ממופה |
ההגדרה הזו מגינה על קוד ממופה חם, על ערימה אנונימית ועל לוח, ומשאירה את 10% הזנב הקרים ביותר ניתנים להחזרה, כך שעדיין אפשר לפנות דפים קרים באמת. מטמון הקבצים שלא מופו (max(0, file - shmem - file_mapped) מתוך memory.stat) מופחת לפני החלת היחס של 90%, כדי שהמטמון של סריקת החבילות בזמן ההפעלה או קריאות חד-פעמיות של נכסים לא ינפחו את הרצפות. |
| מגבלת RAM גלובלית | 10% מנפח ה-RAM הכולל (config_systemMemoryBudgetLowKb) |
תואם למעקה הבטיחות PinnerService config_pinnerMaxPinnedMemoryPercentage. אם סכום מחירי המינימום של 90% מהבקשות גבוה מ-10% מהערך של MemTotal, כל מחירי המינימום מוקטנים באופן יחסי באותו גורם. |
| תדירות הרענון | שנייה אחת, 4 שניות, 16 שניות ו-64 שניות אחרי ההפעלה, ואז כל 30 דקות | סדרה גיאומטרית של התייצבות מתכנסת בקבוצת העבודה של כל דימון לאחר ההפעלה, בלי לקודד באופן קשיח עיכובים בחימום לכל מכשיר, ורענון של מצב יציב למשך 30 דקות עוקב אחר הצמיחה וההתכווצות ההדרגתית של קבוצת העבודה במהלך חיי המכשיר. |
בבקר הזיכרון של Linux cgroup v2 (mm/memcontrol.c:effective_protection), ההגנה האפקטיבית של memory.min על עלה cgroup מוגבלת על ידי memory.min שמוגדר ב-cgroups ההורה והאב הקדמון שלו. אם מגדירים את memory.min רק בספריית עלה pid_<pid>, והערך של uid_<uid>/memory.min הוא 0, לא תהיה הגנה בפועל.
לעומת זאת, הגדרת memory.min קבוע רק בפרוסה העליונה
/sys/fs/cgroup/system מחלקת את ההגנה שלא נתבעה בין כל מזהי ה-UID
והתהליכים הצאצאים באופן יחסי (memory_recursiveprot), ומאפשרת לתהליכים
אבחוניים או משניים שחולקים את uid_1000 (android.uid.system) לשאוב
הגנה מ-surfaceflinger ומ-audioserver.
כדי לספק בידוד מדויק לכל דימון, SystemMemoryBudgetManager כותב הגנה בכל שלוש הרמות של ההיררכיה של cgroup v2 בכל רענון, כפי שמוצג באיור 1:
- צומת תהליך עלה (
pid_<pid>/memory.min): מוגדר לערך המינימלי של 90% מקבוצת העבודה (בקנה מידה) של התהליך. - צומת UID (
uid_<uid>/memory.min): מוגדר לסכום המדויק של ערכי הסף של העלים המנוהלים שלו ב-pid_*, כך שלתהליכים לא מנוהלים שמשתפים את ה-UID הזה (כמו חבילותsharedUserId="android.uid.system"או תהליכי daemon אחרים שלAID_SYSTEM) ישmemory.min = 0והם לא יכולים לבקש הגנה. - פרוסה ברמה העליונה (
/sys/fs/cgroup/system/memory.minו-/sys/fs/cgroup/apps/memory.min): מוגדרת לסכום המדויק שלuid_*הצאצאים שלה. כל ערכיmemory.lowלא עדכניים בצמתים מנוהלים נמחקים ומוחלפים ב-0, וכל ספריותuid_*שנוהלו ברענון קודם (כמו מרכז אפליקציות קודם של דף הבית אחרי שהמשתמש החליף מרכזי אפליקציות) מאופסות ל-0.
איור 1. הקצאת רצפת הגנה בשלוש רמות בין פרוסות של cgroup.
למה משתמשים ב-memory.min במקום ב-memory.low בקרנלים של MGLRU
ב-Multi-Generational LRU (MGLRU, מנוע ברירת המחדל להחזרת דפים בליבות Android מגרסה 6.6 ואילך), memory.low ו-memory.min פועלים באופן שונה ב-mm/vmscan.c:shrink_one:
-
memory.low(דחייה במעבר יחיד): קבוצת בקרה שהשימוש שלה נמוך מ-memory.lowמועברת לסוף ה-LRU של memcg (MEMCG_LRU_TAIL) ומדלגים עליה במעבר יחיד של פינוי מקום. במעבר הבא, תחת לחץ מתמשך על הזיכרון, הליבה מחזירה את ה-cgroup במלואה ומתעדת אירועMEMCG_LOW. בבדיקות עומס במכשירים פיזיים עם צריכת זיכרון אנונימית שגדלה ל-85% מה-RAM הפיזי,memory.lowאפשר ל-SystemUIבשעון חכם לקרוס מ-~45 MB ל-~5 עד 10 MB, ול-audioserverבמכשיר מתקפל בנפח 16GB לרדת מ-15MB ל-2MB לעומת רצפה של ~14 MB. -
memory.min(רצפת הגנה קשיחה): קבוצת בקרה שהשימוש שלה נמוך מ-memory.minלא נכללת בהחזרה הגלובלית. כפי שמוצג באיור 2, באותו עומס עבודה של 85% לחץ על זיכרון ה-RAM, memory.minמשיל רק את 10% מהזנב הקר ביותר של כל דימון, ומחזיק אתSystemUI, surfaceflingerו-audioserverברצפות של 90% עם אפס תקלות אנונימיות וללא עלייה בסיומי תהליכים שלlmkd.
memory.min מספק הקצאה פיזית קשיחה שתואמת להגנה של ההצמדה של PinnerService mlock שהיא מחליפה, עם היתרון שה-LRU של הליבה (ולא רשימת קבצים סטטית) בוחר אילו דפי קוד חם, ערימה וגרפיקה יישארו קבועים.
איור 2. שמירה על קבוצת העבודה של הדימון (daemon) כשהעומס על הזיכרון עולה.
חימום מראש של האתחול ופינוי המטמון של קבצים לא ממופים אחרי האתחול
כדי להחליף את PinnerService הקבוע mlock של config_defaultPinnerServiceFiles, צריך לתאם שתי התנהגויות בזמן ההפעלה:
- חימום מראש של הפעלה ברקע בפעם אחת (
PinnerService): דילוג עלPinnerServicemlockבלי לחמם את מטמון הדפים מעביר כ-95MB של קריאות דיסק של APK, ODEX ו-VDEX שלSystemUIלנתיב ההפעלה הסינכרוני כש-SystemUIמתחיל (OnBootPhase_550), ומוסיף 300 עד 450 אלפיות השנייה להשלמת ההפעלה. לא מספיק להפעיל אתmadvise(MADV_WILLNEED)כיforce_page_cache_raבליבה (kernel) מגביל קריאה אחת שלMADV_WILLNEEDלחלון קריאה מקדימה אחד (בדרך כלל 128KB עד 2MB). במקום זאת, כש-SystemMemoryBudgetManager.shouldSkipDefaultSystemPinningפעיל,PinnerServiceממפה כל קובץ ב-config_defaultPinnerServiceFilesבשרשור ברקע במהלךonStart, מאכלס באופן סינכרוני את הקובץ במטמון הדפים באמצעותOs.mlockוקורא מיד ל-Os.munlock, ל-munmapול-close. כך נשמר תזמון האתחול המהיר, ולא נשארים בייטים נעולים באופן סופי, כך שניתן יהיה לפנות דפי פתיחה (cold startup) בשלב מאוחר יותר. - החרגה והחזרה של מטמון סריקת החבילות של
system_serverבזמן ההפעלה: בזמן ההפעלה,PackageManagerServiceסורק קובצי APK מותקנים, ומשאיר 1.5 עד 2.5GB של מטמון דפי קבצים לא ממופים קרים שחויבו ב-cgroup שלsystem_server. אם הוא נכלל בחישוב שלmemory.minב-system_server, המטמון של האתחול הקר ממלא את המכסה הגלובלית של 10% מה-RAM, ומצמצם אנכית בהתאם לעומס את רצפות ההגנה שלsurfaceflinger, SystemUIו-audioserver. כדי למנוע מחסור במכסה,SystemMemoryBudgetManagerקורא אתmemory.statעבורsystem_server, מחשב את המטמון של הקבצים שלא מופו כ-max(0, file - shmem - file_mapped), ומחסיר את המטמון שלא מופה מ-memory.currentלפני חישוב הסף של 90%, כפי שמוצג באיור 3. בנוסף, 10 שניות אחריPHASE_BOOT_COMPLETED, המנהל מבצע כתיבה חד-פעמית של<unmapped_file> swappiness=0לצומתmemory.reclaimשלsystem_serverבלינוקס 6.2 ובליבות חדשות יותר, כדי לפנות באופן יזום את המטמון של סריקת החבילות הקרות בלי להחליף את ה-heap האנונימי או לפנות את דפי הקוד וה-DEX הממופים. בליבות ישנות יותר (כמוandroid14-6.1) שדוחות אתswappiness=0עםEINVAL, המנהל מדלג על הכתיבה שלmemory.reclaimבמקום לחזור לכתיבה לא מוסמכת של החזרה שתחליף את הערימה האנונימית במהלך ההפעלה.
איור 3. התכנסות של רצפת ההגנה ודרישה מחדש של המטמון לאחר ההפעלה.
דרישות הליבה ושכבות-על של הגדרות המכשיר
SystemMemoryBudgetManager מסתמך על mem_cgroup_calculate_protection בליבת Linux כדי להפיץ את memory.min בין קבוצות cgroup מקוננות. קרנלים נפוצים של Android בגרסאות android15-6.6 ו-android16-6.12 כוללים תמיכה מלאה בהגנה מקוננת של MGLRU. במכשירים עם android14-6.1 שבהם MGLRU מופעל, נדרשים שלושה היסבים בגרסאות מוקדמות יותר (backporting) ב-mm/vmscan.c וב-mm/memcontrol.c, כדי שפרוסות ביניים ריקות וקבוצות בקרה של מזהי משתמשים יפיצו את ההגנה לקבוצות בקרה של תהליכי עלים:
30d77b7eef01(mm/mglru: fix ineffective protection calculation)8aa420617918(mm/mglru: respect min_ttl_ms with memcgs)adb8213014b2(mm: memcg: fix stale protection of reclaim target memcg)
יצרני ציוד מקורי יכולים להתאים אישית או להשבית את SystemMemoryBudgetManager באמצעות שכבות-העל הבאות של משאבי המסגרת:
-
config_systemMemoryBudgetLowKb(מספר שלם, KB):0(ברירת המחדל) מגביל את הסכום של כל ערכי הסף שלmemory.minל-10% מסך זיכרון ה-RAM הפיזי. ערך חיובי מגדיר מגבלה מפורשת בקילו-בייט, וערך שלילי משבית אתSystemMemoryBudgetManagerבמכשיר (שומר על הצמדה סטטית שלmlockמדור קודם שלPinnerService). -
config_systemMemoryBudgetHighKb(מספר שלם, KB):0(ברירת המחדל) משאיר את/sys/fs/cgroup/system/memory.highב-max. ערך חיובי מגדיר תקרה לפרוסה העליונהmemory.high(חייב להיות גדול או שווה לרצפה).
config_systemMemoryBudgetAdditionalSystemProcesses(מערך מחרוזות): רשימה של שמות נוספים של תהליכי דימון (daemon) של המערכת או של הספק, שקריטיים לזמן האחזור, ב-/sys/fs/cgroup/system(בהתאמה ל-/proc/<pid>/commאו לשם הבסיס שלargv[0]ב-/proc/<pid>/cmdlineלמזהי UID של המערכת ושל הספק ב-10000) כדי להגן באמצעות ערכי סף של 90% ב-memory.minלצד ששת תהליכי הדימון של המערכת שמוגדרים כברירת מחדל. צריך להשתמש בשכבת-העל הזו במקום להגדיר אתmemory.minב-init.rcכיSystemMemoryBudgetManagerמחשב מחדש אתuid_<uid>/memory.minכסכום של תהליכי העלה המנוהלים שלו ומנקה צומתיuid_*לא מנוהלים בכל רענון. לדוגמה, במכשיר XR אפשר להגן על הדמונים של HAL למצלמה ולמעקב מרחבי באמצעות שכבת-העל הבאה:<string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses"> <item>android.hardware.perceptionhost</item> <item>auracamerahal</item> </string-array>
config_systemUi(מחרוזת): שם החבילה של אפליקציית SystemUI של הפלטפורמה, שפותרים באמצעותMATCH_SYSTEM_ONLY.
כדי לבדוק את הקומות הפעילות של SystemMemoryBudgetManager ואת צומתי ה-cgroup במכשיר, מריצים את הפקודה:
# Inspect policy state, resolved PIDs, and applied floors in dumpsys meminfo adb shell dumpsys meminfo | sed -n '/System Memory Budgets/,$p'# Inspect raw cgroup v2 memory.min nodes across the system and apps slices adb shell head -n 1 \ /sys/fs/cgroup/system/memory.min \ /sys/fs/cgroup/system/uid_*/memory.min \ /sys/fs/cgroup/system/uid_*/pid_*/memory.min \ /sys/fs/cgroup/apps/memory.min \ /sys/fs/cgroup/apps/uid_*/memory.min \ /sys/fs/cgroup/apps/uid_*/pid_*/memory.min
הגבלת אפליקציות מערכת קבועות וברקע באמצעות תקציבי זיכרון
SystemMemoryBudgetManager מגן על תהליכי רקע אינטראקטיביים מפני החזרה באמצעות רמות memory.min, אבל הרבה אפליקציות מערכת אחרות ושירותי פלטפורמה עם הרשאות פועלים ברקע לתקופות ארוכות בתור תהליכי PERSISTENT, שירותים קשורים שפועלים בחזית או ספקים לניהול גישה שמתבצעות אליהם שאילתות לעיתים קרובות.
בלי מגבלות זיכרון מפורשות, התהליכים האלה לא מוגבלים עד שהם מגיעים למגבלות של מגביל הזיכרון של כמה גיגה-בייט (למשל, 4GB עד 10GB לאפליקציות גלויות ו-2GB עד 5GB לאפליקציות ברקע) או עד שהעומס על הזיכרון הגלובלי מפעיל את kswapd ואת פעילות ה-low-memory killer (lmkd) בכל המערכת.
אם לא מגבילים את תהליכי המערכת ברקע, הם בדרך כלל צוברים כמויות גדולות של זיכרון קר שלא נכלל בקבוצת העבודה הפעילה שלהם:
- מטמון דפים של קבצים לא מוגבלים: שירותים שמוסיפים מדיה לאינדקס, מחפשים במסדי נתונים או מסנכרנים טבלאות של יומנים וספקים (כמו
AppSearch,MediaProviderו-CalendarProvider) ממפים קבצים גדולים בדיסק או מעבירים אותם בסטרימינג. בלי מגבלת cgroupmemory.high, דפים נקיים שמגובים בקבצים מקריאות חד-פעמיות נשארים מחויבים ל-cgroup של התהליך ללא הגבלת זמן, וצורכים מאות מגה-בייטים של זיכרון RAM פיזי. - שמירה של ערימה אנונימית אחרי פרץ פעילות: שירותים שחווים פרצי פעילות חולפים, כמו פרצי גילוי של אודיו ו-BLE ב-
Bluetooth, אימות TLS של פורטל שבוי ב-NetworkStackאו עוזרי ממשק משתמש שעוברים לרקע, יכולים לשמור דפי ערימה אנונימיים קרים זמן רב אחרי שפרץ הפעילות מסתיים.
החלת הצהרות <memory-budget> מכוילות (memory.high מגבלות גמישות, בדרך כלל 30MB עד 190MB) על אפליקציות מערכת מספקת שני יתרונות עיקריים:
- החזרת דפים מקומית ויזומה: כשמערכת חוצה את התקציב שהוגדר לה ל-
memory.high, ליבת Linux מפנה את החזרת הדפים באופן ספציפי לאותה cgroup, מפנה קודם דפים קרים שמגובים בקבצים ודוחסת דפים אנונימיים לא פעילים ל-ZRAM בלי לחכות ללחץ גלובלי על זיכרון המכשיר. - יותר זיכרון RAM זמין לאפליקציות שפועלות בחזית ולמאגר האפליקציות שנשמרו במטמון: הגבלת המטמונים של קבצים קרים בשירותי מערכת שפועלים כל הזמן מחזירה עשרות עד מאות מגה-בייטים של זיכרון RAM פיזי במכשיר, מפחיתה את התקיעות בממשק המשתמש, מפחיתה את הלחץ בהפעלת המצלמה ומשאירה יותר אפליקציות של משתמשים פעילות במצב השמירה במטמון.
הגדרת תקציבי זיכרון לאפליקציות מערכת
אפליקציות מערכת משתמשות באותו תחביר מניפסט של <memory-budget>, מפל מצבי תהליך, שינוי קנה מידה של צפיפות תצוגה וכלל פתרון של סעיף התאמה אחרון כמו אפליקציות כלליות. לרשימה המלאה של המאפיינים, עיינו בחומר העזר בנושא מאפייני XML.
יצרני ציוד מקורי (OEM) יכולים להצהיר על רכיבי <memory-budget> ישירות בתוך AndroidManifest.xml של אפליקציית מערכת, או למזג אותם מקובץ XML נפרד באמצעות המאפיין additional_manifests ב-Android.bp. לפי המוסכמה, ב-AOSP מאוחסנים תקציבים של אפליקציות מערכת בקובצי *-budget.xml נפרדים (ב-frameworks/base/core/memory_budgets/ לחבילות פלטפורמה בסיסיות או לצד מודולים נפרדים כמו packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ ו-packages/services/Telephony/) ומצרפים אותם באמצעות additional_manifests:
android_app {
name: "TeleService",
manifest: "AndroidManifest.xml",
additional_manifests: ["Telephony-budget.xml"],
// ...
}
היקף ברמת החבילה (UID) לעומת היקף ברמת התהליך (PID)
ההצהרה על תקציב בסיסי ברמת החבילה (<application>) או על תקציב לכמה תהליכים (<processes><process>) קובעת אם הליבה אוכפת את memory.high ברמת ה-cgroup של ה-UID (/sys/fs/cgroup/uid_<uid>/memory.high) או ברמת ה-cgroup של כל PID (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), כפי שמוצג באיור 4:
- תקציב ברמת החבילה (
<application>): משתמשים בו כשה-APK מפעיל תהליך יחיד תחת מזהה משתמש ייעודי משלו (לדוגמה,Bluetooth,Nfc,NetworkStack,CalendarProvider,TraceurאוAppSearch). - תקציב ברמת התהליך (
<processes><process>): משתמשים באפשרות הזו כשה-APK משתף אתandroid:sharedUserId(למשלandroid.uid.phoneעם UID1001אוandroid.uid.systemעם UID1000) עם חבילות מערכת אחרות, כדי שתהליכים אחרים שמשתפים את ה-UID הזה לא יוגבלו יחד, או כש-APK יחיד מריץ כמה תהליכים עם תפקידי זיכרון נפרדים (לדוגמה, ה-daemon שלMediaProviderברקע ותהליך המשנה של ממשק המשתמש של:PhotoPicker).
איור 4. מגבלות זיכרון ברמת UID לעומת רמת PID.
בדוגמה הבאה מוצג MediaProvider-budget.xml, שמגדיר את היקף התקציבים ברמת ה-RAM לתהליך ברירת המחדל של MediaProvider (<process>) ותקציב מותאם לתצוגה לתהליך ממשק המשתמש של :PhotoPicker. כל מאפיין android:maxDeviceRamMb תואם למכשירים שבהם ה-RAM הפיזי הכולל (MemTotal) הוא בערך הסף שצוין ב-MB או פחות (או חל על כל קיבולת RAM אם הוא לא צוין). הסעיפים מסודרים מהסעיף שמוגדר כברירת מחדל ללא הגבלה ועד לסעיף עם הסף הקטן ביותר של maxDeviceRamMb, כך שכלל הרזולוציה של הסעיף התואם האחרון בוחר את הרמה המחמירה ביותר שרלוונטית:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.providers.media.module">
<uses-sdk android:targetSdkVersion="10000" />
<application>
<processes>
<process>
<memory-budget
android:maxMb="85" />
<memory-budget
android:maxMb="75"
android:maxDeviceRamMb="12288" />
<memory-budget
android:maxMb="60"
android:maxDeviceRamMb="8192" />
</process>
<process android:process=":PhotoPicker">
<memory-budget
android:maxMb="85"
android:additionalBytesPerDisplayPixel="25" />
</process>
</processes>
</application>
</manifest>
איך כוילו תקציבי ההתייחסות של AOSP במכשירי Google Pixel
תקציבי הזיכרון של אפליקציות המערכת לדוגמה ב-AOSP כוילו במכשירי Google Pixel עם זיכרון RAM פיזי בנפח של 6GB עד 16GB (כולל טלפונים, מכשירים מתקפלים וטאבלטים עם 6GB, 8GB, 12GB ו-16GB) ורזולוציות תצוגה שנעות בין כ-1.2MP (720×1600) לכ-4.5MP (2076×2152).
הכיול שילב שני מקורות נתונים משלימים: טלמטריית שדה של cgroup v2 בכל הצי וסריקות memory.high של מכשירים פיזיים מבוקרים במסגרת חוויות שימוש הכרחיות (CUJ).
ניתוח טלמטריה של cgroup v2 בכל הצי
תמונות מצב תקופתיות של cgroup v2 נאספו מכל המכשירים ופוצלו לפי רמת ה-RAM הפיזי (≤8 GB, 12 GB ו-≥16 GB) ולפי קטגוריית מצב התהליך (foreground, perceptible, background ו-cached). הניתוח הסטטיסטי התבסס על שישה עקרונות:
- פירוק של
memory.currentלקבוצת עבודה לעומת מטמון קבצים שניתן להחזיר: מכיוון ש-memory.highקובע את החיוב של קבוצת הבקרה (cgroup) התושבת (memory.current), כל תמונת מצב פורקה באמצעותmemory.statלערימה אנונימית (anon), דפים אנונימיים שהוחלפו (swap) ומטמון דפים מגובה קבצים (file), כפי שמוצג באיור 5. הביקוש הכולל לאנונימיות שלא ניתנת לדחיסה (anon + swapב-P99) קובע את הרף המינימלי של קבוצת העבודה לתקציב, כדי שהתהליך לא יסתכן במיצוי ה-ZRAM, בעוד שפער גדול ביןmemory.currentP99 ל-anon + swapP99 מציין מטמון של דפי קבצים ש-memory.highיכול להחזיר, בתנאי שהמטמון לא ממופה וקר. - התחשבות בזיכרון ש-
memory.highלא יכול להחזיר: לא כל הקבצים והזיכרון המשותף הם מטמון קר. דפים נעולים (unevictableב-memory.stat, כמו זיכרון משותף שנעול באמצעותmlockאו זיכרון RAM נעול של מכונה וירטואלית שהתהליכים המארחים שלה מחויבים ל-cgroup של האפליקציה) מגדירים רצפה קשיחה מתחת לתקציב, ודפים ממופים (file_mapped) הם בדרך כלל חלק מקבוצת העבודה הפעילה. צריך לבדוק אתunevictable,shmemו-file_mappedלפני שמתייחסים לזיכרון הקבצים כניתן להחזרה. לדוגמה, כ-100MB מהזיכרון שלAppSearchנעולים ואי אפשר לפנות אותם, אז התקציב שלו מכסה את הרצפה הזו בנוסף לקבוצת העבודה של המארח החם, עם מרווח לגדילה של האזור הנעול. - סינון של דילול במצב חוסר פעילות: ספקים לניהול גישה (
CalendarProvider) וכלים למפתחים על פי דרישה (Traceur) מבלים 70% עד 99.9% מתמונות המצב בשטח במצב חוסר פעילות בנפח של כ-1MB עד 3MB. חישוב אחוזונים גולמיים בכל תמונות המצב מדלל באופן משמעותי את ההערכות של P95 ו-P99. במקום זאת, האחוזונים והרווחים בני הסמך חושבו על סמך תמונות מצב פעילות לא מדוללות (סינון של תמונות מצב שלCACHEDותמונות מצב של ערכי בסיס לא פעילים). - שימוש ברווחים ברי-סמך באשכולות של מכשירים: מכיוון שמכשיר אחד תורם הרבה תמונות מצב תקופתיות לאורך זמן, התייחסות לתמונות המצב כדגימות בלתי תלויות מפחיתה את אי-הוודאות בקצוות. כל ההערכות של קבוצות העבודה P95 ו-P99 נבדקו באמצעות רווחים ברי-סמך שמרניים של 95% באשכולות של מכשירים (באמצעות מספר המכשירים הנפרדים
N_devכגודל המדגם בפועל). - התחשבות בפרצי הקצאה זמניים באמצעות
memory.peak: דגימה תקופתית עלולה לפספס פרצי הקצאה קצרים (כמו אימות TLS שלNetworkStackבפורטל שבוי ובדיקות DNS בו-זמניות, או אתחול שלBluetoothA2DP ו-BLE). בשירותים עם עומסים פתאומיים, התקציבים הותאמו כדי לכסות את האחוזון ה-90 עד ה-95 של שיאי השימוש ב-memory.peakבליבה (kernel) לאורך כל חיי השירות, בכל רמות ה-RAM. - מוודאים שרמת ה-RAM משקפת את ה-RAM ולא את דור המכשיר: במכשירים חדשים יותר יש בדרך כלל יותר RAM ויותר תכונות, כך שקבוצת עבודה שגדלה עם רמת ה-RAM עשויה לגדול בפועל עם התכונות. לפני שמוסיפים רמה של
maxDeviceRamMb, צריך לפלח את הנתונים לפי דגם המכשיר. לדוגמה,com.android.phoneמגיע לאותו P99anon + swap(כ-85 עד 90 MB) בטלפונים של 12 GB ו-16 GB מאותו דור, בעוד שבמכשיר מתקפל ישן יותר של 12 GB נעשה שימוש ב-40 MB. לכן, התקציב שלו משתמש בערך יחיד ללא רמת RAM, כדי שמכשיר עם RAM נמוך יותר שכולל את תכונות הטלפוניה החדשות יותר לא יקבל תקציב נמוך מדי.
איור 5. קבוצת עבודה לעומת מטמון קבצים שניתן להחזיר.
מסעות לקוח קריטיים (CUJ) במכשירים פיזיים וסריקות של קנה מידה לתצוגה
כדי לאמת את התקציבים שנגזרו מהטלמטריה ולכייל תקציבים פרופורציונליים לתצוגה (כמו :PhotoPicker) ותקציבים ברקע שספציפיים למצב (כמו Traceur), בוצעו סריקות memory.high בזמן אמת במכשירים פיזיים עם זיכרון RAM בנפח של 6GB עד 16GB:
- מציאת נקודת הברך של אפס תקלות חוזרות: בזמן הרצת CUJ מקצה לקצה (כמו
העברה מהירה של רשת תמונות, דפדוף באלבומים, החלקה של תצוגה מקדימה של תמונות וסרטונים ברזולוציה גבוהה במסך מלא, והקלטה ושמירה של מעקב),
memory.highהופחת בזמן מדידת המונים של cgroupmemory.stat(workingset_refault_anon,workingset_refault_file,pgscanו-pgsteal) ו-memory.events(high). כפי שמוצג באיור 6, התקציב האופטימלי נמצא ממש מעל נקודת הברך של אפס תקלות חוזרות. הנקודה שבה אין שגיאות דף חוזרות היא הסף שבו הליבה מחזירה את המטמון של קובץ קר חד-פעמי (pgsteal > 0) בלי להפעיל את התנודות של קבוצת העבודה הפעילה (workingset_refault == 0). - אימות קנה המידה של פיקסלים בתצוגה ברזולוציות שונות: בדיקת
:PhotoPickerבארבע רזולוציות תצוגה פיזיות ורזולוציות תצוגה באמולטור, מ-~1.2 MP (720×1600) ועד ~4.5 MP (2076×2152 במסך פנימי פתוח), אישרה שקנה המידה של קבוצת העבודה הפעילה של מפות הסיביות ב-Glide וב-ViewPager2 משתנה באופן ליניארי בהתאם למספר הפיקסלים בתצוגה. גם ערך הבסיס של הזיכרון שלא קשור לתצוגה משתנה בהתאם למכשיר: עם מספר פיקסלים כמעט זהה, ערך ה-knee של zero-refault היה כ-140MB במסך החיצוני של מכשיר מתקפל עם 16GB, לעומת 110MB עד 125MB בטלפון עם 6GB. ערך בסיס של 85MB בתוספת 25 בייטים לכל פיקסל במסך (android:maxMb="85"ו-android:additionalBytesPerDisplayPixel="25") נשאר מעל לנקודת הברך שנמדדה בכל מסך שנבדק (כ-110MB ב-HD+, כ-145MB ב-FHD+ וכ-190MB במסכים פתוחים). - אימות של איסוף אשפה ב-ART במצבי רקע: ב-cgroup v2, דפי תמונות של
boot.artושל מסגרת שמשותפים ל-Zygote נשארים ב-cgroup של Zygote, ונחשבים למרחב חסין במהלך איסוף אשפה של דחיסת סימון (CMC) בו-זמנית ב-ART. בדיקות עומסים אישרו שכאשר אפליקציה של ממשק משתמש של המערכת עוברת ל-background, הידוק שלmemory.high(לדוגמה, ל-30MB ב-Traceur) מגביל באופן נקי את המטמון הפרטי של הערימה והדפים מבלי לגרום לפעילות של החלפה או של איסוף זבל (GC).
איור 6. מציאת נקודת הברך של אפס תקלות חוזרות במהלך מסע משתמש קריטי (CUJ).
סיכום של תקציבי אפליקציות המערכת של AOSP
בטבלה הבאה מפורטים תקציבי הייחוס המכוילים ב-AOSP לפי רמות RAM ומצבי תהליכים:
| חבילה או תהליך של המערכת | היקף | RAM בנפח 8GB ומטה | RAM בנפח 12GB | RAM בנפח 16GB ומעלה | מנהל התקן ראשי לכיול |
|---|---|---|---|---|---|
com.android.bluetooth |
מזהה משתמש (<application>) |
80MB | 95MB | 110 MB | מכסה פרצי memory.peak לכל משך החיים של P90 (כ-75 עד 110MB) ו->2x memory.current פעיל של P99. |
com.android.nfc |
מזהה משתמש (<application>) |
50MB | 60MB | 60MB | כולל Host Card Emulation פעיל באחוזון 99 (עד כ-50MB) ובאחוזונים 90 עד 95 של memory.peak, תוך צמצום מטמון הקבצים הקרים. |
com.android.providers.calendar |
מזהה משתמש (<application>) |
70MB כברירת מחדל (חזית) 50MB perceptible ו-background |
קבוצת העבודה הפעילה לא משתנה בין רמות ה-RAM. תקציב הרקע שומר על מרווח אנונימי להתראות תזכורת תוך כדי חיתוך מטמון הדפים של SQLite במהלך סנכרונים. | ||
com.android.traceur |
מזהה משתמש (<application>) |
85MB כברירת מחדל (חזית וניתן לתפיסה) 30MB background |
שומר על מרווח מלא של 85MB במהלך הקלטה וייצוא של מעקב פעיל (TraceService), ומגביל את מצב חוסר הפעילות והשידור ל-30MB. |
||
com.android.providers.media.module (ראשי) |
PID (<process>) |
60MB | 75MB | 85MB | מספק מרווח של כ-10 עד 30 MB מעל P99 anon + swap תוך כדי החזרת מטמון קובץ SQLite קר ומטמון קובץ תמונה ממוזערת. |
com.android.providers.media.module:PhotoPicker |
PID (<process>) |
85MB + 25 בייטים לכל פיקסל (כ-110MB ב-HD+, כ-145MB ב-FHD+, כ-190MB ב-Fold של כ-4.5MP) |
הגודל משתנה באופן פרופורציונלי למספר הפיקסלים במסך (additionalBytesPerDisplayPixel="25") כדי להתאים ל-Glide bitmap working-set knee. |
||
com.android.networkstack |
מזהה משתמש (<application>) |
65 MB | 70MB | 85MB | זיכרון אנונימי בעיקר, בגודל של משך חיים P95 memory.peak כדי לכסות פרצי TLS, DNS ו-DHCP של פורטל שבוי. |
com.android.appsearch.apk |
מזהה משתמש (<application>) |
130MB | כולל כ-100MB של זיכרון נעול שלא ניתן לפינוי, וגם קבוצת עבודה של מארח חם בנפח של כ-5MB, עם מרווח לצמיחה של האזור הנעול. נמדדו אפס שגיאות דף חוזרות ב-≥125 MB בזמן שמאוחזר מטמון דפים קר (עד כ-330 MB ב-P99). | ||
com.android.phone (TeleService) |
PID (<process>) |
120MB | היקף לכל PID במזהה משתמש משותף 1001. קבוצת העבודה עוקבת אחרי דור המכשיר ולא אחרי ה-RAM, כך שתקציב אחד הוא בערך 30MB מעל ה-P99 הגבוה ביותר לכל דגם anon + swap. |
||
איך יצרני ציוד מקורי (OEM) יכולים לכייל מחדש את התקציבים של אפליקציות המערכת במכשירים שלהם
יצרני ציוד מקורי (OEM) שמתאימים אישית אפליקציות מערכת (לדוגמה, מוסיפים פרופילי Bluetooth של ספקים, תוספי טלפוניה בהתאמה אישית או כלי הוספה לאינדקס של מדיה של OEM) או שולחים שירותי מערכת נוספים שמותקנים מראש, יכולים לכייל מחדש את התקציבים הקיימים או ליצור קובצי *-budget.xml חדשים לתיק החומרה שלהם.
איסוף פרופילים פעילים של זיכרון cgroup v2
לכל תהליך במערכת היעד, קוראים את צומתי הזיכרון של cgroup v2 בקטע /sys/fs/cgroup/uid_<uid>/pid_<pid>/ בעומסי עבודה מייצגים וברמות RAM:
-
memory.current: הזיכרון הפיזי הנוכחי שמוקצה ל-cgroup (לא כולל דפים שמשותפים עם Zygote) -
memory.peak: השיא שלmemory.currentבכל משך החיים מאז תחילת התהליך -
memory.swap.current: השימוש הנוכחי בדחיסת זיכרון וירטואלי ב-ZRAM -
memory.stat: פירוט שלanon,file,pgscan,pgsteal,workingset_refault_anonו-workingset_refault_file -
memory.events: מספר מצטבר של הפרות שלhigh(אירועי ויסות והחזרה)memory.high
כשצוברים נתוני טלמטריה ממכשירים לבדיקה פנימית או ממכשירים בשטח, צריך להפריד בין מצבי תהליכים פעילים (TOP, FOREGROUND_SERVICE, קישורים פעילים וסנכרונים פעילים) לבין מצבי CACHED או PERSISTENT לא פעילים, כדי שתמונות מצב לא פעילות לא יטו כלפי מטה את החישובים של קבוצת העבודה P95 ו-P99.
הפרדת ה-heap שלא ניתן לדחוס מהמטמון של הקבצים שניתן להחזיר
השוואה בין האחוזון ה-99 של anon + swap לאחוזון ה-99 של memory.current:
- שירותים שבהם המטמון של הקבצים דומיננטי (
file≫anon, כמו ספקי מסדי נתונים, שירותי הוספה ל<b>אינדקס החיפוש</b> ודמונים של יומנים): מגדירים אתandroid:maxMbכך שיספק מרווח נוח (בדרך כלל פי 1.5 עד פי 4) מעל P99anon + swap, וגם חלון עבודה לדפי קבצים חמים. ההגדרה הזו מאפשרת לליבה לפנות באופן רציף דפי קבצים לא ממופים שלא בשימוש, בלי לגעת בערימה האנונימית. אםunevictableאוfile_mappedגדולים (כמו בזיכרון נעול או במסדי נתונים שממופים לזיכרון), צריך לספור גם את טביעת הרגל הזו כקבוצת עבודה. - שירותים שמתאפיינים בערימה אנונימית או בפרצי פעילות (
anonמהווה את רובmemory.current, כמו מחסניות של רשתות או קישוריות): מגדירים את הגודל שלandroid:maxMbכך שיכסה את האחוזונים P90 עד P95 שלmemory.peak, כדי שפרצי פעילות זמניים לא יפעילו החלפה סינכרונית של ZRAM בשרשורים שרגישים לזמן אחזור. - תהליכי ממשק משתמש שדורשים הרבה משאבי תצוגה (כמו בוררים, גלריות או רכיבי עזר של ממשק המשתמש של המערכת): מודדים את טביעת הרגל הבסיסית שלא קשורה לממשק המשתמש ואת טביעת הרגל המצטברת של מפת הסיביות לכל מגה-פיקסל בתצוגות ברזולוציה הנמוכה והגבוהה ביותר, ומבטאים את התקציב באמצעות
android:maxMbו-android:additionalBytesPerDisplayPixel.
ניקוי memory.high במכשירים פיזיים
לפני שמקצים תקציב חדש ל-build, משתמשים ב-am memory-limiter manual (או כותבים ישירות לצומת cgroup של memory.high בתהליך ב-build של userdebug) כדי לבדוק את המגבלות האפשריות בזמן שמריצים את הבדיקות האוטומטיות של CUJ:
# Override memory.high for a specific PID to 60 MB during a CUJ test
adb shell am memory-limiter manual <PID> 60MBעוקבים אחרי memory.stat ו-memory.events לפני ואחרי ה-CUJ:
- התקציב נמוך מדי:
workingset_refault_anonאוworkingset_refault_fileעולה בחדות במהלך ה-CUJ, memory.swap.currentגדל באופן רציף במהלך עבודה פעילה בחזית, או שיש נסיגה ב-CUJ frame או ב-Binder latency. מגדילים את התקציב מעבר לנקודת הברך שמוגדרת כברירת מחדל. - התקציב גבוה מדי: הערך של
memory.events:highנשאר0גם אחרי עומסי עבודה כבדים של סטרימינג או הוספה לאינדקס של קבצים, והערך שלfileב-memory.statגדל ללא הגבלה עם דפים קרים. הקטינו את התקציב של קבוצת המודעות הפעילה. - התקציב מכויל היטב: הערך של
memory.currentבמצב יציב נשאר מתחת ל-memory.highבמהלך פעולה רגילה. במהלך פרצי קבצים או מטמון כבדים,pgstealמחזיר דפי קבצים קרים בזמן ש-workingset_refault_anonנשאר קרוב לאפס וההשהיה של CUJ לא משתנה.