स्काईफ़्रेम

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 का इस्तेमाल नहीं किया जाता है.