מסגרת עילית

מודל ההערכה והמצטבר של 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).