स्काईफ़्रेम

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 इस बात की गारंटी दे सकता है कि नतीजे एक जैसे होंगे जैसे कि उन्हें क्रम से चलाया जाता था.

बढ़ोतरी

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

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

  • बॉटम-अप अमान्यता के दौरान, ग्राफ़ बनने और बदले गए इनपुट का सेट पता चलने के बाद, सभी नोड अमान्य हो जाते हैं. ये सभी नोड में, बाद में बदली गई फ़ाइलों पर निर्भर होते हैं. अगर हमें पता हो कि वही टॉप-लेवल नोड फिर से बनाया जाएगा, तो यह सबसे बेहतर विकल्प होता है. ध्यान दें कि बॉटम-अप अमान्यता के लिए, पिछले बिल्ड की सभी इनपुट फ़ाइलों पर 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 इस्तेमाल नहीं होंगे).