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