स्काईफ़्रेम

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

डेटा मॉडल

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

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

आकलन

बिल्ड में, बिल्ड के अनुरोध को दिखाने वाले नोड का आकलन किया जाता है. हालांकि, ऐसा करने में हमें कई समस्याएं आ सकती हैं, क्योंकि इसमें लेगसी कोड का इस्तेमाल किया जाता है. सबसे पहले, इसका SkyFunction ढूंढा जाता है और टॉप-लेवल SkyKey की कुंजी के साथ इसे कॉल किया जाता है. इसके बाद, फ़ंक्शन उन नोड के आकलन का अनुरोध करता है जिनकी मदद से टॉप-लेवल नोड का आकलन किया जा सकता है. इससे अन्य फ़ंक्शन कॉल होते हैं. यह प्रोसेस तब तक चलती है, जब तक लीफ़ नोड तक नहीं पहुंचा जाता. लीफ़ नोड आम तौर पर, फ़ाइल सिस्टम में इनपुट फ़ाइलों को दिखाने वाले नोड होते हैं. आखिर में, हमें टॉप-लेवल 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 या इसी तरह के किसी अन्य मैकेनिज़्म का इस्तेमाल करके, बदली गई फ़ाइलों के बारे में जानकारी हासिल करके इसे बेहतर बनाया जा सकता है.

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

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

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

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

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

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

  • इंक्रीमेंटल लिंकिंग
  • जब किसी .jar में कोई .class फ़ाइल बदलती है, तो हम सैद्धांतिक तौर पर .jar फ़ाइल में बदलाव कर सकते हैं. इसके लिए, हमें इसे शुरू से फिर से बनाने की ज़रूरत नहीं है.

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

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

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

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

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