אופטימיזציה של זמן ההפעלה

בדף הזה מפורטים טיפים לשיפור זמן האתחול.

הסרת סמלים של ניפוי באגים ממודולים

בדומה לאופן שבו מסירים את סמלי ניפוי הבאגים מהליבה במכשיר ייצור, חשוב להסיר גם את סמלי ניפוי הבאגים מהמודולים. הסרת סמלי ניפוי הבאגים מהמודולים עוזרת לקצר את זמן האתחול על ידי צמצום הגורמים הבאים:

  • הזמן שלוקח לקרוא את הקבצים הבינאריים מהפלאש.
  • הזמן שלוקח לדחוס את ה-ramdisk.
  • הזמן שנדרש לטעינת המודולים.

הסרת הסמל של ניפוי הבאגים מהמודולים עשויה לחסוך כמה שניות במהלך האתחול.

הסרת הסמלים מופעלת כברירת מחדל ב-build של פלטפורמת Android, אבל כדי להפעיל אותה באופן מפורש, צריך להגדיר את BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES בתצורה הספציפית למכשיר בקטע device/vendor/device.

שימוש בדחיסת LZ4 לליבת ליבה ול-ramdisk

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

נוספה תמיכה בדחיסת LZ4 של דיסק זיכרון (ramdisk) לגרסה היציבה של פלטפורמת Android דרך BOARD_RAMDISK_USE_LZ4. תוכלו להגדיר את האפשרות הזו בהגדרות הספציפיות למכשיר. אפשר להגדיר דחיסת ליבה דרך kernel defconfig.

המעבר ל-LZ4 אמור לספק זמן אתחול מהיר יותר מ-500 אלפיות השנייה ל-1,000 אלפיות השנייה.

נמנעים מהתחברות מוגזמת לנהגים

ב-ARM64 וב-ARM32, קריאות לפונקציות שנמצאות במרחק מסוים מאתר הקריאה צריכות טבלת קפיצה (שנקראת טבלת קישור של פרוצדורה, או PLT) כדי שאפשר יהיה לקודד את כתובת הקפיצה המלאה. מאחר שהמודולים נטענים באופן דינמי, צריך לתקן את טבלאות הקפיצה האלה במהלך טעינת המודול. הקריאות שצריך להעביר אותן נקראות רשומות העברה עם ערכים מפורשים (או RELA בקיצור) בפורמט ELF.

ליבה של Linux מבצעת אופטימיזציה של גודל הזיכרון (כמו אופטימיזציה של היטים במטמון) בזמן הקצאת ה-PLT. בעקבות ההתחייבות הזו, מורכבות תוכנית האופטימיזציה היא O(N^2), כאשר N הוא מספר ה-RELA מסוג R_AARCH64_JUMP26 או R_AARCH64_CALL26. לכן, כדאי להשתמש בפחות RELAs מהסוגים האלה כדי לקצר את זמן הטעינה של המודול.

דפוס תכנות נפוץ אחד שמגדיל את מספר ה-RELA של R_AARCH64_CALL26 או R_AARCH64_JUMP26 הוא רישום יתר ביומן ב-driver. בדרך כלל, כל קריאה ל-printk() או לכל סכימה אחרת של רישום ביומן מוסיפה רשומת RELA מסוג CALL26/JUMP26. בטקסט ההתחייבות בהתחייבות למקור, שימו לב שלמרות האופטימיזציה, הטעינה של ששת המודולים נמשכת כ-250 אלפיות השנייה. הסיבה לכך היא שששת המודולים האלה היו ששת המודולים המובילים עם כמות הנתונים הגדולה ביותר ביומן.

צמצום הרישום ביומן יכול לחסוך 100 עד 300 אלפיות השנייה בזמני האתחול, בהתאם לחומרת הרישום הקיים ביומן.

הפעלת בדיקה אסינכרונית באופן סלקטיבי

