הגדרת מעקב המערכת

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

הגדרה של מעקב אחר עקבות המערכת מבוסס-משחק

הכלי Systrace זמין בשתי דרכים:

‫Systrace הוא כלי ברמה נמוכה ש:

  • מספק אמת קרקע (ground truth). ‫Systrace מתעד פלט ישירות מהליבה, כך שהמדדים שהוא מתעד כמעט זהים למדדים שסדרה של קריאות למערכת הייתה מדווחת עליהם.
  • צריכת משאבים נמוכה. התקורה של Systrace במכשיר נמוכה מאוד, בדרך כלל פחות מ-1%, כי הנתונים מוזרמים למאגר בזיכרון.

הגדרות אופטימליות

חשוב להזין לכלי קבוצה סבירה של ארגומנטים:

  • קטגוריות: הקטגוריות הכי טובות להפעלה במעקב של מערכת מבוססת-משחק הן: {sched, freq, idle, am, wm, gfx, view, sync, binder_driver, hal, dalvik}.
  • שטח אחסון זמני: כלל כללי הוא ששטח אחסון זמני של 10MB לכל ליבת מעבד (CPU) מאפשר נתוני מעקב שנמשכים כ-20 שניות. לדוגמה, אם למכשיר יש שני מעבדים מרובעי ליבה (סה"כ 8 ליבות), הערך המתאים להעברה לתוכנית systrace הוא 80,000KB ‏ (80MB).

    אם המשחק מבצע הרבה החלפות הקשר, כדאי להגדיל את המאגר ל-15MB לכל ליבת CPU.

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

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

python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \
  sched freq idle am wm gfx view sync binder_driver hal dalvik

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

  1. מפעילים את האפשרות Trace debuggable applications (מעקב אחרי אפליקציות שאפשר לנפות בהן באגים).

    כדי להשתמש בהגדרה הזו, במכשיר צריכים להיות 256MB או 512MB זמינים (בהתאם למעבד, אם יש לו 4 או 8 ליבות), וכל חלק של 64MB בזיכרון צריך להיות זמין כחלק רציף.

  2. בוחרים באפשרות קטגוריות ואז מפעילים את הקטגוריות ברשימה הבאה:

    • ‫am: מנהל הפעילויות
    • ‫binder_driver: מנהל התקן של ליבת Binder
    • ‫dalvik: Dalvik VM
    • ‫freq: תדר המעבד
    • gfx: גרפיקה
    • ‫hal: מודולים של חומרה
    • ‫idle: CPU Idle
    • ‫sched: תזמון של מעבד (CPU)
    • sync: סנכרון
    • ‫view: צפייה במערכת
    • ‫wm: Window Manager
  3. מפעילים את האפשרות תיעוד מעקב.

  4. טוענים את המשחק.

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

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

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

כדי לחסוך במקום בדיסק, קבצים של עקבות מערכת במכשיר נשמרים בפורמט דחוס של עקבות (*.ctrace). כדי לבטל את הדחיסה של הקובץ הזה כשמפיקים דוח, משתמשים בתוכנית של שורת הפקודה וכוללים את האפשרות --from-file:

python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \
  -o my_systrace_report.html

שיפור תחומים ספציפיים בביצועים

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

מהירות הטעינה

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

  • ביצוע טעינה מדורגת. אם אתם משתמשים באותם נכסים בסצנות או ברמות עוקבות במשחק, טוענים את הנכסים האלה רק פעם אחת.
  • צריך להקטין את גודל הנכסים. כך תוכלו לצרף לחבילת ה-APK של המשחק גרסאות לא דחוסות של הנכסים האלה.
  • משתמשים בשיטת דחיסה יעילה לדיסק. דוגמה לשיטה כזו היא zlib.
  • במקום mono, צריך להשתמש ב-IL2CPP. (רלוונטי רק אם אתם משתמשים ב-Unity). ‫IL2CPP מספק ביצועים טובים יותר של סקריפטים ב-C#.
  • הפעלת ריבוי-הליכי משנה במשחק. פרטים נוספים מופיעים בקטע עקביות של קצב הפריימים.

עקביות בקצב הפריימים

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

ריבוי נימים

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

ב-Systrace שמוצג באיור 1 אפשר לראות התנהגות שמאפיינת משחק שפועל רק על מעבד אחד בכל פעם:

דיאגרמה של שרשורים בתיעוד עקבות המערכת

איור 1. דוח Systrace למשחק עם שרשור יחיד

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

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

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

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

  • אם אתם מפתחים את המשחק באמצעות מנוע המשחקים Unity, צריך להפעיל את האפשרויות Multithreaded Rendering (רינדור מרובה-הליכי משנה) ו-GPU Skinning (הנפשה של עור באמצעות GPU).
  • אם אתם משתמשים במנוע רינדור בהתאמה אישית, ודאו שצינור הנתונים של פקודת הרינדור וצינור הנתונים של פקודת הגרפיקה מיושרים בצורה נכונה. אחרת, יכול להיות שיהיו עיכובים בהצגת הסצנות של המשחק.

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

דיאגרמה של שרשורים בתיעוד עקבות המערכת

איור 2. דוח Systrace למשחק מרובה-הליכים

טעינה של רכיב בממשק המשתמש

דיאגרמה של מחסנית מסגרות במעקב מערכת
איור 3. דוח Systrace למשחק שבו עשרות רכיבי ממשק משתמש מוצגים בו-זמנית

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

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

מומלץ לצמצם את זמן העדכון של ממשק המשתמש ל-2 עד 3 אלפיות השנייה. כדי לבצע עדכונים מהירים כאלה, אפשר לבצע אופטימיזציות דומות לאלה:

  • עדכון רק של האלמנטים במסך שזזו.
  • להגביל את מספר הטקסטורות והשכבות של ממשק המשתמש. כדאי לשקול לשלב קריאות לגרפיקה, כמו הצללות ומרקמים, שמשתמשות באותו חומר.
  • העברת פעולות אנימציה של רכיבים ל-GPU.
  • ביצוע סינון אגרסיבי יותר של פירמידת הראייה והסתרה.
  • אם אפשר, כדאי לבצע פעולות ציור באמצעות Vulkan API. התקורה של בקשות להזזת פריט גרפי (draw call) נמוכה יותר ב-Vulkan.

צריכת חשמל

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

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

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

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

דחיית עבודות לטווח קצר למעבדים חלשים יותר

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

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

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

דיאגרמה של שרשורים בתיעוד עקבות המערכת

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

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

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

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

#include <sched.h>
#include <sys/types.h>
#include <unistd.h>

pid_t my_pid; // PID of the process containing your thread.

// Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs".
cpu_set_t my_cpu_set;
CPU_ZERO(&my_cpu_set);
CPU_SET(0, &my_cpu_set);
CPU_SET(1, &my_cpu_set);
CPU_SET(2, &my_cpu_set);
CPU_SET(3, &my_cpu_set);
sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);

