תקציבי זיכרון של המערכת

ב-Android בגרסה 17 ואילך, הפלטפורמה מנהלת את הזיכרון של תהליכי המערכת באמצעות שני מנגנונים משלימים של Linux cgroup v2, שפועלים לצד מגביל הזיכרון הכללי של האפליקציה:

  • רצפות הגנה של daemons של המערכת (SystemMemoryBudgetManager עם memory.min): שמירות cgroup v2 memory.min בעלות כוונון עצמי (מוגדרות ל-90% מערך העבודה המיושב של כל daemon ומוגבלות באופן גלובלי ל-10% מסך ה-RAM הפיזי) מגנות על daemons אינטראקטיביים קריטיים ותהליכי מעטפת (system_server, surfaceflinger, הגרפיקה composer ​​HAL, audioserver, cameraserver, netd, SystemUI ומפעיל הבית) תחת לחץ זיכרון, ומחליפות הצמדת קבצים סטטית PinnerService mlock.
  • מגבלות הזיכרון של אפליקציות המערכת (<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, הגרפיקה composer HAL, 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:

  1. ‫צומת תהליך עלה (pid_<pid>/memory.min): מוגדר לערך המינימלי של 90% מקבוצת העבודה (בקנה מידה) של התהליך.
  2. צומת UID‏ (uid_<uid>/memory.min): מוגדר לסכום המדויק של ערכי הסף של העלים המנוהלים שלו ב-pid_*, כך שלתהליכים לא מנוהלים שמשתפים את ה-UID הזה (כמו חבילות sharedUserId="android.uid.system" או תהליכי daemon אחרים של AID_SYSTEM) יש memory.min = 0 והם לא יכולים לבקש הגנה.
  3. פרוסה ברמה העליונה (/sys/fs/cgroup/system/memory.min ו-/sys/fs/cgroup/apps/memory.min): מוגדרת לסכום המדויק של uid_* הצאצאים שלה. כל ערכי memory.low לא עדכניים בצמתים מנוהלים נמחקים ומוחלפים ב-0, וכל ספריות uid_* שנוהלו ברענון קודם (כמו מרכז אפליקציות קודם של דף הבית אחרי שהמשתמש החליף מרכזי אפליקציות) מאופסות ל-0.

היררכיית הגנה של memory.min ב-cgroup v2 בשלוש רמות ב-/sys/fs/cgroup/system וב-/sys/fs/cgroup/apps, שבה מוצגות רצפות של תהליכי עלה שמסוכמות לצומתי UID וצומתי פרוסות, בעוד שתהליכים אחים לא מוגנים נשארים באפס

איור 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 של הליבה (ולא רשימת קבצים סטטית) בוחר אילו דפי קוד חם, ערימה וגרפיקה יישארו קבועים.

השוואה של הזיכרון התושב של הדימון ככל שהלחץ על זיכרון המערכת עולה מ-40% ל-95% מזיכרון ה-RAM הפיזי, שמראה ש-memory.min נשאר ברצפה של 90% בזמן ש-memory.low קורס אחרי מעבר דחייה יחיד של MGLRU

איור 2. שמירה על קבוצת העבודה של הדימון (daemon) כשהעומס על הזיכרון עולה.

חימום מראש של האתחול ופינוי המטמון של קבצים לא ממופים אחרי האתחול

כדי להחליף את PinnerService הקבוע mlock של config_defaultPinnerServiceFiles, צריך לתאם שתי התנהגויות בזמן ההפעלה:

  • חימום מראש של הפעלה ברקע בפעם אחת (PinnerService): דילוג על PinnerService mlock בלי לחמם את מטמון הדפים מעביר כ-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 במקום לחזור לכתיבה לא מוסמכת של החזרה שתחליף את הערימה האנונימית במהלך ההפעלה.

ציר זמן של זיכרון system_server אחרי האתחול, שבו מוצגים רענוני ההתייצבות הגיאומטריים ב-1, 4, 16 ו-64 שניות, החרגה של מטמון סריקת החבילות באתחול שלא מופה מהרצפה של 90% memory.min, ו-memory.reclaim חד-פעמי עם swappiness=0

איור 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) ממפים קבצים גדולים בדיסק או מעבירים אותם בסטרימינג. בלי מגבלת cgroup memory.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 עם UID 1001 או android.uid.system עם UID 1000) עם חבילות מערכת אחרות, כדי שתהליכים אחרים שמשתפים את ה-UID הזה לא יוגבלו יחד, או כש-APK יחיד מריץ כמה תהליכים עם תפקידי זיכרון נפרדים (לדוגמה, ה-daemon של MediaProvider ברקע ותהליך המשנה של ממשק המשתמש של :PhotoPicker).

