लिखने के नियमों की चुनौतियां

इस पेज पर, असरदार Bazel नियम लिखने से जुड़ी खास समस्याओं और चुनौतियों के बारे में खास जानकारी दी गई है.

खास जानकारी से जुड़ी ज़रूरी शर्तें

  • मान्यता: सही तरीके से काम करने, थ्रूपुट, इस्तेमाल में आसानी, और कम समय में नतीजे देने पर फ़ोकस करना
  • मान्यता: बड़े पैमाने पर रिपॉज़िटरी
  • मान्यता: BUILD जैसी जानकारी देने वाली भाषा
  • ऐतिहासिक: लोड करने, विश्लेषण करने, और एक्ज़ीक्यूशन के बीच का अंतर अब पुराना हो चुका है. हालांकि, इसका असर अब भी एपीआई पर पड़ता है
  • मूल: रिमोट एक्ज़ीक्यूशन और कैशिंग मुश्किल है
  • मूल: सही और तेज़ी से इंक्रीमेंटल बिल्ड के लिए, बदलाव की जानकारी का इस्तेमाल करने के लिए, कोडिंग के अलग-अलग पैटर्न की ज़रूरत होती है
  • मूल: क्वाड्रैटिक टाइम और मेमोरी की खपत से बचना मुश्किल है

मान्यताएं

यहां बिल्ड सिस्टम के बारे में कुछ मान्यताएं दी गई हैं. जैसे, सही तरीके से काम करने, इस्तेमाल में आसानी, थ्रूपुट, और बड़े पैमाने पर रिपॉज़िटरी की ज़रूरत. इन मान्यताओं के बारे में यहां बताया गया है. साथ ही, यह पक्का करने के लिए दिशा-निर्देश दिए गए हैं कि नियम असरदार तरीके से लिखे जाएं.

सही तरीके से काम करने, थ्रूपुट, इस्तेमाल में आसानी, और कम समय में नतीजे देने पर फ़ोकस करना

हम मानते हैं कि बिल्ड सिस्टम को सबसे पहले और सबसे अहम तौर पर, इंक्रीमेंटल बिल्ड के हिसाब से सही तरीके से काम करना चाहिए. किसी सोर्स ट्री के लिए, एक ही बिल्ड का आउटपुट हमेशा एक जैसा होना चाहिए. भले ही, आउटपुट ट्री कैसा भी दिखे. पहले अनुमान के मुताबिक, Bazel को हर उस इनपुट के बारे में पता होना चाहिए जो किसी बिल्ड स्टेप में जाता है, ताकि इनपुट में बदलाव होने पर, वह उस स्टेप को फिर से चला सके. Bazel के सही तरीके से काम करने की कुछ सीमाएं हैं, क्योंकि यह कुछ जानकारी लीक करता है. जैसे, बिल्ड की तारीख / समय. साथ ही, यह कुछ तरह के बदलावों को अनदेखा करता है. जैसे, फ़ाइल एट्रिब्यूट में किए गए बदलाव. सैंडबॉक्सिंग की मदद से, यह पक्का किया जाता है कि बिल्ड सही तरीके से काम करे. इसके लिए, यह बिना बताए गए इनपुट फ़ाइलों को पढ़ने से रोकता है. सिस्टम की मूल सीमाओं के अलावा, सही तरीके से काम करने से जुड़ी कुछ समस्याएं भी हैं. इनमें से ज़्यादातर समस्याएं, Fileset या C++ नियमों से जुड़ी हैं. ये दोनों ही मुश्किल समस्याएं हैं. हम इन्हें ठीक करने के लिए लंबे समय से काम कर रहे हैं.

बिल्ड सिस्टम का दूसरा लक्ष्य, ज़्यादा थ्रूपुट होना है. हम रिमोट एक्ज़ीक्यूशन सेवा के लिए, मौजूदा मशीन के आवंटन में क्या किया जा सकता है, इसकी सीमाओं को लगातार बढ़ा रहे हैं. अगर रिमोट एक्ज़ीक्यूशन सेवा ओवरलोड हो जाती है, तो कोई भी काम नहीं कर पाएगा.

इसके बाद, इस्तेमाल में आसानी आती है. रिमोट एक्ज़ीक्यूशन सेवा के एक जैसे (या मिलते-जुलते) फ़ुटप्रिंट वाले कई सही तरीकों में से, हम उस तरीके को चुनते हैं जिसका इस्तेमाल करना आसान हो.

