אופטימיזציה של זמני האתחול

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

ב-Android 8.0 יש תמיכה בכמה שיפורים במגוון רכיבים, שמאפשרים לקצר את זמני האתחול. בטבלה הבאה מפורט סיכום של שיפורי הביצועים האלה (כפי שנמדדו במכשירי Google Pixel ו-Pixel XL).

רכיב שיפור
תוכנת אתחול
  • חיסכון של 1.6 שניות על ידי הסרת יומן UART
  • חיסכון של 0.4 שניות על ידי מעבר מ-GZIP ל-LZ4
הליבה של המכשיר
  • חיסכון של 0.3 שניות על ידי הסרת הגדרות ליבה שלא בשימוש והקטנת גודל מנהל ההתקן
  • חיסכון של 0.3 שניות באמצעות אופטימיזציה של אחסון מראש ב-dm-verity
  • חיסכון של 0.15 שניות על ידי הסרת המתנה/בדיקה מיותרת ב-driver
  • חסכו 0.12 שניות בהסרת CONFIG_CC_OPTIMIZE_FOR_SIZE
כוונון קלט/פלט
  • חיסכון של 2 שניות בהפעלה רגילה
  • חיסכון של 25 שניות בהפעלה הראשונה
init.*.rc
  • חיסכון של 1.5 שניות על ידי הפעלה במקביל של פקודות init
  • חיסכון של 0.25 שניות על ידי הפעלה מוקדמת של zygote
  • חיסכון של 0.22 שניות על ידי שינוי ההגדרות של cpuset
אנימציית אתחול
  • התחיל 2 שניות מוקדם יותר בהפעלה ללא הפעלת fsck, הרבה יותר גדול בהפעלה עם הפעלת fsck
  • חיסכון של 5 שניות ב-Pixel XL באמצעות כיבוי מיידי של אנימציית האתחול
מדיניות SELinux חיסכון של 0.2 שניות על ידי genfscon

אופטימיזציה של תוכנת האתחול

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

  • ביומן:
    • משביתים את כתיבת היומן ב-UART, כי זה יכול לקחת הרבה זמן אם יש הרבה רישום ביומן. (במכשירי Google Pixel, גילינו שהיא מאטה את תוכנת האתחול ב-1.5 שניות).
    • כדאי לתעד ביומן רק מצבים של שגיאות, ולשקול לאחסן מידע אחר בזיכרון באמצעות מנגנון נפרד לאחזור.
  • לדחיסה של הליבה, כדאי להשתמש ב-LZ4 לחומרה מודרנית במקום ב-GZIP (תיקון לדוגמה). חשוב לזכור שלאפשרויות שונות לדחיסת הליבה יכולים להיות זמני טעינה ודחיסה שונים, ואפשרויות מסוימות עשויות לפעול טוב יותר מאחרות לחומרה הספציפית שלכם.
  • בודקים את זמני ההמתנה המיותרים לביטול הרטט או לכניסה למצב מיוחד ומצמצמים אותם.
  • העברת זמן האתחול שחלף ב-bootloader לליבת הליבה כ-cmdline.
  • כדאי לבדוק את שעון המעבד ולשקול ביצוע פעולות במקביל (נדרשת תמיכה במספר ליבות) לטעינת הליבה ולהפעלת יציאה/קלט.

אופטימיזציה של יעילות הקלט/פלט

שיפור היעילות של הקלט/פלט חיוני כדי לקצר את זמן האתחול, ויש לדחות את הקריאה של כל מה שלא הכרחי עד אחרי האתחול (ב-Google Pixel, קוראים כ-1.2GB של נתונים בזמן האתחול).

כוונון מערכת הקבצים

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

במכשירים שתומכים בעדכונים חלקים (A/B), כדאי מאוד לבצע התאמה של מערכת הקבצים בהפעלה הראשונה (למשל, 20 שניות ב-Google Pixel). לדוגמה, שינינו את הפרמטרים הבאים עבור Google Pixel:

on late-fs
  # boot time fs tune
    # boot time fs tune
    write /sys/block/sda/queue/iostats 0
    write /sys/block/sda/queue/scheduler cfq
    write /sys/block/sda/queue/iosched/slice_idle 0
    write /sys/block/sda/queue/read_ahead_kb 2048
    write /sys/block/sda/queue/nr_requests 256
    write /sys/block/dm-0/queue/read_ahead_kb 2048
    write /sys/block/dm-1/queue/read_ahead_kb 2048

