रेंडर करने के बारे में बेहतर जानकारी: LayoutNG

इयान किलपैट्रिक
इयन किलपैट्रिक
कोजी इशी
कोजी इशी

मैं इयन किलपैट्रिक हूं, जो ब्लिंक लेआउट टीम में इंजीनियरिंग लीड और कोजी इशी भी हैं. Blink टीम में काम करने से पहले, मैं एक फ़्रंट-एंड इंजीनियर थी. (इससे पहले कि Google के पास "फ़्रंट-एंड इंजीनियर" का रोल था), Google Docs, Drive, और Gmail में सुविधाएं तैयार करना. उस भूमिका में करीब पांच साल बाद, मैंने Blink टीम में बड़ा दांव लगाया, काम में C++ के बारे में अच्छी तरह से सीखा, और बेहद मुश्किल Blink कोडबेस को बढ़ाने की कोशिश की. आज भी मुझे इसका बहुत कम हिस्सा समझ में आता है. इस दौरान मुझे दिए गए समय के लिए मैं शुक्रगुज़ार हूं. मुझे यह जानकर राहत मिली कि कई "फ़्रंट-एंड इंजीनियर को ठीक कर रहे हैं." उन्होंने मुझसे पहले एक "ब्राउज़र इंजीनियर" बनने की ज़िम्मेदारी संभाली.

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

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

लेआउट इंजन आर्किटेक्चर का 30,000 फ़ुट व्यू

इससे पहले, Blink के लेआउट ट्री को "म्यूटेबल ट्री" कहा जाता था.

नीचे दिए गए टेक्स्ट में बताया गया तरीका अपनाकर, ट्री दिखाता है.

लेआउट ट्री के हर ऑब्जेक्ट में इनपुट जानकारी होती है. जैसे, माता-पिता की ओर से तय किया गया उपलब्ध साइज़, फ़्लोट की पोज़िशन, और आउटपुट की जानकारी, जैसे कि ऑब्जेक्ट की आखिरी चौड़ाई और ऊंचाई या उसकी x और y पोज़िशन.

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

हमें पता चला है कि इस आर्किटेक्चर की वजह से कई तरह की समस्याएं हुईं. इनके बारे में हम नीचे बताएंगे. आइए, सबसे पहले वापस आते हैं और समझते हैं कि लेआउट के इनपुट और आउटपुट क्या हैं.

इस ट्री में नोड पर चलने वाला लेआउट "स्टाइल प्लस डीओएम" और पैरंट लेआउट सिस्टम (ग्रिड, ब्लॉक या फ़्लेक्स) से ली गई कोई भी पैरंट पाबंदी लेता है. यह लेआउट कंस्ट्रेंट एल्गोरिदम को चलाता है और एक नतीजा देता है.

सैद्धांतिक मॉडल, जिसकी जानकारी पहले दी गई है.

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

फ़्रैगमेंट ट्री.

मैंने पहले नहीं बदले जा सकने वाले फ़्रैगमेंट ट्री के बारे में जानकारी दी गई थी. इसमें यह बताया गया था कि इंक्रीमेंटल लेआउट के लिए, पिछले ट्री के बड़े हिस्से का फिर से इस्तेमाल करने के लिए, इसे कैसे डिज़ाइन किया गया है.

इसके अलावा, हम उस फ़्रैगमेंट को जनरेट करने वाले पैरंट कंस्ट्रेंट ऑब्जेक्ट को स्टोर करते हैं. हम इसका इस्तेमाल कैश मेमोरी के तौर पर करते हैं. इसके बारे में नीचे ज़्यादा जानकारी दी गई है.

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

लेआउट बग के प्रकार

आम तौर पर, बोलने वाले लेआउट की गड़बड़ियों को चार अलग-अलग कैटगरी में बांटा जाता है. हर गड़बड़ी की असल वजहें अलग-अलग होती हैं.

सुधार

