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

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

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

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

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

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

पहले, Blink के लेआउट ट्री को "बदलाव किया जा सकने वाला ट्री" कहा जाता था.

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

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

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

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

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

पहले बताया गया कॉन्सेप्ट मॉडल.

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

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

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

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

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

लेआउट से जुड़ी गड़बड़ियों के टाइप

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

सही होना

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

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

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

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

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

अमान्य होने की प्रक्रिया जारी है

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

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

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

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

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

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

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

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

तय चौड़ाई और प्रतिशत चौड़ाई वाली इमेज की तुलना करना.
अगर किसी एलिमेंट के लिए उपलब्ध साइज़ बढ़ाया जाता है, तो फ़िक्स चौड़ाई/ऊंचाई वाले एलिमेंट पर इसका कोई असर नहीं पड़ता. हालांकि, प्रतिशत के आधार पर चौड़ाई/ऊंचाई वाले एलिमेंट पर इसका असर पड़ता है. available-size को 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 में, हमारे पास साफ़ तौर पर इनपुट और आउटपुट डेटा-स्ट्रक्चर होते हैं. साथ ही, पिछली स्थिति को ऐक्सेस करने की अनुमति नहीं होती. इसलिए, हमने लेआउट सिस्टम से इस तरह के गड़बड़ी को कम कर दिया है.

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

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

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

दो पास वाले लेआउट और परफ़ॉर्मेंस में गिरावट

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

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

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

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

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

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

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

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

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

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

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

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

Una Kravets की एक इमेज (आपको पता है कि कौनसी!).