on property:sys.boot_completed=1
    # end boot time fs tune
    write /sys/block/sda/queue/read_ahead_kb 512
    ...

שונות

  • מפעילים את גודל האחזור מראש של גיבוב dm-verity באמצעות הגדרת הליבה DM_VERITY_HASH_PREFETCH_MIN_SIZE (הגודל שמוגדר כברירת מחדל הוא 128).
  • כדי לשפר את היציבות של מערכת הקבצים ולבטל את הבדיקה הכפויה שמתבצעת בכל הפעלה, צריך להשתמש בכלי החדש ליצירת ext4. לשם כך, מגדירים את TARGET_USES_MKE2FS בקובץ BoardConfig.mk.

ניתוח של קלט/פלט

כדי להבין את פעילויות הקלט/פלט במהלך האתחול, משתמשים בנתוני ftrace של הליבה (שמשמש גם את systrace):

trace_event=block,ext4 in BOARD_KERNEL_CMDLINE

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

diff --git a/fs/open.c b/fs/open.c
index 1651f35..a808093 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -981,6 +981,25 @@
 }
 EXPORT_SYMBOL(file_open_root);
 
+static void _trace_do_sys_open(struct file *filp, int flags, int mode, long fd)
+{
+       char *buf;
+       char *fname;
+
+       buf = kzalloc(PAGE_SIZE, GFP_KERNEL);
+       if (!buf)
+               return;
+       fname = d_path(&filp-<f_path, buf, PAGE_SIZE);
+
+       if (IS_ERR(fname))
+               goto out;
+
+       trace_printk("%s: open(\"%s\", %d, %d) fd = %ld, inode = %ld\n",
+                     current-<comm, fname, flags, mode, fd, filp-<f_inode-<i_ino);
+out:
+       kfree(buf);
+}
+
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
 {
 	struct open_flags op;
@@ -1003,6 +1022,7 @@
 		} else {
 			fsnotify_open(f);
 			fd_install(fd, f);
+			_trace_do_sys_open(f, flags, mode, fd);

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

  • system/extras/boottime_tools/bootanalyze/bootanalyze.py מדידת זמן האתחול עם פירוט של השלבים החשובים בתהליך האתחול.
  • system/extras/boottime_tools/io_analysis/check_file_read.py boot_trace מספק את פרטי הגישה לכל קובץ.
  • system/extras/boottime_tools/io_analysis/check_io_trace_all.py boot_trace מציג פירוט ברמת המערכת.

אופטימיזציה של init.*.rc

Init הוא הגשר מהליבה עד להקמת המסגרת, ובדרך כלל התקנים נמצאים בשלבים שונים של init למשך כמה שניות.

הרצת משימות במקביל

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

  • ביצוע פקודות איטיות בשירות של סקריפט מעטפת, והצטרפות אליו מאוחר יותר באמצעות המתנה למאפיין ספציפי. ב-Android 8.0 יש תמיכה בתרחיש לדוגמה הזה באמצעות הפקודה החדשה wait_for_property.
  • זיהוי פעולות איטיות ב-init. המערכת מתעדת ביומן את הפקודה init exec/wait_for_prop או כל פעולה שנמשכת זמן רב (ב-Android 8.0, כל פקודה שנמשכת יותר מ-50 אלפיות השנייה). לדוגמה:
    init: Command 'wait_for_coldboot_done' action=wait_for_coldboot_done returned 0 took 585.012ms

    עיון ביומן הזה עשוי להצביע על הזדמנויות לשיפורים.

  • כדאי להפעיל שירותים ולהפעיל מכשירים היקפיים בנתיב קריטי מוקדם ככל האפשר. לדוגמה, חלק מ-SOCs דורשים להפעיל שירותים שקשורים לאבטחה לפני שמפעילים את SurfaceFlinger. בודקים את יומן המערכת כש-ServiceManager מחזיר את הערך 'wait for service'. בדרך כלל זה סימן שצריך להפעיל קודם שירות תלוי.
  • מסירים את כל הפקודות והשירותים שלא בשימוש בקובץ init.*.rc. כל מה שלא נעשה בו שימוש בשלבים המוקדמים של ה-init צריך להידחות עד להשלמת האתחול.

הערה: שירות הנכסים הוא חלק מתהליך ה-init, כך שקריאה ל-setproperty במהלך האתחול עלולה לגרום לעיכוב ארוך אם ה-init עסוק בפקודות מובנות.

שימוש בהתאמה של מתזמן הבקשות

שימוש בהתאמה של מתזמן לטעינה מוקדמת. דוגמה מ-Google Pixel:

on init
    # boottime stune
    write /dev/stune/schedtune.prefer_idle 1
    write /dev/stune/schedtune.boost 100
    on property:sys.boot_completed=1
    # reset stune
    write /dev/stune/schedtune.prefer_idle 0
    write /dev/stune/schedtune.boost 0

    # or just disable EAS during boot
    on init
    write /sys/kernel/debug/sched_features NO_ENERGY_AWARE
    on property:sys.boot_completed=1
    write /sys/kernel/debug/sched_features ENERGY_AWARE

יכול להיות שחלק מהשירותים יצטרכו לקבל עדיפות גבוהה יותר במהלך האתחול. דוגמה:

init.zygote64.rc:
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
    class main
    priority -20
    user root
...

התחלת zygote מוקדם

במכשירים עם הצפנה מבוססת-קובץ, אפשר להפעיל את zygote מוקדם יותר, בטריגר zygote-start (ברירת המחדל היא הפעלת zygote ב-class main, הרבה מאוחר יותר מ-zygote-start). כשעושים זאת, חשוב לאפשר ל-zygote לפעול בכל מעבדי ה-CPU (כי הגדרה שגויה של cpuset עשויה לאלץ את zygote לפעול במעבדי CPU ספציפיים).

השבתת החיסכון באנרגיה

במהלך האתחול של המכשיר, אפשר להשבית את ההגדרה של חיסכון בצריכת חשמל לרכיבים כמו UFS ו/או ל-CPU.

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

on init
    # Disable UFS powersaving
    write /sys/devices/soc/${ro.boot.bootdevice}/clkscale_enable 0
    write /sys/devices/soc/${ro.boot.bootdevice}/clkgate_enable 0
    write /sys/devices/soc/${ro.boot.bootdevice}/hibern8_on_idle_enable 0
    write /sys/module/lpm_levels/parameters/sleep_disabled Y
on property:sys.boot_completed=1
    # Enable UFS powersaving
    write /sys/devices/soc/${ro.boot.bootdevice}/clkscale_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/clkgate_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/hibern8_on_idle_enable 1
    write /sys/module/lpm_levels/parameters/sleep_disabled N
on charger
    # Enable UFS powersaving
    write /sys/devices/soc/${ro.boot.bootdevice}/clkscale_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/clkgate_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/hibern8_on_idle_enable 1
    write /sys/class/typec/port0/port_type sink
    write /sys/module/lpm_levels/parameters/sleep_disabled N

דחיית אתחול לא קריטי

אפשר לדחות את השלבים הלא קריטיים של האתחול, כמו ZRAM, ל-boot_complete.

on property:sys.boot_completed=1
   # Enable ZRAM on boot_complete
   swapon_all /vendor/etc/fstab.${ro.hardware}

אופטימיזציה של אנימציית האתחול

הטיפים הבאים יעזרו לכם לבצע אופטימיזציה של אנימציית האתחול.

הגדרת התחלה מוקדמת

ב-Android 8.0 אפשר להפעיל את אנימציית האתחול מוקדם, לפני שמרכבים את המחיצה של נתוני המשתמש. עם זאת, גם כשמשתמשים בשרשרת הכלים החדשה של ext4 ב-Android 8.0, הפקודה fsck עדיין מופעלת מדי פעם מטעמי בטיחות, וכתוצאה מכך יש עיכוב בהפעלת השירות bootanimation.

כדי שהאנימציה של האתחול תתחיל מוקדם, צריך לפצל את הטעינה של fstab לשני שלבים:

  • בשלב מוקדם, צריך לטעון רק את המחיצות (כמו system/ ו-vendor/) שלא דורשות בדיקות ריצה, ואז להפעיל את שירותי אנימציית האתחול ואת יחסי התלות שלהם (כמו servicemanager ו-surfaceflinger).
  • בשלב השני, מחברים מחיצות (כמו data/) שדורשות בדיקות ריצה.

אנימציית האתחול תתחיל מהר יותר (ובזמן קבוע) ללא קשר ל-fsck.

סיום נקי

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

אופטימיזציה של SELinux

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

  • שימוש בביטויים רגולריים (regex) נקיים. ביטוי רגולרי שנוצר בצורה לא טובה יכול להוביל לעלויות רבות כשמתאימים את מדיניות SELinux ל-sys/devices ב-file_contexts. לדוגמה, ביטוי ה-regex /sys/devices/.*abc.*(/.*)? מאלץ בטעות סריקת כל תיקיות המשנה של /sys/devices שמכילות את "abc", ומאפשר התאמות גם ל-/sys/devices/abc וגם ל-/sys/devices/xyz/abc. שיפור הביטוי הרגולרי הזה ל-/sys/devices/[^/]*abc[^/]*(/.*)? יאפשר התאמה רק ל-/sys/devices/abc.
  • העברת תוויות אל genfscon. התכונה הקיימת הזו של SELinux מעבירה ליבות תואמות לקובץ לליבה בקבצים הבינאריים של SELinux, שבהם הליבה מחילה אותן על מערכות קבצים שנוצרו על ידי הליבה. כך אפשר גם לתקן קבצים שנוצרו בליבה עם תוויות שגויות, וכך למנוע תנאי מרוץ שיכולים להתרחש בין תהליכים במרחב המשתמש שמנסים לגשת לקבצים האלה לפני שמתבצעת הוספת התווית מחדש.

כלים ושיטות

אתם יכולים להשתמש בכלים הבאים כדי לאסוף נתונים ליעדים של האופטימיזציה.

Bootchart

Bootchart מספק פירוט של עומסי המעבד (CPU) והקלט/פלט (I/O) של כל התהליכים במערכת כולה. לא צריך לבנות מחדש את קובץ האימג' של המערכת, וניתן להשתמש בו לבדיקת תקינות מהירה לפני שמתעמקים ב-systrace.

כדי להפעיל את bootchart:

adb shell 'touch /data/bootchart/enabled'
adb reboot

אחרי האתחול, מאחזרים את תרשים האתחול:

$ANDROID_BUILD_TOP/system/core/init/grab-bootchart.sh

בסיום, מוחקים את /data/bootchart/enabled כדי למנוע את איסוף הנתונים בכל פעם.

אם הכלי bootchart לא פועל ומופיעה הודעת שגיאה על כך שהקובץ bootchart.png לא קיים, מבצעים את הפעולות הבאות:
  1. מריצים את הפקודות הבאות:
          sudo apt install python-is-python3
          cd ~/Documents
          git clone https://github.com/xrmx/bootchart.git
          cd bootchart/pybootchartgui
          mv main.py.in main.py
        
  2. מעדכנים את $ANDROID_BUILD_TOP/system/core/init/grab-bootchart.sh כך שיצביע על העותק המקומי של pybootchartgui (שנמצא ב-~/Documents/bootchart/pybootchartgui.py).

Systrace

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

כדי להפעיל את systrace במהלך האתחול:

  • בקובץ frameworks/native/cmds/atrace/atrace.rc, משנים את הפרטים הבאים:
      write /sys/kernel/debug/tracing/tracing_on 0
      write /sys/kernel/tracing/tracing_on 0

    אל:

      #    write /sys/kernel/debug/tracing/tracing_on 0
      #    write /sys/kernel/tracing/tracing_on 0
  • הפעולה הזו מפעילה את המעקב (שהוא מושבת כברירת מחדל).

  • בקובץ device.mk, מוסיפים את השורה הבאה:
    PRODUCT_PROPERTY_OVERRIDES +=    debug.atrace.tags.enableflags=802922
    PRODUCT_PROPERTY_OVERRIDES +=    persist.traced.enable=0
  • בקובץ BoardConfig.mk של המכשיר, מוסיפים את הפרטים הבאים:
    BOARD_KERNEL_CMDLINE := ... trace_buf_size=64M trace_event=sched_wakeup,sched_switch,sched_blocked_reason,sched_cpu_hotplug
  • לניתוח מפורט של קלט/פלט, מוסיפים גם את block ו-ext4 ו-f2fs.

  • בקובץ init.rc הספציפי למכשיר, מוסיפים את הפרטים הבאים:
    on property:sys.boot_completed=1          // This stops tracing on boot complete
    write /d/tracing/tracing_on 0
    write /d/tracing/events/ext4/enable 0
    write /d/tracing/events/f2fs/enable 0
    write /d/tracing/events/block/enable 0
  • אחרי ההפעלה, מאחזרים את המעקב:

    adb root && adb shell atrace --async_stop -z -c -o /data/local/tmp/boot_trace
    adb pull /data/local/tmp/boot_trace
    $ANDROID_BUILD_TOP/external/chromium-trace/systrace.py --from-file=boot_trace