כשמודול נטען, אם המכשיר שהוא תומך בו כבר מאוכלס מ-DT (devicetree) ונוסף לליבת הנהג, בדיקת המכשיר מתבצעת בהקשר של הקריאה ל-module_init(). כשבודקים מכשיר בהקשר של module_init(), הטעינה של המודול לא יכולה להסתיים עד שהבדיקה מסתיימת. מכיוון שהטעינה של המודולים מתבצעת בעיקר בסדרה, מכשיר שדרוש לו זמן רב יחסית כדי לבדוק את המודולים מאט את זמן האתחול.

כדי למנוע זמני אתחול ארוכים יותר, מומלץ להפעיל בדיקה אסינכרונית של מודולים שדרוש להם זמן מה כדי לבדוק את המכשירים שלהם. הפעלת בדיקה אסינכרונית לכל המודולים עשויה להיות לא יעילה, כי הזמן שנדרש כדי ליצור שיטה חדשה (fork) של שרשור ולהפעיל את הבדיקה עשוי להיות זהה לזמן שנדרש כדי לבדוק את המכשיר.

מכשירים שמחוברים באמצעות אוטובוס איטי כמו I2C, מכשירים שמבצעים טעינת קושחת בפונקציית הבדיקה שלהם ומכשירים שמבצעים הרבה אתחול של חומרה עלולים לגרום לבעיית התזמון. הדרך הטובה ביותר לזהות מתי זה קורה היא לאסוף את זמן הבדיקה של כל מנהל התקן ולמיין אותו.

כדי להפעיל בדיקה אסינכרונית של מודול, לא מספיק להגדיר רק את הדגל PROBE_PREFER_ASYNCHRONOUS בקוד הנהג. במודולים, צריך גם להוסיף את module_name.async_probe=1 בשורת הפקודה של הליבה או להעביר את async_probe=1 כפרמטר של מודול בזמן טעינת המודול באמצעות modprobe או insmod.

הפעלת בדיקה אסינכרונית יכולה לחסוך בערך 100 עד 500 אלפיות השנייה בזמני האתחול, בהתאם לחומרה או לדרייברים שלכם.

בדיקה של מנהל ההתקן של CPUfreq מוקדם ככל האפשר

ככל שהבדיקות של מנהל ההתקן CPUfreq יתבצעו מוקדם יותר, כך תוכלו לשנות את תדר המעבד למקסימום (או למקסימום מוגבל תרמית) במהלך האתחול. ככל שהמעבד מהיר יותר, כך האתחול מהיר יותר. ההנחיה הזו חלה גם על מנהלי ההתקן של devfreq ששולטים בתדר של ה-DRAM, הזיכרון והחיבור המחבר.

במודולים, סדר העומסים יכול להיות תלוי ברמה initcall ולהדר (compile) את מנהלי ההתקנים או לקשר את הסדר שלהם. כדי לוודא שהדרייבר cpufreq נמצא בין המודולים הראשונים שנטענים, צריך להשתמש בכינוי MODULE_SOFTDEP().

חוץ מטעינה מוקדמת של המודול, צריך גם לוודא שכל יחסי התלות שבודקים את מנהל ההתקן של ה-CPUfreq נבדקו. לדוגמה, אם צריכים נקודת אחיזה של שעון או וסתגל כדי לשלוט בתדירות של המעבד (CPU), צריך לבדוק קודם כל אותם. לחלופין, יכול להיות שתצטרכו לטעון מנהלי התקנים תרמו-אלקטרוניים לפני מנהל ההתקן של CPUfreq, אם יש סיכוי שהמעבדים שלכם יתחממו יותר מדי במהלך האתחול. לכן, כדאי לעשות כמיטב יכולתכם כדי לוודא שהבדיקה של מנהלי ההתקנים CPUfreq ו-devfreq הרלוונטיים תתבצע מוקדם ככל האפשר.

החיסכון מבדיקה מוקדמת של מנהל ה-CPUfreq יכול להיות קטן מאוד או גדול מאוד, בהתאם למועד שבו אפשר לבצע את הבדיקה ובתדירות שבה מנהל האתחול משאיר את המעבדים במצב מופעל.

העברת מודולים למחיצה של שלב שני של init, של ספק או של vendor_dlkm

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

