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

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

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

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

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

עיבוד לפני NG

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

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

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

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

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

Style יוצר מבני נתונים משניים שקובעים את מהלך הרכבת התמונות, ומבני הנתונים האלה משתנים במקום בכל שלב אחרי style.

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

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

מה שינינו

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

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

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

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

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

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

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

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

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

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

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

בשלבים האלה מתבצעות הפעולות הבאות:

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

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

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

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

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

Project Squad: צינור עיבוד נתונים לשלב הסגנון

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

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

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

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

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

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

השלב המקדים להמרת תמונה וקטורית למפת סיביות (painting)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

יתרונות

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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