स्काईफ़्रेम

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 इन चीज़ों को सही तरीके से सपोर्ट नहीं करता है. इसकी दो वजहें हैं: हमें परफ़ॉर्मेंस में सिर्फ़ सीमित फ़ायदे मिले और यह गारंटी देना मुश्किल था कि म्यूटेशन का नतीजा, क्लीन रीबिल्ड के नतीजे के जैसा ही होगा. साथ ही, Google ऐसे बिल्ड को अहमियत देता है जिन्हें बिट-फ़ॉर-बिट दोहराया जा सकता है. हम इंक्रीमेंटल लिंकिंग के लिए कुछ हद तक सहायता देते हैं, लेकिन इसे Skyframe में लागू नहीं किया गया है.

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

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 पर निर्भर करता है. आउटपुट आर्टफ़ैक्ट के लिए, यह उस कार्रवाई के FileValue पर निर्भर करता है जिससे आर्टफ़ैक्ट जनरेट होता है.ActionExecutionValue
  • ActionExecutionValue. यह किसी कार्रवाई के पूरा होने की जानकारी देता है. यह इनपुट फ़ाइलों के ArtifactValues पर निर्भर करता है. यह कार्रवाई, फ़िलहाल अपनी स्काई कुंजी में शामिल है. यह इस कॉन्सेप्ट के उलट है कि स्काई कुंजियां छोटी होनी चाहिए. हम इस अंतर को ठीक करने के लिए काम कर रहे हैं. ध्यान दें कि अगर हम Skyframe पर एक्ज़ीक्यूशन फ़ेज़ नहीं चलाते हैं, तो ActionExecutionValue और ArtifactValue का इस्तेमाल नहीं किया जाता है.