लेटेंसी का मतलब है, बिल्ड शुरू करने से लेकर मनचाहा नतीजा मिलने तक लगने वाला समय. यह नतीजा, पास या फ़ेल हुए टेस्ट का टेस्ट लॉग हो सकता है या कोई गड़बड़ी का मैसेज हो सकता है. जैसे, BUILD फ़ाइल में टाइपो होना.

ध्यान दें कि ये लक्ष्य अक्सर एक-दूसरे से जुड़े होते हैं. लेटेंसी, रिमोट एक्ज़ीक्यूशन सेवा के थ्रूपुट पर निर्भर करती है. साथ ही, इस्तेमाल में आसानी के लिए, सही तरीके से काम करना ज़रूरी है.

बड़े पैमाने पर रिपॉज़िटरी

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

BUILD जैसी जानकारी देने वाली भाषा

इस संदर्भ में, हम एक कॉन्फ़िगरेशन भाषा को मानते हैं जो लाइब्रेरी और बाइनरी नियमों के एलान और उनकी इंटरडिपेंडेंसी में, BUILD फ़ाइलों से मिलती-जुलती है. BUILD फ़ाइलों को अलग-अलग पढ़ा और पार्स किया जा सकता है. साथ ही, हम सोर्स फ़ाइलों को देखने से भी बचते हैं. हालांकि, ऐसा तब किया जाता है, जब हम ऐसा कर सकते हैं. सोर्स फ़ाइलों को सिर्फ़ उनके मौजूद होने की जानकारी के लिए देखा जाता है.

ऐतिहासिक

Bazel के वर्शन में अंतर होता है. इससे समस्याएं होती हैं. इनमें से कुछ के बारे में यहां बताया गया है.

लोड करने, विश्लेषण करने, और एक्ज़ीक्यूशन के बीच का अंतर अब पुराना हो चुका है. हालांकि, इसका असर अब भी एपीआई पर पड़ता है

तकनीकी तौर पर, किसी नियम के लिए, किसी कार्रवाई को रिमोट एक्ज़ीक्यूशन पर भेजने से ठीक पहले, कार्रवाई की इनपुट और आउटपुट फ़ाइलों के बारे में जानना ज़रूरी है. हालांकि, Bazel के ओरिजनल कोडबेस में, पैकेज लोड करने, कॉन्फ़िगरेशन (असल में, कमांड-लाइन फ़्लैग) का इस्तेमाल करके नियमों का विश्लेषण करने, और उसके बाद ही कोई कार्रवाई करने के बीच का अंतर साफ़ तौर पर दिखता था. आज भी नियमों के एपीआई में यह अंतर दिखता है. भले ही, Bazel के कोर को इसकी ज़रूरत न हो. इसके बारे में ज़्यादा जानकारी यहां दी गई है.

इसका मतलब है कि नियमों के एपीआई के लिए, नियम इंटरफ़ेस (इसमें कौनसे एट्रिब्यूट हैं, एट्रिब्यूट के टाइप) की जानकारी देने वाला एलान ज़रूरी है. कुछ अपवाद हैं, जहां एपीआई, लोड करने के दौरान कस्टम कोड चलाने की अनुमति देता है. ऐसा, आउटपुट फ़ाइलों के इंप्लिसिट नाम और एट्रिब्यूट की इंप्लिसिट वैल्यू का हिसाब लगाने के लिए किया जाता है. उदाहरण के लिए, 'foo' नाम का java_library नियम, 'libfoo.jar' नाम का आउटपुट इंप्लिसिट तौर पर जनरेट करता है. इसे बिल्ड ग्राफ़ में मौजूद अन्य नियमों से रेफ़र किया जा सकता है.

इसके अलावा, किसी नियम का विश्लेषण, किसी भी सोर्स फ़ाइल को नहीं पढ़ सकता या किसी कार्रवाई के आउटपुट की जांच नहीं कर सकता. इसके बजाय, इसे बिल्ड स्टेप और आउटपुट फ़ाइल के नामों का पार्शियल डायरेक्टेड बाइपार्टाइट ग्राफ़ जनरेट करना होता है. यह ग्राफ़, सिर्फ़ नियम और उसकी डिपेंडेंसी से तय होता है.

मूल

कुछ मूल प्रॉपर्टी हैं, जिनकी वजह से नियम लिखना मुश्किल हो जाता है. इनमें से कुछ सबसे आम प्रॉपर्टी के बारे में यहां बताया गया है.

रिमोट एक्ज़ीक्यूशन और कैशिंग मुश्किल है

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

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

सही और तेज़ी से इंक्रीमेंटल बिल्ड के लिए, बदलाव की जानकारी का इस्तेमाल करने के लिए, कोडिंग के अलग-अलग पैटर्न की ज़रूरत होती है

