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

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

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

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

अनुमान

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

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

हम मानते हैं कि इंक्रीमेंटल बिल्ड के लिए, बिल्ड सिस्टम का सही होना सबसे ज़रूरी है. किसी सोर्स ट्री के लिए, एक ही बिल्ड का आउटपुट हमेशा एक जैसा होना चाहिए. भले ही, आउटपुट ट्री कैसा भी दिखे. पहले अनुमान के मुताबिक, इसका मतलब है कि Bazel को हर उस इनपुट के बारे में पता होना चाहिए जो किसी बिल्ड चरण में जाता है. इससे, अगर कोई भी इनपुट बदलता है, तो 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 को इनमें से किसी एक तरीके से पास करने पर, कॉपी करने का तरीका लागू हो जाता है. साथ ही, मेमोरी का इस्तेमाल फिर से काफ़ी बढ़ जाता है.