ניתוח מעמיק של NG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

אני איאן קילפטריק, ראש הנדסה בצוות הפריסה של Blink, יחד עם קוג'י אישיי. לפני העבודה עם הצוות של Blink, הייתי מהנדס קצה קדמי (לפני ש-Google קיבלה תפקיד של "מהנדס קצה קדמי"), יצירת תכונות בתוך Google Docs, Drive ו-Gmail. אחרי כחמש שנים בתפקיד הזה, עברתי לצוות של Blink בשיטת הימורים גדולה, ללמוד בצורה יעילה את C++ בעבודה, ומנסה להרחיב את היכולות של בסיס הקוד המורכב מאוד של Blink. גם היום, אני מבין רק חלק קטן יחסית ממנו. אני רוצה להודות לך על הזמן שהקדשת לי בתקופה הזו. הרבה "משחזרים של מהנדסים סופיים" ריכזתי אותי עבר ל"מהנדס דפדפנים" לפניי.

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

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

תצוגה של 30,000 מטרים של ארכיטקטורות של מנועי חיפוש

בעבר, עץ הפריסה של Blink היה מה שנקרא "עץ ניתן לשינוי".

הצגת העץ כפי שמתואר בטקסט הבא.

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

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

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

הרצת פריסה על צומת בעץ הזה מבוססת על 'סגנון וגם DOM', ואילוצים ברמה העליונה של מערכת הפריסה ההורה (רשת, בלוק או גמיש), מפעיל את האלגוריתם של אילוצי הפריסה ומפיק תוצאה.

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

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

עץ המקטעים.

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

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

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

סוגי באגים בפריסה

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

תיקון

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

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

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

עכשיו, אחרי ש-LayoutNG מגדיר בבירור את החוזה בין כל הרכיבים במערכת הפריסה, גילינו שאנחנו יכולים ליישם שינויים בצורה בטוחה יותר. אנחנו גם נהנים מאוד מפרויקט Web Platform Tests (WPT) המעולה, שמאפשר למספר גורמים לתרום לחבילה משותפת של בדיקות אינטרנט.

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

ביטול תוקף

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

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

if (/* some very complicated statement */) {
  child->ForceLayout();
}

בדרך כלל תיקון לבאג מהסוג הזה:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

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

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

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

השוואת תמונה ברוחב קבוע ובאחוז רוחב פס.
לרכיב רוחב/גובה קבוע לא משנה אם הגודל הזמין שניתן לו גדל, אבל כן רוחב/גובה מבוסס אחוזים. הערך available-size מיוצג באובייקט Parent Constraints, וכחלק מאלגוריתם הדיפרנציציה (ההבדלים) תתבצע האופטימיזציה הזו.

קוד ההבדלים של הדוגמה שלמעלה הוא:

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

היסטרזה

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

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

בסרטון ובהדגמה מוצג באג היסטרזה ב-Chrome 92 ומטה. השגיאה תוקנה בגרסה 93 של Chrome.

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

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

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

ביטול שגיאות וביצועים

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

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

העלייה של 2 המעברים וצוקי הביצועים

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

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

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

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

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

פריסות של מעבר אחד, שניים ושלושת המעברים שמוסברות בכיתוב.
בתמונה שלמעלה יש שלושה רכיבי <div>. פריסה פשוטה עם כרטיס אחד (כמו פריסת בלוקים) תמשוך שלושה צמתים לפריסה (complexity O(n)). עם זאת, לפריסה עם שני מעברים (כמו גמיש או רשת), הדבר עלול לגרום למורכבות של כניסות O(2n) עבור הדוגמה הזו.
תרשים שבו מוצגת העלייה המעריכית בזמן הפריסה.
התמונה הזו וההדגמה הזו מציגה פריסה מעריכית עם פריסת רשת. השגיאה תוקנה בגרסה 93 של Chrome כתוצאה מהעברת הרשת לארכיטקטורה החדשה

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

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

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

לסיכום

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

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

תמונה אחת (אתם יודעים איזו!) של Una Kravets.