ऊपर, हमने बताया कि सही तरीके से काम करने के लिए, Bazel को उन सभी इनपुट फ़ाइलों के बारे में पता होना चाहिए जो किसी बिल्ड स्टेप में जाती हैं. इससे यह पता लगाया जा सकता है कि वह बिल्ड स्टेप अब भी अप-टू-डेट है या नहीं. Skyframe Skyframe, ग्राफ़ लाइब्रेरी और आकलन फ़्रेमवर्क है. यह एक गोल नोड (जैसे, 'इन विकल्पों के साथ //foo बनाएं') लेता है और इसे इसके कॉम्पोनेंट में तोड़ता है. इसके बाद, इनका आकलन किया जाता है और इन्हें मिलाकर यह नतीजा मिलता है. इस प्रोसेस के तहत, Skyframe पैकेज पढ़ता है, नियमों का विश्लेषण करता है, और कार्रवाइयां करता है.

हर नोड पर, Skyframe इस बात पर नज़र रखता है कि किसी नोड ने अपना आउटपुट कंप्यूट करने के लिए किन नोड का इस्तेमाल किया. यह जानकारी, गोल नोड से लेकर इनपुट फ़ाइलों (जो Skyframe नोड भी हैं) तक होती है. मेमोरी में इस ग्राफ़ को साफ़ तौर पर दिखाने से, बिल्ड सिस्टम को यह पता लगाने में मदद मिलती है कि इनपुट फ़ाइल में किए गए किसी बदलाव (इनपुट फ़ाइल बनाने या मिटाने) से किन नोड पर असर पड़ता है. साथ ही, आउटपुट ट्री को उसकी तय की गई स्थिति में वापस लाने के लिए, कम से कम काम किया जाता है.

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

इसके बजाय, Bazel, फ़िक्स्ड साइज़ वाले थ्रेड पूल का इस्तेमाल करता है. हालांकि, इसका मतलब है कि अगर कोई नोड ऐसी डिपेंडेंसी का एलान करता है जो अब तक उपलब्ध नहीं है, तो हमें उस आकलन को रोकना पड़ सकता है और उसे फिर से शुरू करना पड़ सकता है. ऐसा तब किया जाता है, जब डिपेंडेंसी उपलब्ध हो जाती है. हो सकता है कि इसे किसी दूसरे थ्रेड में शुरू किया जाए. इससे यह भी पता चलता है कि नोड को ऐसा ज़्यादा नहीं करना चाहिए. कोई नोड, N डिपेंडेंसी का एलान क्रम से करता है, तो उसे N बार फिर से शुरू किया जा सकता है. इसमें O(N^2) समय लग सकता है. इसके बजाय, हमारा लक्ष्य है कि डिपेंडेंसी का एलान एक साथ किया जाए. इसके लिए, कभी-कभी कोड को फिर से व्यवस्थित करना पड़ता है या नोड को कई नोड में बांटना पड़ता है, ताकि फिर से शुरू करने की संख्या को सीमित किया जा सके.

ध्यान दें कि फ़िलहाल, यह टेक्नोलॉजी नियमों के एपीआई में उपलब्ध नहीं है. इसके बजाय, नियमों के एपीआई को अब भी लोड करने, विश्लेषण करने, और एक्ज़ीक्यूशन के चरणों की लेगसी कॉन्सेप्ट का इस्तेमाल करके तय किया जाता है. हालांकि, एक बुनियादी पाबंदी यह है कि अन्य नोड के सभी ऐक्सेस, फ़्रेमवर्क के ज़रिए होने चाहिए, ताकि वह उनसे जुड़ी डिपेंडेंसी को ट्रैक कर सके. बिल्ड सिस्टम को जिस भाषा में लागू किया गया है या नियमों को जिस भाषा में लिखा गया है (ज़रूरी नहीं कि ये दोनों भाषाएं एक ही हों), नियम के लेखक, Skyframe को बायपास करने वाली स्टैंडर्ड लाइब्रेरी या पैटर्न का इस्तेमाल नहीं कर सकते. Java के लिए, इसका मतलब है कि java.io.File के साथ-साथ, किसी भी तरह के रिफ़्लेक्शन और ऐसी किसी भी लाइब्रेरी का इस्तेमाल नहीं करना चाहिए जो इनमें से कोई भी काम करती हो. ऐसी लाइब्रेरी जिन्हें Skyframe के लिए, इन लो-लेवल इंटरफ़ेस के डिपेंडेंसी इंजेक्शन के लिए सही तरीके से सेट अप करना ज़रूरी है.

