हमने Chrome DevTools के स्टैक ट्रेस को 10 गुना ज़्यादा तेज़ी से कैसे बढ़ाया

Benedikt Meurer
Benedikt Meurer

वेब डेवलपर को अपने कोड को डीबग करते समय, परफ़ॉर्मेंस पर कम से कम असर पड़ने की उम्मीद होती है. हालांकि, यह ज़रूरी नहीं है कि सभी मामलों में ऐसा हो. C++ डेवलपर कभी नहीं चाहेगा कि उसके ऐप्लिकेशन का डीबग बिल्ड, प्रोडक्शन परफ़ॉर्मेंस तक पहुंच जाए. Chrome के शुरुआती सालों में, DevTools खोलने से ही पेज की परफ़ॉर्मेंस पर काफ़ी असर पड़ता था.

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

हालांकि, किसी भी डीबगर की तरह ही DevTools का ओवरहेड भी कम होना चाहिए. हाल ही में, हमें ऐसी रिपोर्ट की संख्या में काफ़ी बढ़ोतरी हुई है जिनमें बताया गया है कि कुछ मामलों में DevTools, ऐप्लिकेशन को इतना धीमा कर देता है कि उसे इस्तेमाल नहीं किया जा सकता. यहां chromium:1069425 रिपोर्ट की तुलना अगल-बगल में देखी जा सकती है. इससे पता चलता है कि DevTools को सिर्फ़ खोलने से, परफ़ॉर्मेंस पर कितना असर पड़ता है.

वीडियो में देखा जा सकता है कि वीडियो की स्पीड 5 से 10 गुना तक कम हो गई है. यह स्पीड स्वीकार नहीं की जा सकती. सबसे पहले, यह समझना ज़रूरी था कि पूरा समय कहां बीत जाता है और DevTools के खुले होने पर, ब्राउज़र की परफ़ॉर्मेंस इतनी खराब क्यों हो जाती है. Chrome रेंडरर प्रोसेस पर Linux perf का इस्तेमाल करने से, रेंडरर के पूरे एक्सीक्यूशन समय का यह बंटवारा पता चला:

Chrome रेंडरर को लागू करने में लगने वाला समय

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

तरीके का नाम अनुमान लगाना

सबसे हैरान करने वाली बात यह थी कि ज़्यादातर समय V8 में JSStackFrame::GetMethodName() फ़ंक्शन पर खर्च होता है. हालांकि, हमें पिछली जांच से पता था कि परफ़ॉर्मेंस से जुड़ी समस्याओं में JSStackFrame::GetMethodName() का कोई खास योगदान नहीं है. यह फ़ंक्शन, उन फ़्रेम के लिए मेथड का नाम कैलकुलेट करने की कोशिश करता है जिन्हें मेथड कॉल माना जाता है. ये ऐसे फ़्रेम होते हैं जो func() के बजाय obj.func() फ़ॉर्म के फ़ंक्शन कॉल को दिखाते हैं. कोड को एक नज़र से देखने पर पता चलता है कि यह ऑब्जेक्ट और उसकी प्रोटोटाइप चेन को पूरी तरह से ट्रैवर्स करके काम करता है. साथ ही,

  1. डेटा प्रॉपर्टी जिनका value, func क्लोज़र है या
  2. ऐक्सेसर प्रॉपर्टी, जहां get या set, func क्लोज़र के बराबर है.