היררכיית Cgroup v2 שמשווה בין אכיפת memory.high ברמת ה-UID למזהי UID ייעודיים לבין אכיפת memory.high לכל PID למזהי UID משותפים של המערכת ולחבילות APK עם כמה תהליכים

איור 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.current P99 ל-anon + swap P99 מציין מטמון של דפי קבצים ש-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 בו-זמניות, או אתחול של Bluetooth A2DP ו-BLE). בשירותים עם עומסים פתאומיים, התקציבים הותאמו כדי לכסות את האחוזון ה-90 עד ה-95 של שיאי השימוש ב-memory.peak בליבה (kernel) לאורך כל חיי השירות, בכל רמות ה-RAM.
  • מוודאים שרמת ה-RAM משקפת את ה-RAM ולא את דור המכשיר: במכשירים חדשים יותר יש בדרך כלל יותר RAM ויותר תכונות, כך שקבוצת עבודה שגדלה עם רמת ה-RAM עשויה לגדול בפועל עם התכונות. לפני שמוסיפים רמה של maxDeviceRamMb, צריך לפלח את הנתונים לפי דגם המכשיר. לדוגמה, com.android.phone מגיע לאותו P99 anon + swap (כ-85 עד 90 MB) בטלפונים של 12 GB ו-16 GB מאותו דור, בעוד שבמכשיר מתקפל ישן יותר של 12 GB נעשה שימוש ב-40 MB. לכן, התקציב שלו משתמש בערך יחיד ללא רמת RAM, כדי שמכשיר עם RAM נמוך יותר שכולל את תכונות הטלפוניה החדשות יותר לא יקבל תקציב נמוך מדי.

פירוק של זיכרון cgroup לאורך זמן לסט עבודה אנונימי שלא ניתן לדחיסה, חלון מטמון קבצים פעיל ומטמון קבצים קר חד-פעמי שמוחזר מעבר ל-memory.high

איור 5. קבוצת עבודה לעומת מטמון קבצים שניתן להחזיר.

מסעות לקוח קריטיים (CUJ) במכשירים פיזיים וסריקות של קנה מידה לתצוגה

כדי לאמת את התקציבים שנגזרו מהטלמטריה ולכייל תקציבים פרופורציונליים לתצוגה (כמו :PhotoPicker) ותקציבים ברקע שספציפיים למצב (כמו Traceur), בוצעו סריקות memory.high בזמן אמת במכשירים פיזיים עם זיכרון RAM בנפח של 6GB עד 16GB:

  • מציאת נקודת הברך של אפס תקלות חוזרות: בזמן הרצת CUJ מקצה לקצה (כמו העברה מהירה של רשת תמונות, דפדוף באלבומים, החלקה של תצוגה מקדימה של תמונות וסרטונים ברזולוציה גבוהה במסך מלא, והקלטה ושמירה של מעקב), memory.high הופחת בזמן מדידת המונים של cgroup memory.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).

תרשים של החזרת מטמון קר (pgsteal) ושל תנודות בסט העבודה (workingset_refault) כשהערך של memory.high יורד במהלך CUJ, עם הדגשה של נקודת הברך של אפס תקלות והתקציב המיועד

איור 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 &gg; anon, כמו ספקי מסדי נתונים, שירותי הוספה ל<b>אינדקס החיפוש</b> ודמונים של יומנים): מגדירים את android:maxMb כך שיספק מרווח נוח (בדרך כלל פי 1.5 עד פי 4) מעל P99 anon + 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 לא משתנה.