בשלב הראשון לא צריך לבדוק כמה מכשירים כדי לעבור לשלב השני. רק יכולות הקונסולה ואחסון ה-Flash נדרשות לתהליך הפעלה רגיל.

טוענים את מנהלי ההתקנים החיוניים הבאים:

  • watchdog
  • reset
  • cpufreq

בשלב הראשון של תהליך האיניטיאל במצב fastbootd של התאוששות ומרחב המשתמש, נדרשים יותר מכשירים לבדיקה (כמו USB) ותצוגה. שומרים עותק של המודולים האלה ב-ramdisk של השלב הראשון ובמחיצה של הספק או vendor_dlkm. כך הם יכולים להיטען באתחול בשלב הראשון לצורך שחזור או לתהליך ההפעלה של fastbootd. עם זאת, אין לטעון את המודולים של מצב השחזור בשלב הראשון של האינטואיציה במהלך תהליך האתחול הרגיל. אפשר לדחות את המודול של מצב השחזור להפעלה של השלב השני כדי לקצר את זמן האתחול. כל שאר המודולים שלא נדרשים בשלב הראשון של האינטליגנציה צריכים לעבור למחיצה של הספק או למחיצה vendor_dlkm.

כשנותנים ל-script dev needs.sh רשימה של מכשירי עלים (למשל UFS או Serial), הוא מוצא את כל מנהלי ההתקנים, המכשירים והמודולים הנדרשים לבדיקה של יחסי התלות או של הספקים (למשל שעונים, רגולטורים או gpio).

העברת מודולים לאתחול בשלב שני מקצרת את זמני האתחול בדרכים הבאות:

  • הקטנת הגודל של Ramdisk.
    • כך אפשר לקרוא מהפלאש מהר יותר כשמרכז האתחול טוען את ה-ramdisk (שלב האתחול בסדרה).
    • כך אפשר להגיע למהירויות דחיסה מהירות יותר כשהליבה מדחסת את דיסק ה-RAM (שלב האתחול בסדרה).
  • ההפעלה בשלב השני פועלת במקביל, כך שזמן הטעינה של המודול מוסתר והעבודה מתבצעת בהפעלה בשלב השני.

העברת מודולים לשלב השני יכולה לחסוך 500 עד 1,000 אלפיות שנייה בזמני האתחול, בהתאם למספר המודולים שאפשר להעביר ל-init בשלב השני.

הלוגיסטיקה של טעינת המודולים

ב-build העדכני ביותר של Android יש הגדרות של לוח הבקרה ששולטות במודולים שמעתיקים לכל שלב ובמודולים שמטעינים. הקטע הזה מתמקד בקבוצת המשנה הבאה:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. רשימת המודולים שרוצים להעתיק ל-ramdisk.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. רשימת המודולים שצריך לטעון בשלב הראשון של האינטליגנציה המלאכותית.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD. זוהי רשימת המודולים שצריך לטעון כשבוחרים באפשרות 'שחזור' או ב-fastbootd מה-ramdisk.
  • BOARD_VENDOR_KERNEL_MODULES. רשימת המודולים הזו תועתק למחיצה של הספק או של vendor_dlkm בספרייה /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. רשימת המודולים שצריך לטעון בשלב השני של האינטליגנציה המלאכותית.

צריך להעתיק גם את המודולים של האתחול והשחזור ב-ramdisk למחיצה של הספק או של vendor_dlkm‏ ב-/vendor/lib/modules. העתקת המודולים האלה למחיצה של הספק מבטיחה שהמודולים לא יהיו בלתי נראים במהלך ההפעלה של השלב השני, וזה שימושי לניפוי באגים ולאיסוף modinfo לדיווחים על באגים.

הכפילות אמורה לדרוש שטח מינימלי במחיצה של הספק או של vendor_dlkm, כל עוד קבוצת המודולים של האתחול מוקטנת למינימום. מוודאים שבקובץ modules.list של הספק יש רשימה מסוננת של מודולים ב-/vendor/lib/modules. הרשימה המסוננת מבטיחה שזמני האתחול לא יושפעו מהטעינה מחדש של המודולים (תהליך יקר).

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

