स्काईफ़्रेम

Bazel का पैरलल इवैलुएशन और इंक्रीमेंटैलिटी मॉडल.

डेटा मॉडल

डेटा मॉडल में ये आइटम शामिल होते हैं:

  • SkyValue. इन्हें नोड भी कहा जाता है. SkyValues ऐसे ऑब्जेक्ट होते हैं जिनमें बिल्ड के दौरान तैयार किया गया सारा डेटा और बिल्ड के इनपुट शामिल होते हैं. इनमें बदलाव नहीं किया जा सकता. उदाहरण के लिए: इनपुट फ़ाइलें, आउटपुट फ़ाइलें, टारगेट, और कॉन्फ़िगर किए गए टारगेट.
  • SkyKey. यह SkyValue का छोटा और बदला न जा सकने वाला नाम होता है. उदाहरण के लिए, FILECONTENTS:/tmp/foo या PACKAGE://foo.
  • SkyFunction. यह नोड, कुंजियों और उन पर निर्भर नोड के आधार पर नोड बनाता है.
  • नोड ग्राफ़. नोड के बीच डिपेंडेंसी का संबंध दिखाने वाला डेटा स्ट्रक्चर.
  • Skyframe. Bazel, इंक्रीमेंटल इवैलुएशन फ़्रेमवर्क पर आधारित है. इसका कोड नेम.

आकलन

बिल्ड, उस नोड का आकलन करके बनाया जाता है जो बिल्ड के अनुरोध को दिखाता है.

सबसे पहले, Bazel टॉप-लेवल SkyKey की कुंजी से मेल खाने वाला SkyFunction ढूंढता है. इसके बाद, फ़ंक्शन उन नोड के आकलन का अनुरोध करता है जिनकी ज़रूरत उसे टॉप-लेवल नोड का आकलन करने के लिए होती है. इससे अन्य SkyFunction कॉल जनरेट होते हैं. यह प्रोसेस तब तक चलती है, जब तक लीफ़ नोड तक नहीं पहुंच जाता. आम तौर पर, लीफ़ नोड वे होते हैं जो फ़ाइल सिस्टम में इनपुट फ़ाइलों को दिखाते हैं. आखिर में, Bazel को टॉप-लेवल SkyValue की वैल्यू मिलती है. साथ ही, कुछ साइड इफ़ेक्ट (जैसे कि फ़ाइल सिस्टम में आउटपुट फ़ाइलें) और बिल्ड में शामिल नोड के बीच की डिपेंडेंसी का डायरेक्टेड एसाइक्लिक ग्राफ़ मिलता है.

अगर किसी SkyFunction को पहले से यह पता नहीं है कि उसे अपना काम करने के लिए किन-किन नोड की ज़रूरत है, तो वह कई बार SkyKeys का अनुरोध कर सकता है. इसका एक आसान उदाहरण यह है कि किसी इनपुट फ़ाइल नोड का आकलन किया जाता है, जो एक सिमलंक होता है: फ़ंक्शन फ़ाइल को पढ़ने की कोशिश करता है. उसे पता चलता है कि यह एक सिमलंक है. इसलिए, वह फ़ाइल सिस्टम नोड को फ़ेच करता है, जो सिमलंक के टारगेट को दिखाता है. हालांकि, यह खुद एक सिमलंक हो सकता है. ऐसे में, ओरिजनल फ़ंक्शन को भी अपना टारगेट फ़ेच करना होगा.

कोड में फ़ंक्शन को इंटरफ़ेस SkyFunction से दिखाया जाता है. साथ ही, इसे SkyFunction.Environment नाम के इंटरफ़ेस से सेवाएं मिलती हैं. फ़ंक्शन ये काम कर सकते हैं:

  • env.getValue को कॉल करके, किसी दूसरे नोड के आकलन का अनुरोध करें. अगर नोड उपलब्ध है, तो उसकी वैल्यू दिखाई जाती है. अगर नोड उपलब्ध नहीं है, तो null दिखाया जाता है. साथ ही, फ़ंक्शन से भी null दिखाने की उम्मीद की जाती है. दूसरे मामले में, डिपेंडेंट नोड का आकलन किया जाता है. इसके बाद, ओरिजनल नोड बिल्डर को फिर से शुरू किया जाता है. हालांकि, इस बार env.getValue कॉल से, null वैल्यू के अलावा कोई दूसरी वैल्यू मिलेगी.
  • env.getValues() को कॉल करके, कई अन्य नोड के आकलन का अनुरोध करें. यह भी वही काम करता है. हालांकि, इसमें डिपेंडेंट नोड का आकलन एक साथ किया जाता है.
  • इनके शुरू होने पर कैलकुलेशन करना
  • इनके साइड इफ़ेक्ट होते हैं. उदाहरण के लिए, फ़ाइल सिस्टम में फ़ाइलें लिखना. यह ध्यान रखना ज़रूरी है कि दो अलग-अलग फ़ंक्शन एक-दूसरे के काम में रुकावट न डालें. आम तौर पर, लिखने के साइड इफ़ेक्ट (जहां डेटा Bazel से बाहर जाता है) ठीक होते हैं. वहीं, पढ़ने के साइड इफ़ेक्ट (जहां डेटा बिना किसी रजिस्टर की गई डिपेंडेंसी के Bazel में आता है) ठीक नहीं होते, क्योंकि वे रजिस्टर नहीं की गई डिपेंडेंसी होते हैं. इस वजह से, वे इंक्रीमेंटल बिल्ड को गलत बना सकते हैं.

