ניתוח באמצעות רינדור GPU בפרופיל

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

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

ייצוג חזותי

בכלי 'עיבוד פרופיל ב-GPU' מוצגים השלבים והזמנים היחסיים שלהם בצורת תרשים: היסטוגרמה עם צבעים. איור 1 מציג דוגמה לתצוגה כזו.

איור 1. תרשים של עיבוד פרופיל ב-GPU

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

איור 2. מקרא לתרשים לעיבוד של GPU של הפרופיל

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

השלבים והמשמעות שלהם

בקטע הזה נסביר מה קורה בכל שלב שתואם לצבע באיור 2, וגם נסביר על הגורמים לצווארי בקבוק שצריך לשים לב אליהם.

טיפול בקלט

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

כשהפלח הזה גדול

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

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

אנימציה

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

כשהפלח הזה גדול

בדרך כלל, ערכים גבוהים באזור הזה הם תוצאה של עבודה שמתבצעת עקב שינוי כלשהו בנכס של האנימציה. לדוגמה, אנימציה של תנועה אופקית, שגוללת ב-ListView או ב-RecyclerView, גורמת לכמות גדולה של אינפלציה ואוכלוסיה.

מדידה/פריסה

כדי שמערכת Android תציג את פריטי התצוגה במסך, היא מבצעת שתי פעולות ספציפיות בפריסות ובתצוגות בהיררכיית התצוגה.

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

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

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

כשהפלח הזה גדול

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

גם קוד שהוספתם ל-onLayout(boolean, int, int, int, int) או ל-onMeasure(int, int) עלול לגרום לבעיות בביצועים. בעזרת Traceview ו-Systrace תוכלו לבדוק את סטאקי הקריאות כדי לזהות בעיות בקוד.

ציור

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

בסרגל הציור מתועד הזמן שנדרש כדי להשלים את הצילום של הפקודות ברשימה של התצוגה, לכל התצוגות שצריך לעדכן במסך באותו פריים. זמן המדידה חל על כל קוד שמוסיפים לאובייקטים של ממשק המשתמש באפליקציה. דוגמאות לקוד כזה יכולות להיות onDraw(), dispatchDraw() וה-draw ()methods השונים ששייכים למחלקות המשנה של המחלקה Drawable.

כשהפלח הזה גדול

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

סנכרון/העלאה

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

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

הערה: במכשירי Lollipop, השלב הזה הוא סגול.

כשהפלח הזה גדול

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

כדי לכווץ את הסרגל הזה, אפשר להשתמש בשיטות כמו:

  • חשוב לוודא שהרזולוציות של קובצי ה-bitmap לא גדולות בהרבה מהגודל שבו הן יוצגו. לדוגמה, האפליקציה צריכה להימנע מהצגת תמונה בגודל 1,024x1,024 כתמונה בגודל 48x48.
  • מנצלים את prepareToDraw() להעלאה אסינכרונית מראש של מפת סיביות לפני שלב הסנכרון הבא.

בעיות בפקודות

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

כדי שהמערכת תוכל לצייר את רשימות התצוגה במסך, היא שולחת את הפקודות הנדרשות ל-GPU. בדרך כלל, הפעולה הזו מתבצעת דרך API של OpenGL ES.

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

כשפלח גדול

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

Kotlin

for (i in 0 until 1000) {
    canvas.drawPoint()
}

Java

for (int i = 0; i < 1000; i++) {
    canvas.drawPoint()
}

הרבה יותר יקר להנפיק אותה מאשר:

Kotlin

canvas.drawPoints(thousandPointArray)

Java

canvas.drawPoints(thousandPointArray);

אין תמיד קשר של 1:1 בין ביצוע פקודות לבין ציור רשימות תצוגה בפועל. בניגוד למדד העברת פקודות, שמתעד את הזמן שנדרש לשליחת פקודות ציור ל-GPU, המדד ציור מייצג את הזמן שנדרש לתיעוד הפקודות שהועברו ברשימת התצוגה.

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

מאגרי נתונים להחלפה/לתהליכים

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

כשפלח גדול

חשוב להבין שה-GPU מבצע את העבודה במקביל למעבד (CPU). המערכת של Android יוצרת פקודות לצייר פקודות ל-GPU, ולאחר מכן עוברת למשימה הבאה. ה-GPU קורא את פקודות המשיכה מתור ומעבד אותן.

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

המפתח לצמצום הבעיה הזו הוא הפחתת המורכבות של העבודה שמתרחשת ב-GPU, בדומה למה שהייתם עושים בשלב 'Issue Commands'.

שונות

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

כשהפלח הזה גדול

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