אתגרים בתחום כללי הכתיבה

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

דרישות לסיכום

  • הנחה: השאיפה לנכונות, תפוקה, קלות השימוש & זמן אחזור
  • הנחה: מאגרים בקנה מידה גדול
  • הנחה: שפת תיאור דמוית BUILD
  • היסטורי: הפרדה קשה בין טעינה, ניתוח והפעלה מיושנת, אבל היא עדיין משפיעה על ה-API
  • פנימי: ביצוע מרחוק ושמירה במטמון
  • עקרונות בסיסיים: שימוש בפרטי שינוי עבור גרסאות build מהירות ומצטברות נדרשות תבניות קידוד חריגות
  • פנימי: קשה להימנע מצריכת זמן ריבועית ומזיכרון

הנחות

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

המטרה היא לנכונות, תפוקה, נוחות שימוש וזמן אחזור

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

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

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

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

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

מאגרים בקנה מידה גדול

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

שפת תיאור דמוי BOOLD

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

היסטוריים

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

ההפרדה קשה בין הטעינה, הניתוח וההוצאה משימוש היא מיושנת, אבל היא משפיעה על ה-API

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

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

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

פנימי

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

קשה לבצע הפעלה או לשמור במטמון

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

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

שימוש בפרטי שינוי עבור גרסאות build מהירות ומצטברות דורש תבניות קידוד חריגות

למעלה, טענו שכדי שיהיה נכון, בזל צריך לדעת את כל קובצי הקלט שעוברים לשלב build כדי לזהות אם השלב הזה עדיין עדכני. הדבר נכון גם לגבי טעינת חבילות וניתוח כללים, ועיצבנו את Skyframe כך שיוכל לטפל בנושא באופן כללי. Skyframe היא ספריית תרשימים ומסגרת הערכה שמשתמשת בצומת יעד (למשל 'build //foo with these options&#39); כחלק מהתהליך, המערכת של Skyframe קוראת חבילות, מנתחת כללים ומבצעת פעולות.

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

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

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

חשוב לשים לב שהטכנולוגיה הזו לא זמינה כרגע ב-APIs של כללים. במקום זאת, ה-API של הכללים עדיין מוגדר באמצעות הקונספטים הקודמים של טעינה, ניתוח והפעלה. עם זאת, מגבלה בסיסית היא שכל הגישה לצמתים אחרים צריכה לעבור דרך המסגרת כדי שניתן יהיה לעקוב אחר התלות המקבילה. ללא קשר לשפה שבה מוטמעת מערכת ה-build או שבה נכתבים הכללים (הם לא חייבים להיות זהים), כותבי הכללים לא יכולים להשתמש בספריות או בתבניות רגילות כדי לעקוף את Skyframe. עבור Java, הכוונה היא להימנע מ-Java.io.File, כמו גם מכל צורה של השתקפות, וגם כל ספרייה שעושה זאת. עדיין צריך להגדיר בצורה נכונה ל-Skyframe ספריות שתומכות בהזרקת תלות בממשקים ברמה נמוכה.

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

קשה להימנע מזמן רבעוני ומצריכת זיכרון

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

  1. שרשראות של כללי ספרייה – כדאי להתייחס לשרשרת של כללי ספרייה א' תלויים ב', תלוי ב', וכן הלאה. לאחר מכן, אנחנו רוצים לחשב נכס מסוים במהלך הסגירה העקיפה של הכללים האלה, כמו נתיב הריצה של Java או הפקודה של קישור C++ עבור כל ספרייה. אנחנו עשויים לבצע הטמעת רשימה רגילה. עם זאת, האפשרות הזו כבר כוללת צריכת זיכרון ריבועית: הספרייה הראשונה מכילה ערך אחד בנתיב class, השני ואחר כך, וכן 3, וכן יותר מסה"כ 1+2+3+...+N = O(N^2).

  2. כללים בינאריים בהתאם לאותם סוגי ספרייה – קחו בחשבון את המקרה שבו קבוצה בינארית שתלויה באותם כללי ספרייה – למשל, אם יש כמה כללי בדיקה שבודקים את אותו קוד ספרייה. נניח שמתוך N כללים, מחצית מהכללים הם כללים בינאריים וחצי הכללים האחרים של הספרייה. עכשיו עליכם לזכור שכל קובץ בינארי יוצר עותק של חלק מהנכסים המחושבים במהלך הסגירה העקיפה של כללי הספרייה, כמו נתיב הנתיב של זמן ריצה של Java או שורת הפקודה המקשר C+. לדוגמה, הפעולה יכולה להרחיב את הייצוג של שורת הפקודה של פעולת הקישור C++. N/2 עותקים של רכיבי N/2 הם זיכרון O(N^2).

שיעורים מותאמים אישית של אוספים כדי למנוע מורכבות ריבועית

שתי האפשרויות האלה משפיעות מאוד על Bazel, ולכן הצגנו קבוצה של מחלקות מותאמות אישית שדוחפות את המידע ביעילות על ידי מניעת ההעתקה בכל שלב. כמעט בכל מבני הנתונים האלה מוגדרות מאפיינים סמנטיים, לכן קראנו ל-depset (שנקרא גם NestedSet בהטמעה הפנימית). רוב השינויים שעשינו כדי להפחית את צריכת הזיכרון של Bazel&#39 בשנים האחרונות היו שינויים בשימוש במאגרים במקום בדברים שנעשה בהם שימוש בעבר.

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