स्काईफ़्रेम

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