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

איאן קילפטריק
איאן קילפטריק
קוג'י אישי
קוג'י אישי

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

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

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

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

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

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

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

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

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

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

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

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

עץ המקטע.

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

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

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

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

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

תקינות

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

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

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

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

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

חוסר תוקף

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

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

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

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

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

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

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

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

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

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

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, יש לנו מבני נתונים מפורשים לקלט ולפלט, וגישה למצב הקודם אסורה, ולכן צמצמנו באופן נרחב את סוג הבאג הזה ממערכת הפריסה.

ביטול תוקף יתר וביצועים

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

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

עלייה בפריסות של שני מעברים וצוקי ביצועים

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

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

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

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

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

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

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

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

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

לסיכום

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

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

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