אם אי אפשר למדוד את זה, אי אפשר לשפר את זה.
לורד קלווין
כדי שתוכלו להריץ את משחקי ה-HTML5 מהר יותר, קודם כול עליכם לזהות את נקודות החולשה בביצועים, אבל זה יכול להיות קשה. הערכת הנתונים של הפריימים לשנייה (FPS) היא התחלה, אבל כדי לראות את התמונה המלאה, צריך להבין את הניואנסים בפעילויות ב-Chrome.
הכלי about:tracing
מספק תובנות שיעזרו לכם להימנע מפתרון בעיות מהיר שמטרתו לשפר את הביצועים, אבל הוא בעצם ניחוש מכוון. כך תחסכו הרבה זמן ואנרגיה, תקבלו תמונה ברורה יותר של מה ש-Chrome עושה עם כל פריים ותשתמשו במידע הזה כדי לבצע אופטימיזציה של המשחק.
שלום about:tracing
הכלי about:tracing
ב-Chrome מאפשר לכם לראות את כל הפעילויות ב-Chrome במשך תקופה מסוימת, ברמת פירוט כה גבוהה שיכול להיות שתרגישו מוצפים בהתחלה. רבות מהפונקציות ב-Chrome מוגדרות מראש למעקב, כך שעדיין תוכלו להשתמש ב-about:tracing
כדי לעקוב אחרי הביצועים בלי לבצע כלים ידניים. (בקטע מאוחר יותר מוסבר איך מבצעים הטמעה ידנית של JS)
כדי להציג את תצוגת המעקב, פשוט מקלידים about:tracing בסרגל הכתובות של Chrome.
בכלי המעקב תוכלו להתחיל את ההקלטה, להריץ את המשחק למשך כמה שניות ואז להציג את נתוני המעקב. דוגמה למראה הנתונים:
כן, זה באמת מבלבל. עכשיו נראה איך קוראים אותו.
כל שורה מייצגת תהליך שנוצר לו פרופיל, הציר שמימין לשמאל מציין זמן וכל תיבה צבעונית היא קריאה למנגנון למדידה של פונקציה. יש שורות למספר סוגים שונים של משאבים. הנתונים המעניינים ביותר לצורך יצירת פרופיל של משחק הם CrGpuMain, שמראה מה יחידת העיבוד הגרפי (GPU) עושה, ו-CrRendererMain. כל מעקב מכיל שורות של CrRendererMain לכל כרטיסייה פתוחה במהלך תקופת המעקב (כולל הכרטיסייה about:tracing
עצמה).
כשקוראים את נתוני המעקב, המשימה הראשונה היא לקבוע איזו שורה של CrRendererMain תואמת למשחק שלכם.
בדוגמה הזו, שתי המועמדות הן: 2216 ו-6516. לצערנו, בשלב הזה אין דרך מושלמת לזהות את האפליקציה, מלבד לחפש את השורה שמתבצעת בה הרבה עדכונים תקופתיים (או אם הטמעתם את הקוד באופן ידני עם נקודות מעקב, לחפש את השורה שמכילה את נתוני המעקב). בדוגמה הזו, נראה ש-6516 מפעיל לולאה ראשית לפי תדירות העדכונים. אם תסגרו את כל הכרטיסיות האחרות לפני שתתחילו את המעקב, יהיה קל יותר למצוא את CrRendererMain הנכון. עם זאת, עדיין עשויות להיות שורות CrRendererMain לתהליכים שאינם המשחק שלכם.
איך מוצאים את המסגרת
אחרי שמאתרים את השורה הנכונה בכלי המעקב אחרי המשחק, השלב הבא הוא למצוא את הלולאה הראשית. הלולאה הראשית נראית כמו דפוס חוזר בנתוני המעקב. אפשר לנווט בנתוני המעקב באמצעות המקשים W, A, S ו-D: A ו-D כדי לזוז שמאלה או ימינה (קדימה ואחורה בזמן) ו-W ו-S כדי להגדיל או להקטין את התצוגה של הנתונים. אם המשחק פועל במהירות 60Hz, הלולאה הראשית אמורה להיות דפוס שמתרחש שוב ושוב כל 16 אלפיות השנייה.
אחרי שמאתרים את הדופק של המשחק, אפשר להבין בדיוק מה הקוד עושה בכל פריים. מגדילים את התצוגה באמצעות המקשים W, A, S ו-D עד שאפשר לקרוא את הטקסט בתיבות הפונקציות.
באוסף התיבות הזה מוצגת סדרה של קריאות לפונקציות, כאשר כל קריאה מיוצגת על ידי תיבה צבעונית. כל פונקציה הוזמנה על ידי התיבה שמעליה, כך שבמקרה הזה אפשר לראות ש-MessageLoop::RunTask הזמינה את RenderWidget::OnSwapBuffersComplete, שבתורה הזמינה את RenderWidget::DoDeferredUpdate, וכן הלאה. קריאת הנתונים האלה מאפשרת לכם לקבל תמונה מלאה של מה גרם לקריאה של מה וכמה זמן נמשכה כל הפעלה.
אבל כאן מתחילה הבעיה. המידע שחשוף על ידי about:tracing
הוא קריאות הפונקציה הגולמיות מקוד המקור של Chrome. אפשר לנחש מהשם של כל פונקציה מה היא עושה, אבל המידע לא ממש ידידותי למשתמש. כדאי לראות את התמונה הכוללת של המסגרת, אבל צריך משהו שקל יותר לקריאה על ידי בני אדם כדי להבין מה קורה בפועל.
הוספת תגי מעקב
למרבה המזל, יש דרך ידידותית להוסיף לקוד שלכם מכשירי מדידה ידניים כדי ליצור נתוני מעקב: console.time
ו-console.timeEnd
.
console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");
הקוד שלמעלה יוצר תיבות חדשות בשם תצוגת המעקב עם התגים שצוינו, כך שאם תפעילו מחדש את האפליקציה יופיעו התיבות 'עדכון' ו'עיבוד', שבהן יוצג הזמן שחלף בין הקריאות להתחלה ולסיום של כל תג.
כך תוכלו ליצור נתוני מעקב שקריאים לבני אדם כדי לעקוב אחרי נקודות חמות בקוד.
GPU או CPU?
כשמשתמשים בגרפיקה עם האצה בחומרה, אחת מהשאלות החשובות ביותר שאפשר לשאול במהלך יצירת הפרופיל היא: האם הקוד הזה מוגבל ל-GPU או ל-CPU? בכל פריים תבצעו קצת עיבוד ב-GPU וקצת לוגיקה ב-CPU. כדי להבין מה מאט את המשחק, תצטרכו לראות איך העבודה מאוזנת בין שני המשאבים.
קודם כל, מחפשים את השורה CrGPUMain בתצוגת המעקב, שמציינת אם ה-GPU עסוק בזמן מסוים.
אפשר לראות שכל פריים במשחק גורם לעבודה של המעבד (CPU) ב-CrRendererMain וגם ב-GPU. בתרשים שלמעלה מוצג תרחיש לדוגמה פשוט מאוד, שבו גם המעבד (CPU) וגם המעבד הגרפי (GPU) לא פעילים במשך רוב כל פריים של 16 אלפיות השנייה.
תצוגת המעקב שימושית במיוחד כשיש משחק שפועל לאט ואתם לא בטוחים איזה משאב מגיע למקסימום. הדרך העיקרית לניפוי באגים היא לבדוק את הקשר בין הקווים של ה-GPU לבין הקווים של ה-CPU. נשתמש באותה דוגמה כמו קודם, אבל נוסיף קצת עבודה נוספת בלולאת העדכון.
console.time("update");
doExtraWork();
update(Math.min(50, now - time));
console.timeEnd("update");
console.time("render");
render();
console.timeEnd("render");
עכשיו יוצג לכם מעקב שנראה כך:
מה אנחנו יכולים ללמוד מהמעקב הזה? אפשר לראות שהפריים בתמונה עובר מ-2,270 אלפיות השנייה ל-2,320 אלפיות השנייה, כלומר כל פריים נמשך כ-50 אלפיות השנייה (קצב פריימים של 20Hz). לצד תיבת העדכון אפשר לראות קטעים של תיבות צבעוניות שמייצגות את פונקציית הרינדור, אבל התמונה כולה מוקדשת לעדכון עצמו.
בניגוד למה שקורה ב-CPU, אפשר לראות שה-GPU עדיין לא פעיל ברוב הפריימים. כדי לבצע אופטימיזציה של הקוד הזה, אפשר לחפש פעולות שאפשר לבצע בקוד של שדה הצבעים ולהעביר אותן ל-GPU כדי לנצל את המשאבים בצורה הטובה ביותר.
מה קורה כשקוד ה-shader עצמו איטי וה-GPU עמוס בעבודה? מה אם נחסוך את העבודה הלא הכרחית במעבד ונוסיף אותה במקום זאת לקוד של שובר הפירורים (fragment shader). לפניכם שובר פרגמנט יקר שלא לצורך:
#ifdef GL_ES
precision highp float;
#endif
void main(void) {
for(int i=0; i<9999; i++) {
gl_FragColor = vec4(1.0, 0, 0, 1.0);
}
}
איך נראה קוד עם שריטות שמשתמש בשיידר הזה?
שוב, חשוב לשים לב למשך הזמן של פריים. כאן התבנית החוזרת נמשכת מ-2,750 אלפיות שנייה עד 2,950 אלפיות שנייה, למשך 200 אלפיות שנייה (קצב פריימים של כ-5Hz). השורה CrRendererMain כמעט ריקה לחלוטין, כלומר המעבד (CPU) לא פעיל רוב הזמן, בעוד שה-GPU עמווס. זה סימן בטוח לכך שהשידרוגים שלכם כבדים מדי.
אם לא הייתה לכם תובנה לגבי הגורם המדויק לפריים-ריט מסוים, יכול להיות שתבחינו בעדכון של 5Hz ותתפתתו להיכנס לקוד המשחק ולהתחיל לנסות לבצע אופטימיזציה או להסיר את הלוגיקה של המשחק. במקרה כזה, זה לא יעזור בכלל, כי הלוגיקה בלולאת המשחק היא לא זו שגורמת לבזבוז הזמן. למעשה, מה שאפשר לראות בתרשים הזה הוא שעבודה נוספת של המעבד בכל פריים תהיה בעצם 'ללא תשלום', כי המעבד לא עושה כלום, ולכן הוספת עבודה לא תשפיע על משך הזמן של היצירה של הפריים.
דוגמאות מהחיים
עכשיו נבדוק איך נראים נתוני המעקב ממשחק אמיתי. אחד מהדברים המגניבים במשחקים שנוצרו בטכנולוגיות אינטרנט פתוחות הוא שאפשר לראות מה קורה במוצרים האהובים עליכם. אם אתם רוצים לבדוק את הכלים ליצירת פרופילים, אתם יכולים לבחור את כותר ה-WebGL המועדף עליכם בחנות האינטרנט של Chrome וליצור לו פרופיל באמצעות about:tracing
. זוהי דוגמה למעקב אחר אירועים שנלקח מהמשחק המצוין Skid Racer ב-WebGL.
נראה שכל פריים נוצר תוך כ-20 אלפיות השנייה, כלומר קצב הפריימים הוא כ-50FPS. אפשר לראות שהעבודה מאוזנת בין המעבד לבין המעבד הגרפי, אבל המעבד הגרפי הוא המשאב שהכי נדרש. כדי לראות איך נראית יצירת פרופיל של דוגמאות אמיתיות למשחקי WebGL, כדאי לנסות לשחק בחלק מהכותרים בחנות האינטרנט של Chrome שנוצרו באמצעות WebGL, כולל:
סיכום
אם רוצים שהמשחק יפעל ב-60Hz, כל הפעולות צריכות להיכנס ל-16 אלפיות השנייה של זמן ה-CPU ול-16 אלפיות השנייה של זמן ה-GPU בכל פריים. יש לכם שני משאבים שאפשר להשתמש בהם במקביל, ואפשר להעביר ביניהם משימות כדי למקסם את הביצועים. התצוגה about:tracing
ב-Chrome היא כלי חשוב שמאפשר לקבל תובנות לגבי הפעולות שהקוד מבצע בפועל, ועוזר לכם למקסם את זמן הפיתוח על ידי טיפול בבעיות הנכונות.
מה השלב הבא?
בנוסף ל-GPU, אפשר גם לעקוב אחרי חלקים אחרים בסביבת זמן הריצה של Chrome. ב-Chrome Canary, הגרסה המוקדמת של Chrome, יש כלים למעקב אחר IO, IndexedDB ופעילויות נוספות. מומלץ לקרוא את המאמר הזה בנושא Chromium כדי להבין לעומק את המצב הנוכחי של מעקב האירועים.
מפתחי משחקים לאינטרנט, כדאי לכם לצפות בסרטון הבא. זוהי מצגת של צוות התמיכה למפתחי משחקים של Google בכנס GDC 2012 בנושא אופטימיזציה של ביצועים במשחקים ל-Chrome: