מודל ההערכה והמצטבר של Bazel.
מודל נתונים
מודל הנתונים כולל את הפריטים הבאים:
SkyValue
. נקרא גם צמתים.SkyValues
הם אובייקטים בלתי משתנים שמכילים את כל הנתונים שנוצרו במהלך ה-build ואת הקלט של ה-build. לדוגמה: קובצי קלט, קובצי פלט, יעדים ויעדים מוגדרים.SkyKey
. שם קצר שלא ניתן לשנות להתייחסות ל-SkyValue
, לדוגמה,FILECONTENTS:/tmp/foo
אוPACKAGE://foo
.SkyFunction
. בניית צמתים על סמך המפתחות שלהם וצמתים תלויים.- תרשים צומת. מבנה נתונים שמכיל את הקשר בין תלות לצמתים.
Skyframe
. שם הקוד של המסגרת המסכמת בהערכה מבוססת על.
הערכה
גרסת build מורכבת מהערכה של הצומת שמייצג את בקשת ה-build (זו המדינה שאנחנו שואפים לקבל, אבל יש בה הרבה קודים ישנים). תחילה, ה-SkyFunction
נמצא ונקרא עם המפתח של ה-SkyKey
ברמה העליונה. לאחר מכן, הפונקציה מבקשת את ההערכה של הצמתים שעליהם היא צריכה להעריך את הצומת ברמה העליונה, אשר בתורו גורם הפעלות אחרות של הפונקציה, וכן הלאה, עד שמגיעים לצומתי העלה (שהם בדרך כלל צמתים המייצגים קובצי קלט במערכת הקבצים). לבסוף, בסופו של דבר מתקבל הערך SkyValue
ברמה העליונה, כמה תופעות לוואי (כמו קובצי פלט במערכת הקבצים) ותרשים אציקלי מכוון של התלויות בין הצמתים שהיו מעורבים במודל.
SkyFunction
יכול לבקש SkyKeys
בכמה מסלולים אם הוא לא יכול להודיע מראש על כל הצמתים שהוא צריך כדי לבצע את עבודתו. דוגמה פשוטה: הערכה של צומת של קובץ קלט שזוהה כקישור סימולטני: הפונקציה מנסה לקרוא את הקובץ, מבינה שזהו קישור סימולטני, ומאחזרת את הצומת של מערכת הקבצים שמייצג את היעד של הקישור הסמלי. אבל גם הוא יכול להיות קישור סימבולי, ולכן גם הפונקציה המקורית צריכה לאחזר את היעד שלה.
הפונקציות מיוצגות בקוד על ידי הממשק SkyFunction
, ועל ידי השירותים המסופקים לו על ידי ממשק בשם SkyFunction.Environment
. הפעולות שהפונקציות האלה יכולות לבצע:
- בקשת הערכה של צומת אחר באמצעות התקשרות ל-
env.getValue
. אם הצומת זמין, הערך שלו מוחזר. אחרת, הפונקציהnull
מוחזרת והפונקציה עצמה צפויה להחזירnull
. במקרה השני, הצומת התלוי נבדק, ולאחר מכן מופעל בונה הצמתים המקורי שוב, אבל הפעם אותה קריאה מסוגenv.getValue
תחזיר ערך שאינוnull
. - צריך לבקש הערכה של מספר צמתים אחרים על ידי התקשרות ל-
env.getValues()
. למעשה, האופרטור הזה זהה, אבל הצמתים התלויים מוערכים בו-זמנית. - לבצע חישוב במהלך ההפעלה
- לדוגמה, אם יש לכם תופעות לוואי, למשל אם אתם כותבים קבצים למערכת הקבצים. יש להיזהר כי שתי פונקציות שונות לא עולות זו על זו. באופן כללי, כתיבת תופעות לוואי (כאשר הנתונים יוצאים החוצה מ-Bazel) היא בסדר, קריאת תופעות לוואי (כאשר הנתונים נופלים פנימה ל-Bazel ללא תלות רשומה), כיוון שהן תלויות שלא נרשמו, ולכן עלולות לגרום לתוספות מצטבר שגויות.
להטמעות של SkyFunction
אין לגשת לנתונים בשום צורה אחרת מלבד בקשת תלות (למשל על ידי קריאה ישירה במערכת הקבצים). בעקבות זאת, מאחר ש-Bazel לא מתעדת את התלות של הנתונים בקובץ שנקרא, וכתוצאה מכך הנתונים המצטברים שגויים.
כשבפונקציה יש מספיק נתונים לביצוע המשימה, היא אמורה להחזיר ערך שאינו null
.
לאסטרטגיית ההערכה הזו יש כמה יתרונות:
- הרמוניה. אם פונקציות יבקשו נתוני קלט רק בהתאם לצמתים אחרים, Bazel יכולה להבטיח שאם מצב הקלט זהה, יוחזרו אותם הנתונים. אם כל הפונקציות ב-Sky הן דטרמיניסטיות, המשמעות היא שהמבנה כולו גם הוא דטרמיניסטי.
- המרות מצטברות מדויקות ומושלמות. אם כל נתוני הקלט של כל הפונקציות מתועדים, Bazel יכולה לבטל את התוקף של קבוצת הצמתים המדויקת שיש לבטל את התוקף שלה כאשר נתוני הקלט משתנים.
- מקבילות. מכיוון שפונקציות יכולות לתקשר אחת עם השנייה באמצעות בקשת תלות, יכולות להיות פונקציות שלא תלויות אחת בשנייה בו-זמנית, ו-Bazel יכולה להבטיח שהתוצאה תהיה זהה למצב שבו הן יפעלו ברצף.
עלייה
מאחר שפונקציות יכולות לגשת לנתוני קלט רק בהתאם לצמתים אחרים, Bazel יכולה ליצור תרשים זרימה מלא של נתונים מקובצי הקלט לקובצי הפלט, ולהשתמש במידע הזה כדי לבנות מחדש את הצמתים שצריכים להיבנה מחדש: סגירה הפוכה של קבוצת קובצי הקלט שהשתנו.
באופן ספציפי, קיימות שתי אסטרטגיות מצטברות: האחת למטה והאחת למטה. הדרך האופטימלית תלויה במראה של תרשים התלות.
במהלך ביטול התוקף מתחתית המסך, לאחר יצירת תרשים וידוע שקבוצת הקלט השתנתה, כל הצמתים לא יהיו תקפים ותלויים בקבצים שהשתנו. זו האפשרות האופטימלית, אם אנחנו יודעים שאותו צומת ברמה העליונה ייווצר שוב. חשוב לשים לב שכדי לבטל את האימות של למטה, צריך להפעיל את
stat()
בכל קובצי הקלט של ה-build הקודם כדי לקבוע אם הם השתנו. ניתן לשפר זאת על ידי שימוש ב-inotify
או במנגנון דומה כדי ללמוד על קבצים שהשתנו.במהלך ביטול האימות מלמעלה למטה, מתבצעת בדיקה של הסגירה העקיפה של הצומת ברמה העליונה ורק הצמתים האלה נשמרים כאשר הסגירה העקיפה שלהם נקייה. שיטה זו טובה יותר אם אנחנו יודעים שהתרשים הנוכחי של הצומת גדול, אבל אנחנו צריכים רק קבוצת משנה קטנה שלו במודל הבא: ביטול התוקף כלפי מטה יגרום לביטול התרשים הגדול יותר של גרסת ה-build הראשונה, בניגוד לביטול מלמעלה למטה, שפשוט מעביר את התרשים הקטן של ה-build השני.
בשלב זה, אנחנו מבטלים את התוקף של הספירה לאחור.
כדי להגדיל את הנתונים עוד יותר, אנחנו משתמשים בגיזום שינויים: אם הצומת לא תקין, אבל לאחר הבנייה מחדש מבחינים שהערך החדש שלו זהה לערך הישן. הצמתים שאינם חוקיים עקב שינוי בצומת הזה נמצאים 'מחדש'.
האפשרות הזו שימושית, למשל, אם משנים תגובה בקובץ C++: הקובץ .o
שיופק ממנה יהיה זהה, כך שלא נצטרך לקרוא שוב לקישור.
קישור / אוסף מצטבר
המגבלה העיקרית של המודל הזה היא שביטול התוקף של צומת הוא עניין 'הכול' או 'הכול': כשצומת תלוי, הוא תמיד נוצר מחדש מאפס, גם אם קיים אלגוריתם טוב יותר שעשוי לשנות את הערך הישן של הצומת בהתבסס על השינויים. הנה כמה דוגמאות ליתרונות הבאים:
- קישור מצטבר
- כשקובץ אחד (
.class
) משתנה ב-.jar
, אפשר לשנות את התיאור.jar
של הקובץ במקום ליצור אותו שוב מאפס.
הסיבה לכך ש-Bazel אינה תומכת בדברים אלה באופן עקרוני (יש לנו מידה מסוימת של תמיכה בקישור מצטבר, אך אנו לא מטמיעים אותו ב-Skyframe). יש לנו שני רווחים מוגבלים בלבד, וקשה היה להבטיח שהתוצאה של המוטציה תהיה זהה לזו של שיקום נקי, ושהערכים ב-Google Build הם ביטים ניתנים להרחבה.
עד עכשיו, תמיד היינו יכולים להשיג ביצועים טובים מספיק על ידי ניתוח שלב יקר של בדיקה מחדש. לשם כך הוא מחלק מחדש את כל הכיתות באפליקציה למספר קבוצות ומבצע בהן הפרדה בנפרד. כך, אם כיתות בקבוצה לא משתנות, אין צורך לבצע פענוח חוזר של תהליך הביטול.
מיפוי לקונספטים של Bazel
זוהי סקירה כללית של כמה מההטמעות של SkyFunction
, ש-Bazel משתמשת בהן כדי לבצע build:
- FileStateValue. התוצאה של
lstat()
. בקבצים קיימים, אנחנו מחשבים גם מידע נוסף כדי לזהות שינויים בקובץ. הצומת הזה הוא הרמה הנמוכה ביותר בתרשים Skyframe ואין לו יחסי תלות. - FileValue. משמש כל מה שחשוב לתוכן עצמו ו/או לנתיב שהושג של קובץ. בהתאם ל-
FileStateValue
המתאים ולכל קישורי ה-URL שצריך לפתור (כמוFileValue
עבורa/b
, דרוש הנתיב המטופל שלa
והנתיב שהסיומת שלa/b
). ההבחנה ביןFileStateValue
חשובה כי במקרים מסוימים (למשל, הערכת כדורי מערכת של הקובץ (כמוsrcs=glob(["*/*.java"])
) בפועל אין צורך בתוכן הקובץ. - DirectoryListingValue. למעשה, התוצאה היא
readdir()
. תלוי ב-FileValue
המשויך לספרייה. - packageValue. מייצגת את הגרסה שניתוחה של קובץ BUILD. בהתאם ל-
FileValue
של הקובץ המשויך ב-BUILD
, וגם באופן עקרוני בכלDirectoryListingValue
שמשמש לפתרון הגלובוסים בחבילה (מבנה הנתונים שמייצג את התוכן של קובץBUILD
באופן פנימי) - הוגדר כ-TargetValue. מייצג יעד מוגדר, שהוא מגוון של פעולות שנוצרו במהלך ניתוח יעד ומידע שסופק ליעדים מוגדרים שתלויים ביעד הזה. בהתאם ל-
PackageValue
שבו נמצא היעד התואם,ConfiguredTargetValues
יחסי התלות הישירים וצומת מיוחד שמייצג את תצורת ה-build. - ArtifactValue. מייצג קובץ ב-build, בין אם הוא מקור או פריט מידע שנוצר בתהליך פיתוח (פריטי מידע שנוצרו כמעט זהים לקבצים, ומשמשים להתייחס לקבצים במהלך ההפעלה בפועל של שלבי ה-build). עבור קובצי מקור, הדבר תלוי ב-
FileValue
של הצומת המשויך לפריטי פלט, תלוי ב-ActionExecutionValue
של פעולה שיוצרת את פריט המידע שנוצר בתהליך הפיתוח. - ActionExecutionValue. מייצג ביצוע. תלוי ב-
ArtifactValues
קובצי הקלט שלה. הפעולה שהיא מבצעת כוללת כרגע במפתח השמיים שלה, בניגוד לרעיון שמפתחות השמיים צריכים להיות קטנים. אנחנו עובדים על תיקון חוסר ההתאמה הזו (שימו לב שאנחנו לא משתמשים ב-ActionExecutionValue
וב-ArtifactValue
אם אנחנו לא מפעילים את שלב הביצוע ב-Skyframe).