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

סטפן זאגר
סטפן זאגר
כריס הרלסון
כריס הרלסון

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

Blink התחיל את חייו כמזלג של WebKit, שהוא בעצם מזלג של KHTML, משנת 1998. הוא מכיל חלק מהקוד הישן ביותר (והקריטי ביותר) ב-Chromium, ועד 2014 ללא ספק הוצג הגיל שלו. באותה שנה התחלנו ליצור פרויקטים שאפתניים תחת הבאנר BlinkNG, במטרה לטפל בחסרונות ארוכי טווח בארגון ובמבנה של קוד Blink. המאמר הזה עוסק ב-BlinkNG ובפרויקטים שהם מורכבת: למה אנחנו עושים אותם, מה הם השיגו, העקרונות המנחים שעיצבו את העיצוב שלהם וההזדמנויות לשיפורים עתידיים שהם יכולים להשיג.

צינור עיבוד הנתונים לפני ואחרי BlinkNG.

רינדור לפני NG

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

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

יש לזה דוגמאות רבות, וביניהן:

סגנון ייצור ComputedStyle על סמך גיליונות סגנונות. אבל ComputedStyle לא היה ניתן לשינוי; במקרים מסוימים הוא ישתנה על ידי שלבים מאוחרים יותר של צינור עיבוד הנתונים.

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

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

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

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

מה שינינו

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

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

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

מחזור החיים של המסמך

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

  • אם אנחנו משנים מאפיין של ComputedStyle, מחזור החיים של המסמך חייב להיות kInStyleRecalc.
  • אם מצב DocumentLifecycle הוא kStyleClean ואילך, NeedsStyleRecalc() חייב להחזיר את הערך false עבור כל צומת מצורף.
  • כשנכנסים לשלב מחזור החיים של הצבע, המצב של מחזור החיים חייב להיות kPrePaintClean.

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

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

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

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

סגנון צינור, פריסה ולפני צביעה

ביחד, שלבי הרינדור לפני ה-Paint אחראים על הדברים הבאים:

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

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

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

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

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

צוות הפרויקט: תכנון שלב הסגנון

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

יש שתי פלטות עיקריות של שלב הסגנון: ComputedStyle, שמכילות את התוצאה של הפעלת האלגוריתם של מפלס ה-CSS בעץ ה-DOM, ועץ של LayoutObjects שקובע את סדר הפעולות בשלב הפריסה. באופן עקרוני, הפעלת אלגוריתם הדירוג צריכה להתבצע רק לפני יצירת עץ הפריסה; אבל בעבר, שתי הפעולות האלה שולבו. Project Squad הצליח לפצל את שני השלבים האלה לשלבים נפרדים ורציפים.

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

LayoutNG: צינור עיבוד הנתונים של שלב הפריסה

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

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

שלב טרום-הצביעה

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

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

עצי נכסים: גיאומטריה עקבית

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

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

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

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

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

חומר מרוכב אחרי צבע: צבע בצינורות ואיחוד

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

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

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

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

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

הסיבה השנייה לפרויקט Composite After Paint הייתה הבאג Fundamental Compositing. אחת הדרכים להצהיר על הבאג הזה היא שרכיבי DOM אינם ייצוג טוב של 1:1 סכימת שכבות יעילה או מלאה לתוכן של דפי אינטרנט. ומכיוון שהאיחוד היה לפני הצבע, הוא היה תלוי יותר או פחות באופן מעצם ברכיבי DOM, ולא ברשימות תצוגה או בעצי נכסים. הדבר דומה מאוד לסיבה שבגללה השקנו עצי נכסים, ובדיוק כמו במקרה של עצי נכסים, הפתרון נופל ישירות אם מזהים את שלב צינור עיבוד הנתונים הנכון, מפעילים אותו בזמן הנכון ומספקים לו את מבני הנתונים העיקריים. בדומה לעצי נכסים, זו הייתה הזדמנות טובה להבטיח שברגע ששלב הצביעה יושלם, הפלט שלו לא יהיה ניתן לשינוי עבור כל השלבים הבאים בצינור עיבוד הנתונים.