जब हम रेंडरिंग सिस्टम में मौजूद गड़बड़ियों के बारे में सोचते हैं, तब हम आम तौर पर गड़बड़ियों को ठीक करने के बारे में सोचते हैं. उदाहरण के लिए: "ब्राउज़र A का X व्यवहार है, जबकि ब्राउज़र B का Y व्यवहार" या "ब्राउज़र A और B, दोनों में गड़बड़ी है". पहले हम इसी पर अपना बहुत ज़्यादा समय बिताते थे और इस प्रक्रिया में हम लगातार सिस्टम के साथ लड़ रहे थे. आम तौर पर, फ़ेलियर मोड में किसी एक गड़बड़ी को ठीक से लागू किया जाता था. हालांकि, कई हफ़्तों के बाद हमने पाया कि सिस्टम के किसी दूसरे हिस्से में रिग्रेशन हुआ है (ऐसा लग रहा है कि कोई गड़बड़ी नहीं है).

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

उदाहरण के लिए, एक समय पर हमारे पास एक साल से ज़्यादा समय के दौरान, फ़्लेक्स लेआउट से जुड़ी करीब 10 गड़बड़ियों की चेन थी. हर बार गड़बड़ी ठीक करने की वजह से सिस्टम के किसी हिस्से में गड़बड़ी ठीक हुई या परफ़ॉर्मेंस की समस्या आई जिससे एक और गड़बड़ी सामने आई.

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

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

अमान्य विज्ञापन

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

नीचे बताए गए टू-पास (आखिरी लेआउट की स्थिति तय करने के लिए लेआउट ट्री को दो बार चलाना) वाले लेआउट मोड के साथ ऐसा होना बहुत आम है. पहले हमारा कोड ऐसा दिखेगा:

if (/* some very complicated statement */) {
  child->ForceLayout();
}

आम तौर पर, इस तरह की गड़बड़ी को ठीक करने के लिए यह तरीका अपनाया जाता है:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

इस तरह की समस्या को ठीक करने से आम तौर पर परफ़ॉर्मेंस रिग्रेशन पर गंभीर असर पड़ सकता है, (यहां ज़्यादा अमान्यता को देखें). साथ ही, इसे ठीक करना बहुत नाज़ुक था.

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

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

इमेज की तय चौड़ाई और प्रतिशत में तुलना की जा रही है.
एक तय चौड़ाई/ऊंचाई वाले एलिमेंट से इस बात की परवाह नहीं होती कि उसे उपलब्ध कराया गया साइज़ बढ़ गया है या नहीं. हालांकि, चौड़ाई/ऊंचाई के प्रतिशत के हिसाब से, इससे फ़र्क़ नहीं पड़ता. उपलब्ध साइज़ को Parent Constraints ऑब्जेक्ट पर दिखाया जाता है. अलग-अलग एल्गोरिदम के हिस्से के तौर पर, यह ऑप्टिमाइज़ेशन करेगा.

ऊपर दिए गए उदाहरण के लिए, अलग-अलग कोड का इस्तेमाल यह हुआ है:

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

हिस्टेसिस

बग की यह क्लास 'अमान्य' कैटगरी के जैसी है. असल में, पिछले सिस्टम में यह पक्का करना बहुत मुश्किल था कि लेआउट एक ही तरह से काम का हो. इसका मतलब है कि एक ही इनपुट के साथ लेआउट को फिर से चलाने पर एक ही आउटपुट मिले.

नीचे दिए गए उदाहरण में, हम सीएसएस प्रॉपर्टी को दो वैल्यू के बीच स्विच कर रहे हैं. हालांकि, इसका नतीजा "अनिश्चित रूप से बढ़ने वाला" रेक्टैंगल होता है.

वीडियो और डेमो में, Chrome 92 और उससे पहले के वर्शन में हिस्टेसिस बग दिखाया गया है. यह समस्या Chrome 93 में ठीक कर दी गई है.

हमारे पिछले म्यूटेबल ट्री की मदद से, इस तरह की गड़बड़ियों को शामिल करना बहुत आसान था. अगर कोड से गलती से किसी ऑब्जेक्ट के साइज़ या जगह को गलत समय या स्टेज पर पढ़ लिया गया है (उदाहरण के लिए, हमने पिछले साइज़ या पोज़िशन को "साफ़" नहीं किया था), तो हमें तुरंत एक मामूली हिस्टेसिस बग जोड़ना होगा. आम तौर पर, ये गड़बड़ियां टेस्टिंग में नहीं दिखती हैं, क्योंकि ज़्यादातर टेस्ट एक ही लेआउट और रेंडर पर फ़ोकस करते हैं. हमें चिंता करने की बात यह थी कि कुछ लेआउट मोड को सही तरीके से काम करने के लिए, इन समस्याओं को हल करने की ज़रूरत थी. हमारे पास कुछ गड़बड़ियां थीं जिनमें हम लेआउट पास को हटाने के लिए ऑप्टिमाइज़ेशन करते थे, लेकिन "गड़बड़ी" पेश करते हैं, क्योंकि सही आउटपुट पाने के लिए लेआउट मोड में दो पास की ज़रूरत होती है.