SkyFunction को सही तरीके से लागू करने पर, डेटा को किसी अन्य तरीके से ऐक्सेस नहीं किया जाता. इसके बजाय, सिर्फ़ डिपेंडेंसी का अनुरोध किया जाता है. जैसे, फ़ाइल सिस्टम को सीधे तौर पर पढ़कर. ऐसा इसलिए किया जाता है, क्योंकि इससे Bazel उस फ़ाइल पर डेटा डिपेंडेंसी रजिस्टर नहीं करता जिसे पढ़ा गया था. इस वजह से, इंक्रीमेंटल बिल्ड गलत होते हैं.

जब किसी फ़ंक्शन के पास अपना काम करने के लिए ज़रूरी डेटा होता है, तो उसे null के अलावा कोई ऐसी वैल्यू दिखानी चाहिए जिससे पता चले कि काम पूरा हो गया है.

इस आकलन की रणनीति के कई फ़ायदे हैं:

  • हर्मेटिकिटी. अगर फ़ंक्शन, सिर्फ़ अन्य नोड पर निर्भर रहकर इनपुट डेटा का अनुरोध करते हैं, तो Bazel यह गारंटी दे सकता है कि अगर इनपुट की स्थिति एक जैसी है, तो एक जैसा डेटा वापस मिलेगा. अगर सभी स्काई फ़ंक्शन डिटरमिनिस्टिक हैं, तो इसका मतलब है कि पूरा बिल्ड भी डिटरमिनिस्टिक होगा.
  • इंक्रीमेंटैलिटी को सही और सटीक तरीके से मेज़र करना. अगर सभी फ़ंक्शन का इनपुट डेटा रिकॉर्ड किया जाता है, तो Bazel सिर्फ़ उन नोड के सेट को अमान्य कर सकता है जिन्हें इनपुट डेटा में बदलाव होने पर अमान्य करना ज़रूरी है.
  • पैरललिज़्म. फ़ंक्शन, सिर्फ़ डिपेंडेंसी का अनुरोध करके एक-दूसरे के साथ इंटरैक्ट कर सकते हैं. इसलिए, एक-दूसरे पर निर्भर न रहने वाले फ़ंक्शन को एक साथ चलाया जा सकता है. साथ ही, Bazel यह गारंटी दे सकता है कि नतीजे वही होंगे जो उन्हें क्रम से चलाने पर मिलते.

बढ़ोतरी

फ़ंक्शन, सिर्फ़ दूसरे नोड पर निर्भर रहकर इनपुट डेटा को ऐक्सेस कर सकते हैं. इसलिए, Bazel इनपुट फ़ाइलों से लेकर आउटपुट फ़ाइलों तक, पूरा डेटा फ़्लो ग्राफ़ बना सकता है. साथ ही, इस जानकारी का इस्तेमाल करके सिर्फ़ उन नोड को फिर से बना सकता है जिन्हें फिर से बनाने की ज़रूरत है: बदले गए इनपुट फ़ाइलों के सेट का रिवर्स ट्रांज़िटिव क्लोज़र.

