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

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

खास जानकारी देने के लिए ज़रूरी शर्तें

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

अनुमान

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

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

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

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

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

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

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

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

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

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

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

ऐतिहासिक

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

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

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

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

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

इंट्रिंसिक

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

रिमोट एक्ज़ीक्यूशन और कैश मेमोरी का इस्तेमाल करना मुश्किल है

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

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

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

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