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

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

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

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

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

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

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

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

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

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

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

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

सैद्धांतिक मॉडल ऊपर बताया गया है.

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

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

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

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

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

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

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

सही जानकारी

जब हम रेंडरिंग सिस्टम की गड़बड़ियों के बारे में बात करते हैं, तो आम तौर पर हम उनके सही होने, उदाहरण के लिए: "ब्राउज़र 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();
}

इस तरह की समस्या को ठीक करने से, आम तौर पर परफ़ॉर्मेंस पर बुरा असर पड़ सकता है, (नीचे ज़्यादा अमान्य होने के बारे में बताया गया है) और सही सुझाव पाने के मामले में बहुत बारीकी से जांच की.

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

इस फ़र्क़ कोड में सुधार आम तौर पर आसान होते हैं, और इन स्वतंत्र ऑब्जेक्ट को बनाने की आसानी की वजह से, इनकी इकाई की जांच की जा सकती है.

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

ऊपर दिए गए उदाहरण के लिए अलग-अलग कोड यह है:

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 में ठीक कर दी गई है.

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

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

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

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

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

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

टू-पास लेआउट और परफ़ॉर्मेंस क्लिफ़ का उभरता हुआ

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

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

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

बॉक्स के दो सेट. पहला सेट माप पास में बॉक्स का आकार दिखाता है. दूसरे सेट में सभी लेआउट बराबर हैं.

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

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

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

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

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

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

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

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

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