עקה תרמית

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

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

קודם כול, מצהירים על האובייקט PowerManager ומאתחלים אותו בשיטה onCreate(). מוסיפים פונקציית listener לסטטוס התרמי לאובייקט.

Kotlin

class MainActivity : AppCompatActivity() {
    lateinit var powerManager: PowerManager

    override fun onCreate(savedInstanceState: Bundle?) {
        powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
        powerManager.addThermalStatusListener(thermalListener)
    }
}

Java

public class MainActivity extends AppCompatActivity {
    PowerManager powerManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        powerManager.addThermalStatusListener(thermalListener);
    }
}

הגדרת הפעולות שיש לבצע כשהמאזין מזהה שינוי סטטוס. אם המשחק שלכם משתמש ב-C/C++, מוסיפים קוד לרמות הסטטוס התרמי ב-onThermalStatusChanged() כדי לקרוא לקוד המשחק המקורי באמצעות JNI או להשתמש ב-Thermal API המקורי.

Kotlin

val thermalListener = object : PowerManager.OnThermalStatusChangedListener() {
    override fun onThermalStatusChanged(status: Int) {
        when (status) {
            PowerManager.THERMAL_STATUS_NONE -> {
                // No thermal status, so no action necessary
            }

            PowerManager.THERMAL_STATUS_LIGHT -> {
                // Add code to handle light thermal increase
            }

            PowerManager.THERMAL_STATUS_MODERATE -> {
                // Add code to handle moderate thermal increase
            }

            PowerManager.THERMAL_STATUS_SEVERE -> {
                // Add code to handle severe thermal increase
            }

            PowerManager.THERMAL_STATUS_CRITICAL -> {
                // Add code to handle critical thermal increase
            }

            PowerManager.THERMAL_STATUS_EMERGENCY -> {
                // Add code to handle emergency thermal increase
            }

            PowerManager.THERMAL_STATUS_SHUTDOWN -> {
                // Add code to handle immediate shutdown
            }
        }
    }
}