हालांकि, यह शुल्क अपने-आप में कम नहीं है, लेकिन ऐसा नहीं लगता कि इसकी वजह से, वीडियो की परफ़ॉर्मेंस में इतनी ज़्यादा गिरावट आई है. इसलिए, हमने chromium:1069425 में बताए गए उदाहरण की जांच शुरू की. हमें पता चला कि स्टैक ट्रेस, असाइन किए गए टास्क के साथ-साथ classes.js से आने वाले लॉग मैसेज के लिए इकट्ठा किए गए थे. classes.js एक 10 एमबी की JavaScript फ़ाइल है. ध्यान से देखने पर पता चला कि यह मूल रूप से एक Java रनटाइम प्लस ऐप्लिकेशन कोड था, जिसे JavaScript में कंपाइल किया गया था. स्टैक ट्रेस में कई फ़्रेम थे, जिनमें ऑब्जेक्ट A पर तरीके लागू किए जा रहे थे. इसलिए, हमने सोचा कि यह समझना ज़रूरी है कि हम किस तरह के ऑब्जेक्ट के साथ काम कर रहे हैं.

किसी ऑब्जेक्ट के स्टैक ट्रेस

ऐसा लगता है कि Java से JavaScript कंपाइलर ने एक ऑब्जेक्ट जनरेट किया है, जिसमें 82,203 फ़ंक्शन हैं - यह साफ़ तौर पर दिलचस्प लग रहा था. इसके बाद, हम V8 के JSStackFrame::GetMethodName() पर वापस गए, ताकि यह समझा जा सके कि क्या कोई ऐसा लक्ष्य है जिसे हम वहां से चुन सकते हैं.

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

हमारे उदाहरण में, सभी फ़ंक्शन बिना नाम के हैं और इनमें "name" प्रॉपर्टी खाली हैं.

A.SDV = function() {
   // ...
};

पहली खोज से पता चला कि रिवर्स लुकअप को दो चरणों में बांटा गया था. यह प्रोटोटाइप चेन में मौजूद ऑब्जेक्ट और उसके लिए किया गया था:

  1. सभी एन्यूमेरेबल प्रॉपर्टी के नाम निकालें और
  2. हर नाम के लिए सामान्य प्रॉपर्टी लुकअप करें. साथ ही, यह जांच करें कि नतीजों में मिली प्रॉपर्टी वैल्यू, उस क्लोज़र से मेल खाती है या नहीं जिसे हम खोज रहे थे.

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

दूसरा नतीजा और भी दिलचस्प था. फ़ंक्शन तकनीकी तौर पर बिना नाम वाले फ़ंक्शन थे. हालांकि, V8 इंजन ने उनके लिए अनुमानित नाम रिकॉर्ड किया था. obj.foo = function() {...} फ़ॉर्म में असाइनमेंट की दाईं ओर दिखने वाले फ़ंक्शन लिटरल के लिए, V8 पार्सर "obj.foo" को फ़ंक्शन लिटरल के लिए अनुमानित नाम के तौर पर याद रखता है. इसलिए, हमारे मामले में इसका मतलब है कि हमारे पास प्रॉपर्टी का सही नाम नहीं था, लेकिन हमारे पास कुछ ऐसा था जो प्रॉपर्टी के नाम से मिलता-जुलता था: ऊपर दिए गए A.SDV = function() {...} उदाहरण के लिए, हमारे पास अनुमानित नाम के तौर पर "A.SDV" था. हमने आखिरी बिंदु को ढूंढकर, अनुमानित नाम से प्रॉपर्टी का नाम पता लगाया. इसके बाद, ऑब्जेक्ट पर प्रॉपर्टी "SDV" को ढूंढा. इससे, ज़्यादा समय लेने वाले पूरे ट्रैवर्स को एक प्रॉपर्टी लुकअप से बदलकर, ज़्यादातर मामलों में काम हो गया. ये दो सुधार, इस सीएल के हिस्से के तौर पर किए गए हैं. इनसे chromium:1069425 में बताए गए उदाहरण में, काम करने में लगने वाले समय में काफ़ी कमी आई है.

Error.stack

