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

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

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

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

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

רינדור לפני NG

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

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

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

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

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

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

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

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

מה שינינו

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

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

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

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

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

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

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

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

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

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

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

ביחד, שלבי הרינדור לפני הצביעה אחראים לגורמים הבאים:

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

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

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

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

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

Project Squad: תכנון של שלב הסגנון

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

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

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

LayoutNG: שרטוט של שלב הפריסה

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

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

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

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

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

עצי המאפיין: גיאומטריה עקבית

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

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

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

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

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

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

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

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

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

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

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

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

יתרונות

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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