स्काईफ़्रेम

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