खास तौर पर, इंक्रीमेंटैलिटी की दो रणनीतियां होती हैं: बॉटम-अप और टॉप-डाउन. इनमें से कौनसा तरीका सबसे सही है, यह डिपेंडेंसी ग्राफ़ पर निर्भर करता है.

  • बॉटम-अप इनवैलिडेशन के दौरान, ग्राफ़ बनाने और बदले गए इनपुट का सेट पता चलने के बाद, उन सभी नोड को अमान्य कर दिया जाता है जो बदले गए फ़ाइलों पर ट्रांज़िटिव तरीके से निर्भर होते हैं. अगर एक ही टॉप-लेवल नोड को फिर से बनाया जाना है, तो यह सबसे सही तरीका है. ध्यान दें कि बॉटम-अप अमान्य करने की प्रोसेस में, पिछली बिल्ड की सभी इनपुट फ़ाइलों पर stat() चलाना ज़रूरी होता है. इससे यह पता चलता है कि उनमें बदलाव किया गया है या नहीं. inotify या इसी तरह के किसी अन्य तरीके का इस्तेमाल करके, बदली गई फ़ाइलों के बारे में जानकारी हासिल की जा सकती है. इससे इस प्रोसेस को बेहतर बनाया जा सकता है.

  • टॉप-डाउन अमान्य करने की प्रोसेस के दौरान, टॉप-लेवल नोड के ट्रांज़िटिव क्लोज़र की जांच की जाती है. इसके बाद, सिर्फ़ उन नोड को रखा जाता है जिनके ट्रांज़िटिव क्लोज़र में कोई गड़बड़ी नहीं होती. अगर नोड ग्राफ़ बड़ा है, लेकिन अगले बिल्ड के लिए सिर्फ़ इसका छोटा सबसेट चाहिए, तो यह बेहतर है: बॉटम-अप अमान्य करने की सुविधा, पहले बिल्ड के बड़े ग्राफ़ को अमान्य कर देगी. वहीं, टॉप-डाउन अमान्य करने की सुविधा, दूसरे बिल्ड के छोटे ग्राफ़ को अमान्य कर देगी.

Bazel, सिर्फ़ बॉटम-अप इनवैलिडेशन करता है.

ज़्यादा इंक्रीमेंटैलिटी पाने के लिए, Bazel बदलावों को कम करने की सुविधा का इस्तेमाल करता है: अगर किसी नोड को अमान्य कर दिया जाता है, लेकिन फिर से बनाने पर यह पता चलता है कि उसकी नई वैल्यू, पुरानी वैल्यू के बराबर है, तो इस नोड में बदलाव की वजह से अमान्य किए गए नोड को "फिर से चालू" कर दिया जाता है.

यह तब फ़ायदेमंद होता है, जब कोई व्यक्ति C++ फ़ाइल में किसी टिप्पणी में बदलाव करता है. उदाहरण के लिए, इससे जनरेट हुई .o फ़ाइल वही रहेगी. इसलिए, लिंकर को फिर से कॉल करने की ज़रूरत नहीं है.

इंक्रीमेंटल लिंकिंग / कंपाइलेशन

इस मॉडल की मुख्य सीमा यह है कि किसी नोड को अमान्य करने का मतलब है कि उसे पूरी तरह से अमान्य कर दिया गया है: जब कोई डिपेंडेंसी बदलती है, तो डिपेंडेंट नोड को हमेशा शुरू से फिर से बनाया जाता है. भले ही, कोई ऐसा बेहतर एल्गोरिदम मौजूद हो जो बदलावों के आधार पर नोड की पुरानी वैल्यू में बदलाव कर सके. कुछ उदाहरण जहां यह सुविधा काम आ सकती है:

  • इंक्रीमेंटल लिंकिंग
  • किसी JAR फ़ाइल में एक क्लास फ़ाइल बदलने पर, JAR फ़ाइल को फिर से बनाने के बजाय, उसे उसी जगह पर बदला जा सकता है.

Bazel इन चीज़ों को सिद्धांत के तौर पर क्यों नहीं अपनाता, इसकी दो वजहें हैं:

  • परफ़ॉर्मेंस में सीमित सुधार हुआ.
  • यह पुष्टि करना मुश्किल होगा कि म्यूटेशन का नतीजा, क्लीन रीबिल्ड के नतीजे के जैसा है. साथ ही, Google ऐसे बिल्ड को अहमियत देता है जिन्हें बिट-फ़ॉर-बिट दोहराया जा सकता है.

अब तक, किसी महंगे बिल्ड स्टेप को अलग-अलग हिस्सों में बांटकर, अच्छी परफ़ॉर्मेंस हासिल की जा सकती थी. इससे, कुछ हिस्सों का फिर से आकलन किया जा सकता था. उदाहरण के लिए, Android ऐप्लिकेशन में सभी क्लास को कई ग्रुप में बांटा जा सकता है और उन्हें अलग-अलग डेक्स किया जा सकता है. इस तरह, अगर किसी ग्रुप में मौजूद क्लास में कोई बदलाव नहीं किया जाता है, तो डेक्सिंग को फिर से नहीं करना पड़ता.

Bazel के कॉन्सेप्ट से मैपिंग करना

