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

इस पेज पर, 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 को इनमें से किसी एक तरीके से पास करने से, कॉपी करने का व्यवहार शुरू हो जाता है. साथ ही, क्वाड्रैटिक मेमोरी की खपत फिर से शुरू हो जाती है.