Java

PowerManager.OnThermalStatusChangedListener thermalListener =
    new PowerManager.OnThermalStatusChangedListener () {

    @Override
    public void onThermalStatusChanged(int status) {

        switch (status)
        {
            case PowerManager.THERMAL_STATUS_NONE:
                // No thermal status, so no action necessary
                break;

            case PowerManager.THERMAL_STATUS_LIGHT:
                // Add code to handle light thermal increase
                break;

            case PowerManager.THERMAL_STATUS_MODERATE:
                // Add code to handle moderate thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SEVERE:
                // Add code to handle severe thermal increase
                break;

            case PowerManager.THERMAL_STATUS_CRITICAL:
                // Add code to handle critical thermal increase
                break;

            case PowerManager.THERMAL_STATUS_EMERGENCY:
                // Add code to handle emergency thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SHUTDOWN:
                // Add code to handle immediate shutdown
                break;
        }
    }
};

זמן האחזור של המגע עד להצגה

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

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

  1. יצירת דוח Systrace שכולל את הקטגוריות gfx ו-input. הקטגוריות האלה כוללות מדדים שימושיים במיוחד לקביעת זמן האחזור של מגע להצגה.
  2. בודקים את הקטע SurfaceView בדוח Systrace. מאגר נתונים זמני עמוס מדי גורם למספר הציורים של מאגר הנתונים הזמני בהמתנה לנוע בין 1 ל-2, כפי שמוצג באיור 5:

    דיאגרמה של תור מאגר נתונים זמני בתוך עקבות המערכת

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

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

שילוב של Android Frame Pacing API במשחק

Android Frame Pacing API עוזר לכם לבצע החלפות של פריימים ולהגדיר מרווח החלפה, כך שקצב הפריימים במשחק יהיה עקבי יותר.

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

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

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

רמת ההחלקה של הרינדור

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

תרשים של פריימים
שחסר להם חלון Vsync כי הם התחילו לצייר מאוחר מדי

איור 6. דוח Systrace שמראה איך יכול להיות שפריים לא יסונכרן עם Vsync

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

כדי לפתור את הבעיה הזו, אפשר להשתמש ב-Android Frame Pacing API, שתמיד מציג פריים חדש ב-VSync wavefront.

מצב זיכרון

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

במצב כזה, כדאי לבדוק את פעילות המעבד בדוח Systrace ולראות באיזו תדירות המערכת מבצעת קריאות ל-daemon של kswapd. אם יש הרבה קריאות במהלך ההרצה של המשחק, מומלץ לבדוק איך המשחק מנהל את הזיכרון ומנקה אותו.

מידע נוסף זמין במאמר מידע על ניהול זיכרון.

מצב השרשור

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

דיאגרמה של דוח Systrace

איור 7. דוח Systrace שמראה איך בחירה של שרשור גורמת לדוח להציג סיכום מצב של השרשור הזה

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

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

מקורות מידע נוספים

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

סרטונים

  • מצגת בנושא Systrace for Games מתוך כנס Android Game Developer Summit‏ 2018