यहां मुख्य SkyFunction और SkyValue के बारे में खास जानकारी दी गई है. Bazel, बिल्ड करने के लिए इनका इस्तेमाल करता है:

  • FileStateValue. lstat() का नतीजा. मौजूदा फ़ाइलों के लिए, यह फ़ंक्शन अतिरिक्त जानकारी भी इकट्ठा करता है, ताकि फ़ाइल में हुए बदलावों का पता लगाया जा सके. यह Skyframe ग्राफ़ में सबसे निचला नोड है और इसकी कोई निर्भरता नहीं है.
  • FileValue. इसका इस्तेमाल उन सभी चीज़ों के लिए किया जाता है जिन्हें किसी फ़ाइल के असली कॉन्टेंट या हल किए गए पाथ की ज़रूरत होती है. यह इस बात पर निर्भर करता है कि FileStateValue और किसी भी ऐसे सिमलंक को हल करने की ज़रूरत है या नहीं (जैसे कि a/b के लिए FileValue को a और a/b के हल किए गए पाथ की ज़रूरत होती है). FileValue और FileStateValue के बीच अंतर करना ज़रूरी है, क्योंकि बाद वाले का इस्तेमाल उन मामलों में किया जा सकता है जहां फ़ाइल के कॉन्टेंट की ज़रूरत नहीं होती. उदाहरण के लिए, फ़ाइल सिस्टम ग्लोब (जैसे कि srcs=glob(["*/*.java"])) का आकलन करते समय, फ़ाइल के कॉन्टेंट काम के नहीं होते.
  • DirectoryListingStateValue. readdir() का नतीजा. FileStateValue की तरह, यह सबसे निचले लेवल का नोड है और यह किसी भी नोड पर निर्भर नहीं करता.
  • DirectoryListingValue. इसका इस्तेमाल डायरेक्ट्री की एंट्री के लिए किया जाता है. यह उससे जुड़े DirectoryListingStateValue के साथ-साथ डायरेक्ट्री के FileValue पर भी निर्भर करता है.
  • PackageValue. यह BUILD फ़ाइल के पार्स किए गए वर्शन को दिखाता है. यह FileValue फ़ाइल के FileValue पर निर्भर करता है. साथ ही, यह उस DirectoryListingValue पर भी निर्भर करता है जिसका इस्तेमाल पैकेज में ग्लोब को हल करने के लिए किया जाता है. पैकेज, BUILD फ़ाइल के कॉन्टेंट को अंदरूनी तौर पर दिखाने वाला डेटा स्ट्रक्चर होता है.BUILD
  • ConfiguredTargetValue. यह कॉन्फ़िगर किए गए टारगेट को दिखाता है. यह टारगेट के विश्लेषण के दौरान जनरेट की गई कार्रवाइयों के सेट और कॉन्फ़िगर किए गए डिपेंडेंट टारगेट को दी गई जानकारी का टपल होता है. यह PackageValue पर निर्भर करता है. साथ ही, यह ConfiguredTargetValues पर भी निर्भर करता है. इसके अलावा, यह बिल्ड कॉन्फ़िगरेशन को दिखाने वाले खास नोड पर भी निर्भर करता है.
  • ArtifactValue. यह बिल्ड में मौजूद किसी फ़ाइल को दिखाता है. यह फ़ाइल, सोर्स या आउटपुट आर्टफ़ैक्ट हो सकती है. आर्टफ़ैक्ट, फ़ाइलों के बराबर होते हैं. इनका इस्तेमाल, बिल्ड के चरणों को असल में लागू करने के दौरान फ़ाइलों को रेफ़र करने के लिए किया जाता है. सोर्स फ़ाइलें, उससे जुड़े नोड के FileValue पर निर्भर करती हैं. साथ ही, आउटपुट आर्टफ़ैक्ट, उस कार्रवाई के ActionExecutionValue पर निर्भर करते हैं जिससे आर्टफ़ैक्ट जनरेट होता है.
  • ActionExecutionValue. यह किसी कार्रवाई के पूरा होने की जानकारी देता है. यह इनपुट फ़ाइलों के ArtifactValues पर निर्भर करता है. यह कार्रवाई, SkyKey में शामिल है. यह इस सिद्धांत के उलट है कि SkyKey छोटे होने चाहिए. ध्यान दें कि अगर एक्ज़ीक्यूशन फ़ेज़ नहीं चलता है, तो ActionExecutionValue और ArtifactValue का इस्तेमाल नहीं किया जाता.

विज़ुअल एड के तौर पर, इस डायग्राम में Bazel के बिल्ड के बाद SkyFunction के लागू होने के बीच के संबंध दिखाए गए हैं:

SkyFunction को लागू करने के संबंध दिखाने वाला ग्राफ़