हम यहां बातचीत खत्म कर सकते थे. हालांकि, इसमें कुछ गड़बड़ी थी, क्योंकि DevTools कभी भी स्टैक फ़्रेम के लिए, मेथड के नाम का इस्तेमाल नहीं करता. असल में, C++ API में v8::StackFrame क्लास, मेथड के नाम को ऐक्सेस करने का तरीका भी नहीं दिखाती. इसलिए, हमें यह गलत लगा कि हम JSStackFrame::GetMethodName() को पहले कॉल करेंगे. इसके बजाय, हम सिर्फ़ JavaScript स्टैक ट्रेस एपीआई में, तरीके के नाम का इस्तेमाल (और उसे दिखाते हैं). इस इस्तेमाल को समझने के लिए, यह आसान उदाहरण देखें error-methodname.js:

function foo() {
    console.log((new Error).stack);
}

var object = {bar: foo};
object.bar();

यहां हमारे पास एक फ़ंक्शन foo है, जिसे object पर "bar" के नाम से इंस्टॉल किया गया है. Chromium में इस स्निपेट को चलाने पर, यह आउटपुट मिलता है:

Error
    at Object.foo [as bar] (error-methodname.js:2)
    at error-methodname.js:6

यहां हम मैथड के नाम का लुकअप देखते हैं: सबसे ऊपर मौजूद स्टैक फ़्रेम, bar नाम के मैथड की मदद से Object के किसी इंस्टेंस पर फ़ंक्शन foo को कॉल करने के लिए दिखाया गया है. इसलिए, स्टैंडर्ड error.stack प्रॉपर्टी, JSStackFrame::GetMethodName() का ज़्यादा इस्तेमाल करती है. असल में, परफ़ॉर्मेंस की जांच से भी पता चलता है कि हमारे बदलावों से चीज़ें काफ़ी तेज़ी से काम करने लगी हैं.

StackTrace माइक्रो मानदंडों को तेज़ करना

हालांकि, Chrome DevTools के विषय पर वापस आकर, यह बात सही नहीं लगती कि error.stack का इस्तेमाल न करने के बावजूद, मेथड का नाम कैलकुलेट किया जाता है. यहां कुछ इतिहास है जिससे हमें मदद मिलती है: आम तौर पर, V8 में ऊपर बताए गए दो अलग-अलग एपीआई (C++ v8::StackFrame एपीआई और JavaScript स्टैक ट्रेस एपीआई) के लिए, स्टैक ट्रेस को इकट्ठा करने और उसे दिखाने के लिए दो अलग-अलग तरीके थे. एक ही काम को दो अलग-अलग तरीकों से करने पर, गड़बड़ियां होने की संभावना बढ़ जाती है. साथ ही, अक्सर गड़बड़ियां और बग भी होते हैं. इसलिए, हमने 2018 के आखिर में एक प्रोजेक्ट शुरू किया, ताकि स्टैक ट्रेस कैप्चर करने के लिए एक ही तरीका अपनाया जा सके.

यह प्रोजेक्ट काफ़ी सफल रहा और स्टैक ट्रेस इकट्ठा करने से जुड़ी समस्याओं की संख्या काफ़ी कम हो गई. स्टैंडर्ड error.stack प्रॉपर्टी के ज़रिए दी गई ज़्यादातर जानकारी का हिसाब भी तब लगाया जाता था, जब उसकी ज़रूरत होती थी. हालांकि, रीफ़ैक्टर करने के दौरान, हमने v8::StackFrame ऑब्जेक्ट पर भी यही तरीका अपनाया. स्टैक फ़्रेम के बारे में सारी जानकारी, उस पर किसी भी तरीके को पहली बार लागू करने पर कैलकुलेट की जाती है.