יתרונות

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

  • שיפור משמעותי באמינות: זו שיטה פשוטה למדי. קוד נקי יותר עם ממשקים מוגדרים ומובנים יותר קל להבנה, לכתיבה ולבדיקה. כך היא מהימנה יותר. הוא גם הופך את הקוד לבטוח ויציב יותר, עם פחות קריסות ופחות באגים ללא תשלום.
  • כיסוי בדיקות מורחב: במהלך BlinkNG, הוספנו הרבה בדיקות חדשות לחבילה שלנו. זה כולל בדיקות יחידה שמספקות אימות ממוקד של נתונים פנימיים; בדיקות רגרסיה שמונעות מאיתנו להציג שוב באגים ישנים שתוקנו (כל כך הרבה!) והרבה תוספות לחבילת בדיקת פלטפורמת האינטרנט הציבורית, שכל הדפדפנים משתמשים בה למדידת התאימות לתקני אינטרנט.
  • קל יותר להרחיב: אם מערכת מחולקת לרכיבים ברורים, לא צריך להבין רכיבים אחרים בכל רמת פירוט כדי להתקדם במערכת הנוכחית. כך קל יותר לכולם להוסיף ערך לקוד הרינדור בלי להיות מומחים עמוקים, וגם קל יותר להבין את ההתנהגות של המערכת כולה.
  • ביצועים: קשה מספיק לבצע אופטימיזציה של אלגוריתמים שנכתבו בקוד ספגטי, אבל כמעט בלתי אפשרי להשיג דברים גדולים יותר כמו גלילה ואנימציות בשרשור אוניברסלי או תהליכים ושרשורים לבידוד אתרים ללא צינור עיבוד נתונים כזה. מקביליות יכולה לעזור לנו לשפר את הביצועים באופן ניכר, אבל היא גם מורכבת מאוד.
  • יבול ובלימה: יש כמה תכונות חדשות שמתאפשרות הודות ל-BlinkNG, שמפעילות את צינור עיבוד הנתונים בדרכים חדשות וחדשניות. לדוגמה, מה קורה אם רוצים להפעיל את צינור עיבוד הנתונים לעיבוד רק עד שיפוג תוקפו של התקציב? או לדלג על העיבוד של עצים משניים שידוע שאינם רלוונטיים למשתמש כרגע? כך נכס ה-CSS הרשאות גישה לתוכן מאפשר. מה לגבי שינוי הסגנון של רכיב מסוים בהתאם לפריסה שלו? מדובר בשאילתות של קונטיינר.

מקרה לדוגמה: שאילתות לגבי קונטיינרים

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

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

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

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

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

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

עתיד: איחוד ללא שרשור ראשי ... ומעבר לכך!

צינור עיבוד הנתונים לעיבוד שמוצג כאן מקדים למעשה את ההטמעה הנוכחית של RenderingNG. הצגת השכבות כ'מחוץ ל-thread הראשי', אבל כרגע היא עדיין ב-thread הראשי. עם זאת, רק רגע לפני שסיימתם לשלוח את התמונה, עכשיו לאחר השליחה של המאפיין Composite After Paint והיצירת שכבות של הפילטר.

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

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

עכשיו, כשהכלי BlinkNG מתקרב להשלמת התהליך, אנחנו כבר מתחילים לחקור את התחום. Non-block Commit הוא הניסיון הראשון לשינוי מודל השרשור של הכלי לעיבוד. Compositor report (או פשוט commit) הוא שלב סנכרון בין ה-thread הראשי ל-thread של המחבר. במהלך השמירה, אנחנו יוצרים עותקים של נתוני הרינדור שמופקים ב-thread הראשי, כדי להשתמש בהם בקוד החיבור במורד הזרם שפועל ב-thread הראשי. בזמן שהסנכרון מתבצע, ההפעלה של ה-thread הראשי נפסקת בזמן שקוד ההעתקה פועל ב-thread של הקומפוזיטור. כך ה-thread הראשי לא ישנה את נתוני הרינדור שלו בזמן שה-thread הראשי מעתיק אותו.

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

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

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