हमने 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 से मिलने वाले लॉग मैसेज 10MiB 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 का इस्तेमाल न किया गया हो. यहां दिए गए इतिहास से हमें मदद मिली है: ऊपर बताए गए दो अलग-अलग एपीआई (C++ v8::StackFrame API और JavaScript स्टैक ट्रेस एपीआई) को इकट्ठा करने और दिखाने के लिए, V8 में दो अलग-अलग तरीके थे. एक ही काम को दो अलग-अलग तरीकों से करने पर, गड़बड़ियां होने की संभावना बढ़ जाती है. साथ ही, अक्सर गड़बड़ियां और बग भी होते हैं. इसलिए, हमने 2018 के आखिर में एक प्रोजेक्ट शुरू किया, ताकि स्टैक ट्रेस कैप्चर करने के लिए एक ही तरीका अपनाया जा सके.

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

इससे आम तौर पर परफ़ॉर्मेंस बेहतर होती है, लेकिन अफ़सोस है कि यह C++ API ऑब्जेक्ट, 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 कहां समय बर्बाद कर रहा था.

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

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

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

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

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

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

नतीजा

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

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

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

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

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

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

  • crbug.com पर जाकर, हमें सुझाव/राय दें या शिकायत करें. साथ ही, किसी सुविधा का अनुरोध करें.
  • DevTools में ज़्यादा विकल्प > सहायता > DevTools से जुड़ी समस्या की शिकायत करें का इस्तेमाल करके, DevTools से जुड़ी समस्या की शिकायत करें.
  • @ChromeDevTools पर ट्वीट करें.
  • DevTools YouTube वीडियो में नया क्या है या DevTools सलाह YouTube वीडियो पर टिप्पणी की जा सकती हैं.