इससे आम तौर पर परफ़ॉर्मेंस बेहतर होती है. हालांकि, माफ़ करें, यह Chromium और DevTools में इन C++ एपीआई ऑब्जेक्ट के इस्तेमाल के तरीके से कुछ अलग है. खास तौर पर, हमने एक नई v8::internal::StackFrameInfo क्लास शुरू की थी, जिसमें स्टैक फ़्रेम के बारे में सारी जानकारी होती है. यह जानकारी, v8::StackFrame या error.stack के ज़रिए एक्सपोज़ की जाती है. हम दोनों एपीआई से मिली जानकारी के सुपर-सेट का हिसाब हमेशा लगाते हैं. इसका मतलब है कि स्टैक फ़्रेम के बारे में कोई भी जानकारी मांगे जाने पर, हम v8::StackFrame (खास तौर पर, DevTools के लिए) के इस्तेमाल के लिए, तरीके का नाम भी कैलकुलेट करेंगे. ऐसा लगता है कि DevTools हमेशा सोर्स और स्क्रिप्ट की जानकारी का अनुरोध तुरंत करता है.

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

फ़ंक्शन के नाम

ऊपर बताए गए रीफ़ैक्टर करने के बाद, सिंबलाइज़ेशन (v8_inspector::V8Debugger::symbolize में बिताया गया समय) का ओवरहेड, पूरे प्रोग्राम को एक्ज़ीक्यूट करने में लगने वाले समय के 15% तक कम हो गया. साथ ही, हमें यह भी साफ़ तौर पर पता चल पाया कि DevTools में इस्तेमाल करने के लिए, स्टैक फ़्रेम को इकट्ठा और सिंबलाइज़ करते समय V8 कहां समय बर्बाद कर रहा था.

सिंबलाइज़ेशन की लागत

सबसे पहले, लाइन और कॉलम की संख्या का हिसाब लगाने के लिए कुल लागत का पता चला. यहां सबसे ज़्यादा समय, स्क्रिप्ट में वर्ण के ऑफ़सेट का हिसाब लगाने में लगता है. यह ऑफ़सेट, V8 से मिलने वाले बाइटकोड ऑफ़सेट पर आधारित होता है. ऊपर बताए गए रीफ़ैक्टर करने की वजह से, हमने दो बार ऑफ़सेट का हिसाब लगाया. पहला, लाइन नंबर का हिसाब लगाते समय और दूसरा, कॉलम नंबर का हिसाब लगाते समय. v8::internal::StackFrameInfo इंस्टेंस पर सोर्स पोज़िशन को कैश मेमोरी में सेव करने से, इस समस्या को तुरंत ठीक करने में मदद मिली. साथ ही, किसी भी प्रोफ़ाइल से v8::internal::StackFrameInfo::GetColumnNumber को पूरी तरह हटा दिया गया.

हमारे लिए सबसे दिलचस्प बात यह थी कि हमने जिन प्रोफ़ाइलों की जांच की उनमें v8::StackFrame::GetFunctionName काफ़ी ज़्यादा था. इस बारे में ज़्यादा जानने पर, हमें पता चला कि DevTools में स्टैक फ़्रेम में फ़ंक्शन के लिए दिखाए जाने वाले नाम का हिसाब लगाना, ज़रूरत से ज़्यादा महंगा था,

  1. सबसे पहले नॉन-स्टैंडर्ड "displayName" प्रॉपर्टी खोजें. अगर इससे कोई ऐसी डेटा प्रॉपर्टी मिलती है जिसकी वैल्यू स्ट्रिंग है, तो हम उसका इस्तेमाल करेंगे,
  2. अगर ऐसा नहीं होता है, तो स्टैंडर्ड "name" प्रॉपर्टी खोजने के लिए वापस जाएं और फिर से देखें कि क्या इससे कोई ऐसी डेटा प्रॉपर्टी मिलती है जिसकी वैल्यू स्ट्रिंग है,
  3. आखिर में, यह किसी ऐसे इंटरनल डीबग नेम पर वापस आ जाता है जिसे V8 पार्सर से अनुमान लगाया जाता है और फ़ंक्शन लिटरल पर सेव किया जाता है.