אפשר להשתמש בקבצים Board.Config.mk של המכשיר כדי לבצע את הפעולות האלה, כפי שמתואר בדוגמה הבאה:

# All kernel modules
KERNEL_MODULES := $(wildcard $(KERNEL_MODULE_DIR)/*.ko)
KERNEL_MODULES_LOAD := $(strip $(shell cat $(KERNEL_MODULE_DIR)/modules.load)

# First stage ramdisk modules
BOOT_KERNEL_MODULES_FILTER := $(foreach m,$(BOOT_KERNEL_MODULES),%/$(m))

# Recovery ramdisk modules
RECOVERY_KERNEL_MODULES_FILTER := $(foreach m,$(RECOVERY_KERNEL_MODULES),%/$(m))
BOARD_VENDOR_RAMDISK_KERNEL_MODULES += \
     $(filter $(BOOT_KERNEL_MODULES_FILTER) \
                $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# ALL modules land in /vendor/lib/modules so they could be rmmod/insmod'd,
# and modules.list actually limits us to the ones we intend to load.
BOARD_VENDOR_KERNEL_MODULES := $(KERNEL_MODULES)
# To limit /vendor/lib/modules to just the ones loaded, use:
# BOARD_VENDOR_KERNEL_MODULES := $(filter-out \
#     $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# Group set of /vendor/lib/modules loading order to recovery modules first,
# then remainder, subtracting both recovery and boot modules which are loaded
# already.
BOARD_VENDOR_KERNEL_MODULES_LOAD := \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
        $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))
BOARD_VENDOR_KERNEL_MODULES_LOAD += \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER) \
            $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# NB: Load order governed by modules.load and not by $(BOOT_KERNEL_MODULES)
BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD := \
        $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# Group set of /vendor/lib/modules loading order to boot modules first,
# then the remainder of recovery modules.
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD := \
    $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD += \
    $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
    $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))

בדוגמה הזו מוצגת קבוצת משנה של BOOT_KERNEL_MODULES ו-RECOVERY_KERNEL_MODULES שקל יותר לנהל, וניתן לציין אותה באופן מקומי בקובצי התצורה של הלוח. הסקריפט הקודם מוצא וממלא כל אחד מהמודולים של קבוצת המשנה מתוך מודולי הליבה הזמינים שנבחרו, ומותיר את המודולים הנותרים ל-init בשלב השני.

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

אפשר להתעלם מכשל טעינה של מודול ניפוי באגים שלא קיים בגרסאות build של משתמשים. כדי להתעלם מהכשל הזה, מגדירים את המאפיין vendor.device.modules.ready כך שיפעיל שלבים מאוחרים יותר של תהליך האתחול של הסקריפטים של init rc כדי להמשיך למסך ההפעלה. עיינו בסקריפט הבא לדוגמה, אם יש לכם את הקוד הבא ב-/vendor/etc/init.insmod.sh:

#!/vendor/bin/sh
. . .
if [ $# -eq 1 ]; then
  cfg_file=$1
else
  # Set property even if there is no insmod config
  # to unblock early-boot trigger
  setprop vendor.common.modules.ready
  setprop vendor.device.modules.ready
  exit 1
fi

if [ -f $cfg_file ]; then
  while IFS="|" read -r action arg
  do
    case $action in
      "insmod") insmod $arg ;;
      "setprop") setprop $arg 1 ;;
      "enable") echo 1 > $arg ;;
      "modprobe") modprobe -a -d /vendor/lib/modules $arg ;;
     . . .
    esac
  done < $cfg_file
fi

בקובץ ה-rc של החומרה, אפשר לציין את השירות one shot באמצעות:

service insmod-sh /vendor/etc/init.insmod.sh /vendor/etc/init.insmod.<hw>.cfg
    class main
    user root
    group root system
    Disabled
    oneshot

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

כדי לשפר את זמני האתחול, אפשר לבחור מודולים ספציפיים בשירות הטעינה של המודולים שמתאימים יותר לטעינה אחרי מסך ההפעלה. לדוגמה, אפשר לטעון מאוחר באופן מפורש את המודולים של ממיר הווידאו או ה-Wi-Fi אחרי שהסתיים תהליך האתחול של init (למשל, sys.boot_complete, אות של נכס Android). חשוב לוודא שה-HALs של המודולים שמופעלים בשלב מאוחר יותר נחסמים למשך זמן מספיק כשמנגיני הליבה לא נמצאים.

לחלופין, אפשר להשתמש בפקודה wait<file>[<timeout>] של init בסקריפטים של תהליך האתחול כדי להמתין לרשאות sysfs נבחרות שיציגו שהמודולים של הנהגים השלימו את פעולות הבדיקה. דוגמה לכך היא המתנה לטעינת מנהל המסך ברקע של תהליך השחזור או של fastbootd, לפני הצגת הגרפיקה של התפריט.

אתחול תדירות המעבד (CPU) לערך סביר בתוכנת האתחול

יכול להיות שלא כל ה-SoCs/המוצרים יוכלו לאתחל את המעבד בתדירות הגבוהה ביותר בגלל בעיות חום או בעיות חשמל במהלך בדיקות לולאת אתחול. עם זאת, חשוב לוודא שה-bootloader מגדיר את התדירות של כל מעבדי ה-CPU במצב אונליין למהירות הגבוהה ביותר שאפשר ללא סיכון למוצר או ל-SoC. זה חשוב מאוד כי בליבה מודולרית לחלוטין, הדחיסה של init ramdisk מתרחשת לפני שאפשר לטעון את מנהל ה-CPUfreq. לכן, אם מעבד ה-CPU נשאר בקצה התחתון של התדירות שלו על ידי מנהל האתחול, זמן הדחיסה של ה-ramdisk עשוי להימשך זמן רב יותר מאשר ליבה שעבר הידור סטטי (אחרי התאמה להבדל בגודל ה-ramdisk), כי תדירות ה-CPU תהיה נמוכה מאוד כשמבצעים עבודה שמתבצעת בעיקר על ידי מעבד (דחיסה). אותו עיקרון חל על תדרי זיכרון וחיבור.

איפוס תדר המעבד של מעבדים גדולים בתוכנת האתחול

לפני טעינת מנהל ההתקן CPUfreq, הליבה לא מודעת לתדרי המעבד ולא משנה את קיבולת התזמון של המעבד בהתאם לתדר הנוכחי. הליבה עשויה להעביר את השרשור ל-CPU הגדול אם העומס ב-CPU הקטן גבוה מספיק.

חשוב לוודא שהמעבדים הגדולים מניבים ביצועים לפחות כמו המעבדים הקטנים בתדירות שבה מנהל האתחול משאיר אותם במצב פעיל. לדוגמה, אם הביצועים של המעבד הגדול טובים פי 2 מאלה של המעבד הקטן באותה תדר, אבל מנהל האתחול מגדיר את התדר של המעבד הקטן ל-1.5GHz ואת התדר של המעבד הגדול ל-300MHz, ביצועי האתחול ירדו אם הליבה תעביר שרשור למעבד הגדול. בדוגמה הזו, אם בטוח להפעיל את המעבד הגדול במהירות 750MHz, כדאי לעשות זאת גם אם לא מתכננים להשתמש בו באופן מפורש.

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

יכול להיות שיהיו מקרים בלתי נמנעים שבהם צריך לטעון את הקושחה בשלב הראשון של האינטליגנציה המלאכותית. אבל באופן כללי, מנהלי התקנים לא צריכים לטעון קושחת בשלב הראשון של ה-init, במיוחד בהקשר של בדיקת המכשיר. טעינת קושחה בשלב הראשון גורמת לתהליך האתחול כולו להיתקע אם הקושחה לא זמינה ב-ramdisk בשלב הראשון. גם אם הקושחה מופיעה ב-ramdisk בשלב הראשון, היא עדיין גורמת לעיכוב מיותר.