पिछले टेक्स्ट में बताई गई समस्याओं को दिखाने वाला पेड़.
पिछले लेआउट के नतीजे की जानकारी के आधार पर, नतीजे ऐसे लेआउट होते हैं जो बिना काम के लेआउट में मिलते हैं

LayoutNG के साथ, क्योंकि हमारे पास इनपुट और आउटपुट डेटा स्ट्रक्चर होते हैं और पिछली स्थिति को ऐक्सेस करने की अनुमति नहीं होती, इसलिए हमने लेआउट सिस्टम से इस क्लास की गड़बड़ी को कम कर दिया है.

ज़्यादा अमान्यता और परफ़ॉर्मेंस

यह गड़बड़ी की अमान्य कैटगरी से बिलकुल उलट है. अक्सर अमान्य होने की गड़बड़ी को ठीक करते समय, हम परफ़ॉर्मेंस क्लिफ़ को ट्रिगर करते हैं.

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

टू-पास लेआउट और परफ़ॉर्मेंस क्लिफ़ का उदय

Flex और ग्रिड लेआउट ने वेब पर, लेआउट की एक्सप्रेशन में बदलाव को दिखाया है. हालांकि, ये एल्गोरिदम उनसे पहले के ब्लॉक लेआउट एल्गोरिदम से बुनियादी तौर पर अलग थे.

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

उदाहरण के लिए, आपको सभी बच्चों का साइज़ सबसे बड़ा करना है. इसमें मदद करने के लिए, पैरंट लेआउट (फ़्लेक्स या ग्रिड) एक मेज़रमेंट पास करेगा, जिससे यह पता चलेगा कि हर चाइल्ड कितना बड़ा है. इसके बाद, लेआउट पास की मदद से, सभी बच्चों को इस साइज़ तक पहुंचाया जाएगा. यह तरीका फ़्लेक्स और ग्रिड लेआउट, दोनों के लिए डिफ़ॉल्ट रूप से लागू होता है.

बॉक्स के दो सेट, पहले वाले में मेज़र पास में बॉक्स का असली साइज़ दिखाया जाता है. दूसरे सेट में, सभी बॉक्स की ऊंचाई बराबर होती है.

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

एक, दो, और तीन-पास के लेआउट के बारे में कैप्शन में बताया गया है.
ऊपर दी गई इमेज में, तीन <div> एलिमेंट दिए गए हैं. एक सामान्य वन-पास लेआउट (जैसे ब्लॉक लेआउट) तीन लेआउट नोड (जटिलता O(n)) पर जाएगा. हालांकि, फ़्लेक्सिबल या ग्रिड जैसे टू-पास लेआउट के मामले में, इस उदाहरण के लिए O(2n) विज़िट जटिल हो सकती है.
लेआउट समय में चरघातांकी वृद्धि दिखाने वाला ग्राफ़.
इस इमेज और डेमो में ग्रिड लेआउट के साथ एक्सपोनेन्शियल लेआउट दिखाया गया है. ग्रिड को नए आर्किटेक्चर पर ले जाने की वजह से, इस समस्या को Chrome 93 में ठीक कर दिया गया है

पहले हम परफ़ॉर्मेंस में इस तरह की बढ़ोतरी से निपटने के लिए, फ़्लेक्स और ग्रिड लेआउट में बहुत खास कैश मेमोरी जोड़ने की कोशिश करते थे. इससे कारगर साबित हुआ (और हम Flex के साथ आगे बढ़ गए), लेकिन हम लगातार अमान्य समस्याओं से लड़ रहे थे.

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

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

खास जानकारी में

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

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

यूना क्रावेट्स की एक इमेज (आपको पता है कौनसी!).