"displayName" प्रॉपर्टी को "name" प्रॉपर्टी के लिए, Function इंस्टेंस के तौर पर जोड़ा गया था, क्योंकि JavaScript में ये इंस्टेंस रीड-ओनली होते हैं और इन्हें कॉन्फ़िगर नहीं किया जा सकता. हालांकि, इसे कभी स्टैंडर्ड नहीं बनाया गया और इसका बड़े पैमाने पर इस्तेमाल नहीं किया गया. इसकी वजह यह है कि ब्राउज़र डेवलपर टूल में फ़ंक्शन के नाम का अनुमान लगाने की सुविधा जोड़ी गई है, जो 99.9% मामलों में काम करती है. इसके अलावा, ES2015 ने Function इंस्टेंस पर "name" प्रॉपर्टी को कॉन्फ़िगर करने लायक बनाया, जिससे किसी खास "displayName" प्रॉपर्टी की ज़रूरत पूरी तरह से खत्म हो गई. "displayName" के लिए नेगेटिव लुकअप की प्रोसेस काफ़ी महंगी है और ज़रूरी भी नहीं है. ES2015 को पांच साल से ज़्यादा पहले रिलीज़ किया गया था. इसलिए, हमने V8 (और DevTools) से नॉन-स्टैंडर्ड fn.displayName प्रॉपर्टी के लिए सहायता हटाने का फ़ैसला लिया है.

"displayName" के नेगेटिव लुकअप को हटाने के बाद, v8::StackFrame::GetFunctionName की आधी कीमत हटा दी गई. बाकी आधा हिस्सा, सामान्य "name" प्रॉपर्टी लुकअप में जाता है. सौभाग्य से, हमारे पास पहले से ही कुछ लॉजिक मौजूद थे, ताकि Function इंस्टेंस पर "name" प्रॉपर्टी के महंगे लुकअप से बचा जा सके. हमने कुछ समय पहले V8 में इसे पेश किया था, ताकि Function.prototype.bind() को ज़्यादा तेज़ बनाया जा सके. हमने ज़रूरी जांचों को पोर्ट किया है, ताकि हम सबसे पहले महंगी सामान्य लुकअप को छोड़ सकें. इस वजह से, v8::StackFrame::GetFunctionName अब उन सभी प्रोफ़ाइलों में नहीं दिखता जिन पर हमने विचार किया है.

नतीजा

ऊपर बताए गए सुधारों की मदद से, हमने स्टैक ट्रेस के मामले में DevTools के ओवरहेड को काफ़ी कम कर दिया है.

हम जानते हैं कि इसमें अब भी कई सुधार किए जा सकते हैं. उदाहरण के लिए, chromium:1077657 में बताई गई समस्या के मुताबिक, MutationObserver का इस्तेमाल करने पर अब भी ओवरहेड दिखता है. हालांकि, फ़िलहाल हमने इस समस्या को हल कर दिया है. आने वाले समय में, हम डीबगिंग की परफ़ॉर्मेंस को और बेहतर बनाने के लिए, इस पर फिर से काम कर सकते हैं.

झलक वाले चैनल डाउनलोड करना

Chrome कैनरी, डेवलपर या बीटा को अपने डिफ़ॉल्ट डेवलपमेंट ब्राउज़र के तौर पर इस्तेमाल करें. इन झलक वाले चैनलों की मदद से, आपको DevTools की नई सुविधाओं का ऐक्सेस मिलता है. साथ ही, इनसे आपको वेब प्लैटफ़ॉर्म के सबसे नए एपीआई की जांच करने में मदद मिलती है. इसके अलावा, इनकी मदद से उपयोगकर्ताओं से पहले ही अपनी साइट पर समस्याओं का पता लगाया जा सकता है!

Chrome DevTools की टीम से संपर्क करना

DevTools से जुड़ी नई सुविधाओं, अपडेट या किसी भी अन्य चीज़ के बारे में चर्चा करने के लिए, यहां दिए गए विकल्पों का इस्तेमाल करें.