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

बेनेडिक्ट मीरर
बेनेडिक्ट मीरर

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

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

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

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

Chrome रेंडरर की निष्पादन समय

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

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

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

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

हालांकि, यह बॉक्स अपने-आप में कम कीमत में नहीं लगता, लेकिन लग रहा है कि यह भी इस भयानक गिरावट को समझा नहीं जा सकता. इसलिए, हमने Chromium:1069425 में बताए गए उदाहरण के बारे में ज़्यादा जाना शुरू किया. हमें पता चला कि स्टैक ट्रेस, एसिंक्रोनस टास्क के साथ-साथ, 10MiB JavaScript फ़ाइल, classes.js से शुरू होने वाले लॉग मैसेज के लिए इकट्ठा किए गए थे. बारीकी से देखने पर पता चला कि यह मूल रूप से 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-10x तक तेज़ हो गया.

दूसरी खोज और भी दिलचस्प थी. हालांकि, फ़ंक्शन तकनीकी रूप से अनाम फ़ंक्शन थे, लेकिन 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 API और JavaScript स्टैक ट्रेस एपीआई) के लिए, स्टैक ट्रेस को इकट्ठा करने और उसे दिखाने के लिए दो अलग-अलग तरीके होते थे. करने के दो अलग-अलग तरीके (आम तौर पर) एक जैसे होने की वजह से अक्सर गड़बड़ियां होती हैं और गड़बड़ियां और गड़बड़ियां होती हैं. इसलिए, साल 2018 के आखिर में हमने एक प्रोजेक्ट शुरू किया, ताकि स्टैक ट्रेस को कैप्चर करने के लिए हर समस्या को हल किया जा सके.

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

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

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

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

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

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

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

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

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

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

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

नतीजा

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

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

झलक दिखाने वाले चैनलों को डाउनलोड करना

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

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

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

  • crbug.com के ज़रिए हमें कोई सुझाव या सुझाव सबमिट करें.
  • DevTools में ज़्यादा विकल्प   ज़्यादा दिखाएं   > सहायता > DevTools से जुड़ी समस्याओं की शिकायत करें का इस्तेमाल करके, DevTools से जुड़ी समस्या की शिकायत करें.
  • @ChromeDevTool पर ट्वीट करें.
  • हमारे DevTools YouTube वीडियो या DevTools सलाह YouTube वीडियो में नया क्या है पर टिप्पणी करें.