इससे यह साफ़ तौर पर पता चलता है कि नियम के लेखकों को सबसे पहले, पूरी भाषा के रनटाइम से दूर रखना चाहिए. ऐसे एपीआई के गलती से इस्तेमाल होने का खतरा बहुत ज़्यादा होता है. पिछली बार, Bazel में कई गड़बड़ियां, असुरक्षित एपीआई का इस्तेमाल करने वाले नियमों की वजह से हुई थीं. भले ही, इन नियमों को Bazel की टीम या अन्य डोमेन के विशेषज्ञों ने लिखा हो.

क्वाड्रैटिक टाइम और मेमोरी की खपत से बचना मुश्किल है

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

  1. लाइब्रेरी के नियमों की चेन - लाइब्रेरी के नियमों की चेन का उदाहरण देखें. इसमें नियम A, नियम B पर निर्भर करता है, नियम B, नियम C पर निर्भर करता है, और इसी तरह आगे भी. इसके बाद, हमें इन नियमों के ट्रांज़िटिव क्लोज़र पर कोई प्रॉपर्टी कंप्यूट करनी है. जैसे, Java रनटाइम क्लासपाथ या हर लाइब्रेरी के लिए C++ लिंकर कमांड. आम तौर पर, हम स्टैंडर्ड लिस्ट के लागू होने की प्रोसेस को अपना सकते हैं. हालांकि, इससे पहले से ही क्वाड्रैटिक मेमोरी की खपत होती है: पहली लाइब्रेरी में क्लासपाथ पर एक एंट्री होती है, दूसरी में दो, तीसरी में तीन, और इसी तरह आगे भी. कुल मिलाकर, 1+2+3+...+N = O(N^2) एंट्री होती हैं.

  2. एक ही लाइब्रेरी के नियमों पर निर्भर बाइनरी नियम - मान लीजिए कि बाइनरी के सेट, एक ही लाइब्रेरी के नियमों पर निर्भर करते हैं. जैसे, अगर आपके पास कई टेस्ट नियम हैं जो एक ही लाइब्रेरी कोड की जांच करते हैं. मान लीजिए कि N नियमों में से, आधे नियम बाइनरी नियम हैं और बाकी आधे लाइब्रेरी नियम हैं. अब मान लीजिए कि हर बाइनरी, लाइब्रेरी के नियमों के ट्रांज़िटिव क्लोज़र पर कंप्यूट की गई किसी प्रॉपर्टी की कॉपी बनाती है. जैसे, Java रनटाइम क्लासपाथ या C++ लिंकर कमांड लाइन. उदाहरण के लिए, यह C++ लिंक ऐक्शन के कमांड लाइन स्ट्रिंग के प्रतिनिधित्व को बड़ा कर सकता है. N/2 एलिमेंट की N/2 कॉपी, O(N^2) मेमोरी होती है.

क्वाड्रैटिक कॉम्प्लेक्सिटी से बचने के लिए, कलेक्शन की कस्टम क्लास

Bazel पर इन दोनों स्थितियों का बहुत ज़्यादा असर पड़ता है. इसलिए, हमने कलेक्शन की कस्टम क्लास का सेट पेश किया है. यह सेट, हर चरण में कॉपी करने से बचकर, मेमोरी में मौजूद जानकारी को असरदार तरीके से कंप्रेस करता है. इनमें से ज़्यादातर डेटा स्ट्रक्चर में सेट सिमैंटिक होती है. इसलिए, हमने इसे depset कहा है. इसे इंटरनल तौर पर लागू करने के दौरान, NestedSet के तौर पर भी जाना जाता है. पिछले कुछ सालों में, Bazel की मेमोरी की खपत को कम करने के लिए किए गए ज़्यादातर बदलाव, पहले इस्तेमाल किए जाने वाले किसी भी तरीके के बजाय, depsets का इस्तेमाल करने से जुड़े थे.

दुर्भाग्य से, depsets का इस्तेमाल करने से सभी समस्याएं अपने-आप हल नहीं होतीं. खास तौर पर, हर नियम में सिर्फ़ depset पर इटरेट करने से भी, क्वाड्रैटिक टाइम की खपत फिर से शुरू हो जाती है. इंटरनल तौर पर, NestedSets में सामान्य कलेक्शन क्लास के साथ इंटरऑपरेबिलिटी को आसान बनाने के लिए, कुछ हेल्पर तरीके भी होते हैं. दुर्भाग्य से, गलती से किसी NestedSet को इनमें से किसी एक तरीके से पास करने पर, कॉपी करने का व्यवहार शुरू हो जाता है. साथ ही, क्वाड्रैटिक मेमोरी की खपत फिर से शुरू हो जाती है.