रूबी हमेशा अपने डेवलपर्स के लिए लाई जाने वाली उत्पादकता के लिए जाना जाता रहा है। सुरुचिपूर्ण सिंटैक्स, समृद्ध मेटा-प्रोग्रामिंग समर्थन आदि जैसी सुविधाओं के साथ जो कोड लिखते समय आपको उत्पादक बनाते हैं, इसका एक और गुप्त हथियार है जिसे TracePoint कहा जाता है जो आपको "डिबग" करने में तेज़ी ला सकता है।
इस पोस्ट में, मैं एक सरल उदाहरण का उपयोग करके डिबगिंग के बारे में 2 दिलचस्प तथ्य दिखाऊंगा जो मुझे पता चले:
- अधिकांश समय, बग को ढूंढना कठिन नहीं है, लेकिन यह समझना कि आपका प्रोग्राम विस्तार से कैसे काम करता है, कठिन है। एक बार जब आपके पास इसकी गहरी समझ होती है, तो आप आमतौर पर बग को तुरंत देख सकते हैं।
- अपने प्रोग्राम को मेथड कॉल स्तर तक देखना समय लेने वाला है, और हमारी डिबगिंग प्रक्रिया का प्रमुख बॉटलनेक है।
फिर, मैं आपको दिखाऊंगा कि TracePoint प्रोग्राम को "हमें बताकर" कि यह क्या कर रहा है, डिबगिंग के प्रति हमारे दृष्टिकोण को कैसे बदल सकता है।
डिबगिंग आपके प्रोग्राम और उसके डिज़ाइन को समझने के बारे में है
मान लें कि हमारे पास plus_1 नामक एक रूबी प्रोग्राम है और यह सही ढंग से काम नहीं कर रहा है। हम इसे कैसे डिबग करते हैं?
# plus_1.rb def plus_1(n) n + 2 end input = ARGV[0].to_i puts(plus_1(input))$ ruby plus_1.rb 1 3आदर्श रूप से, हमें बग को 3 चरणों में संबोधित करने में सक्षम होना चाहिए:
- डिज़ाइन से अपेक्षाएं सीखें
- वर्तमान कार्यान्वयन को समझें
- बग को ट्रेस करें
डिज़ाइन से अपेक्षाएं सीखना
यहां अपेक्षित व्यवहार क्या है? plus_1 को अपने तर्क में 1 जोड़ना चाहिए, जो कमांड लाइन से हमारा इनपुट है। लेकिन हम इसे "जानते" कैसे हैं?
वास्तविक दुनिया के मामले में, हम टेस्ट केस, दस्तावेज़, मॉकअप पढ़कर, अन्य लोगों से प्रतिक्रिया मांगकर अपेक्षाओं को समझ सकते हैं, आदि। हमारी समझ इस बात पर निर्भर करती है कि प्रोग्राम कैसे "डिज़ाइन" किया गया है।
यह चरण हमारी डिबगिंग प्रक्रिया का सबसे महत्वपूर्ण हिस्सा है। यदि आप नहीं समझते कि प्रोग्राम को कैसे काम करना चाहिए, तो आप कभी भी इसे डिबग नहीं कर पाएंगे।
हालांकि, कई कारक हैं जो इस चरण का हिस्सा हो सकते हैं, जैसे टीम समन्वय, विकास वर्कफ़्लो, आदि। TracePoint उनमें आपकी मदद नहीं कर पाएगा, इसलिए हम आज इन समस्याओं पर ध्यान नहीं देंगे।
वर्तमान कार्यान्वयन को समझना
एक बार जब हम प्रोग्राम के अपेक्षित व्यवहार को समझ लेते हैं, तो हमें यह सीखना होगा कि यह वर्तमान में कैसे कार्य करता है।
अधिकांश मामलों में, हमें पूरी तरह से समझने के लिए निम्नलिखित जानकारी की आवश्यकता होती है कि प्रोग्राम कैसे काम करता है:
- प्रोग्राम के निष्पादन के दौरान कॉल किए गए मेथड्स
- मेथड कॉल का कॉल और रिटर्न ऑर्डर
- प्रत्येक मेथड कॉल को पास किए गए आर्ग्यूमेंट्स
- प्रत्येक मेथड कॉल से लौटाए गए मान
- प्रत्येक मेथड कॉल के दौरान होने वाले कोई भी साइड इफेक्ट्स, जैसे डेटा म्यूटेशन या डेटाबेस अनुरोध
आइए उपरोक्त जानकारी के साथ हमारे उदाहरण का वर्णन करें:
# plus_1.rb def plus_1(n) n + 2 end input = ARGV[0].to_i puts(plus_1(input))$ ruby plus_1.rb 1 3plus_1नामक एक मेथड को परिभाषित करता हैARGVसे इनपुट ("1") प्राप्त करता है"1"परto_iकॉल करता है, जो1लौटाता है1को लोकल वेरिएबलinputमें असाइन करता हैplus_1मेथड कोinput(1) को इसके आर्ग्यूमेंट के रूप में कॉल करता है। पैरामीटरnअब1मान रखता है1पर+मेथड को आर्ग्यूमेंट2के साथ कॉल करता है, और परिणाम3लौटाता है- चरण 5 के लिए
3लौटाता है putsकॉल करता है3परto_sकॉल करता है, जो"3"लौटाता हैputsकॉल को चरण 8 से"3"पास करता है, जो एक साइड इफेक्ट को ट्रिगर करता है जो स्ट्रिंग को Stdout पर प्रिंट करता है। फिर यहnilलौटाता है।
विवरण 100% सटीक नहीं है, लेकिन सरल व्याख्या के लिए पर्याप्त है।
बग को संबोधित करना
अब जब हमने सीख लिया है कि हमारा प्रोग्राम कैसे काम करना चाहिए और यह वास्तव में कैसे काम करता है, तो हम बग की तलाश शुरू कर सकते हैं। हमारे पास मौजूद जानकारी के साथ, हम मेथड कॉल्स को ऊपर की ओर (चरण 10 से शुरू) या नीचे की ओर (चरण 1 से शुरू) ट्रेस करके बग की खोज कर सकते हैं। इस मामले में, हम इसे उस मेथड पर वापस ट्रेस करके कर सकते हैं जिसने पहली बार में 3 लौटाया था—जो step 6 में 1 + 2 है।
यह वास्तविकता से बहुत दूर है!
बेशक, हम सभी जानते हैं कि वास्तविक डिबगिंग उतना सरल नहीं है जितना उदाहरण दिखाता है। वास्तविक जीवन के प्रोग्राम और हमारे उदाहरण के बीच महत्वपूर्ण अंतर आकार है। हमने 5-लाइन प्रोग्राम को समझाने के लिए 10 चरणों का उपयोग किया। एक छोटे रेल्स ऐप के लिए हमें कितने चरणों की आवश्यकता होगी? मूल रूप से एक वास्तविक प्रोग्राम को उतना विस्तृत रूप से तोड़ना असंभव है जितना हमने उदाहरण के लिए किया था। अपने प्रोग्राम की विस्तृत समझ के बिना, आप स्पष्ट रास्ते के माध्यम से बग को ट्रैक नहीं कर पाएंगे, इसलिए आपको धारणाएं या अनुमान लगाने होंगे।
जानकारी महंगी है
जैसा कि आप शायद पहले ही नोटिस कर चुके हैं, डिबगिंग में मुख्य कारक यह है कि आपके पास कितनी जानकारी है। लेकिन उस जानकारी को प्राप्त करने में क्या लगता है? आइए देखें:
# plus_1_with_tracing.rb def plus_1(n) puts("n = #{n}") n + 2 end raw_input = ARGV[0] puts("raw_input: #{raw_input}") input = raw_input.to_i puts("input: #{input}") result = plus_1(input) puts("result of plus_1 #{result}") puts(result)$ ruby plus_1_with_tracing.rb 1 raw_input: 1 input: 1 n = 1 result of plus_1: 3 3 जैसा कि आप देख सकते हैं, हमें यहां केवल 2 प्रकार की जानकारी मिलती है: कुछ वेरिएबल्स के मान और हमारे puts का मूल्यांकन क्रम (जो प्रोग्राम के निष्पादन क्रम को दर्शाता है)।
इस जानकारी की लागत कितनी है?
def plus_1(n) + puts("n = #{n}") n + 2 end -input = ARGV[0].to_i -puts(plus_1(input)) +raw_input = ARGV[0] +puts("raw_input: #{raw_input}") +input = raw_input.to_i +puts("input: #{input}") + +result = plus_1(input) +puts("result of plus_1: #{result}") + +puts(result)न केवल हमें कोड में 4 puts जोड़ने की आवश्यकता है, बल्कि, मानों को अलग से प्रिंट करने के लिए, हमें अपने तर्क को विभाजित करने की भी आवश्यकता है ताकि कुछ मानों की मध्यवर्ती स्थितियों तक पहुंचा जा सके। इस मामले में, हमें आंतरिक स्थितियों के लिए 4 अतिरिक्त आउटपुट मिले 8 लाइनों के परिवर्तनों के साथ। औसतन 1 लाइन आउटपुट के लिए 2 लाइनों के परिवर्तन! और चूंकि परिवर्तनों की संख्या प्रोग्राम के आकार के साथ रैखिक रूप से बढ़ती है, हम इसकी तुलना O(n) ऑपरेशन से कर सकते हैं।
डिबगिंग महंगी क्यों है?
हमारे प्रोग्राम कई लक्ष्यों को ध्यान में रखकर लिखे जा सकते हैं: रखरखाव, प्रदर्शन, सरलता, आदि। लेकिन आमतौर पर "ट्रेसेबिलिटी" के लिए नहीं, जिसका अर्थ है, निरीक्षण के लिए मान प्राप्त करना, जिसके लिए आमतौर पर कोड के संशोधन की आवश्यकता होती है, जैसे चेन किए गए मेथड कॉल को विभाजित करना।
- आपको जितनी अधिक जानकारी मिलती है, कोड में उतने ही अधिक जोड़/परिवर्तन आपको करने पड़ते हैं।
हालांकि, एक बार जब आपके द्वारा प्राप्त जानकारी की मात्रा एक निश्चित बिंदु पर पहुंच जाती है, तो आप इसे कुशलतापूर्वक संसाधित नहीं कर पाएंगे। इसलिए हमें या तो जानकारी को फ़िल्टर करना होगा या इसे लेबल करना होगा ताकि हमें इसे समझने में मदद मिल सके।
- जानकारी जितनी अधिक सटीक होगी, कोड में उतने ही अधिक जोड़/परिवर्तन आपको करने पड़ेंगे।
अंत में, क्योंकि काम कोडबेस को छूने से जुड़ा होता है—जो बग्स के बीच बहुत अलग हो सकता है (जैसे कंट्रोलर बनाम मॉडल तर्क)—इसे स्वचालित करना कठिन है। भले ही आपका कोडबेस ट्रेसिंग-फ्रेंडली हो (जैसे यह "डिमेटर का नियम" सख्ती से पालन करता है), अधिकांश समय, आपको अलग-अलग वेरिएबल/मेथड नाम मैन्युअल रूप से टाइप करने होंगे।
(वास्तव में, रूबी में इससे बचने के कुछ ट्रिक्स हैं—जैसे __method__। लेकिन आइए यहां चीजों को जटिल न करें।)
TracePoint: द सेवियर
हालांकि, रूबी हमें एक असाधारण उपकरण प्रदान करता है जो लागत को काफी हद तक कम कर सकता है: TracePoint. मुझे यकीन है कि आप में से अधिकांश ने इसके बारे में सुना है या इसे पहले इस्तेमाल किया है। लेकिन मेरे अनुभव में, कई लोग इस शक्तिशाली उपकरण का दैनिक डिबगिंग प्रथाओं में उपयोग नहीं करते हैं।
मैं आपको दिखाऊंगा कि इसका उपयोग जानकारी जल्दी से एकत्र करने के लिए कैसे करें। इस बार, हमें अपने किसी भी मौजूदा तर्क को छूने की आवश्यकता नहीं है, हमें बस इसके पहले कुछ कोड की आवश्यकता है:
TracePoint.trace(:call, :return, :c_call, :c_return) do |tp| event = tp.event.to_s.sub(/(.+(call|return))/, '\2').rjust(6, " ") message = "#{event} of #{tp.defined_class}##{tp.callee_id} on #{tp.self.inspect}" # यदि आप किसी नॉन-रिटर्न इवेंट पर `return` कॉल करते हैं, तो यह त्रुटि उठाएगा message += " => #{tp.return_value.inspect}" if tp.event == :return || tp.event == :c_return puts(message) end def plus_1(n) n + 2 end input = ARGV[0].to_i puts(plus_1(input))यदि आप कोड चलाते हैं, तो आप देखेंगे:
return of #<Class:TracePoint>#trace on TracePoint => #<TracePoint:c_return `trace'@plus_1_with_trace_point.rb:1> call of Module#method_added on Object return of Module#method_added on Object => nil call of String#to_i on "1" return of String#to_i on "1" => 1 call of Object#plus_1 on main return of Object#plus_1 on main => 3 call of Kernel#puts on main call of IO#puts on #<IO:<STDOUT>> call of Integer#to_s on 3 return of Integer#to_s on 3 => "3" call of IO#write on #<IO:<STDOUT>> 3 return of IO#write on #<IO:<STDOUT>> => 2 return of IO#puts on #<IO:<STDOUT>> => nil return of Kernel#puts on main => nil हमारा कोड अब बहुत अधिक पठनीय है। क्या यह अद्भुत नहीं है? यह बहुत सारे विवरणों के साथ प्रोग्राम निष्पादन के अधिकांश को प्रिंट करता है! हम इसे मेरे पहले के निष्पादन ब्रेकडाउन के साथ मैप भी कर सकते हैं:
plus_1नामक एक मेथड को परिभाषित करता हैARGVसे इनपुट ("1") प्राप्त करता है"1"परto_iकॉल करता है, जो1लौटाता है1को लोकल वेरिएबलinputमें असाइन करता हैplus_1मेथड कोinput(1) को इसके आर्ग्यूमेंट के रूप में कॉल करता है। पैरामीटरnअब1मान रखता है1पर+मेथड को आर्ग्यूमेंट2के साथ कॉल करता है, और परिणाम3लौटाता है- चरण 5 के लिए
3लौटाता है putsकॉल करता है3परto_sकॉल करता है, जो"3"लौटाता हैputsकॉल को चरण 8 से"3"पास करता है, जो एक साइड इफेक्ट को ट्रिगर करता है जो स्ट्रिंग को Stdout पर प्रिंट करता है। और फिर यहnilलौटाता है।
# इसे अनदेखा करें, यह TracePoint खुद को ट्रेस कर रहा है ;D return of #<Class:TracePoint>#trace on TracePoint => #<TracePoint:c_return `trace'@plus_1_with_trace_point.rb:1> call of Module#method_added on Object # 1. `plus_1` नामक एक मेथड को परिभाषित करता है। return of Module#method_added on Object => nil call of String#to_i on "1" # 3-1. `"1"` पर `to_i` कॉल करता है return of String#to_i on "1" => 1 # 3-2. जो `1` लौटाता है call of Object#plus_1 on main # 5. `plus_1` मेथड को `input`(`1`) को इसके आर्ग्यूमेंट के रूप में कॉल करता है। return of Object#plus_1 on main => 3 # 7. चरण 5 के लिए `3` लौटाता है call of Kernel#puts on main # 8. `puts` कॉल करता है call of IO#puts on #<IO:<STDOUT>> call of Integer#to_s on 3 # 9. `3` पर `to_s` कॉल करता है, जो `"3"` लौटाता है return of Integer#to_s on 3 => "3" call of IO#write on #<IO:<STDOUT>> # 10-1. चरण 8 से `puts` कॉल को `"3"` पास करता है # 10-2. जो Stdout पर स्ट्रिंग प्रिंट करने का साइड इफेक्ट ट्रिगर करता है 3 # मूल आउटपुट return of IO#write on #<IO:<STDOUT>> => 2 return of IO#puts on #<IO:<STDOUT>> => nil return of Kernel#puts on main => nil # 10-3. और फिर यह `nil` लौटाता है। हम यह भी कह सकते हैं कि यह मेरे पहले के कथन से अधिक विस्तृत है! हालांकि, आप ध्यान दे सकते हैं कि चरण 2, 4 और 6 आउटपुट से गायब हैं। दुर्भाग्य से, वे निम्नलिखित कारणों से TracePoint द्वारा ट्रैक करने योग्य नहीं हैं:
ARGVसे इनपुट ("1") प्राप्त करता है
ARGVऔर निम्नलिखित[]वर्तमान में कॉल/सी_कॉल नहीं माने जाते हैं
1को लोकल वेरिएबलinputमें असाइन करता है
- वर्तमान में, वेरिएबल असाइनमेंट के लिए कोई इवेंट नहीं है। हम (किसी तरह) इसे
lineइवेंट + रेगेक्स के साथ ट्रैक कर सकते हैं, लेकिन यह सटीक नहीं होगा
1पर+मेथड को आर्ग्यूमेंट2के साथ कॉल करता है, और परिणाम3लौटाता है
- कुछ मेथड कॉल जैसे बिल्ट-इन
+या एट्रिब्यूट एक्सेसर मेथड्स वर्तमान में ट्रैक करने योग्य नहीं हैं
O(n) से O(log n) तक
पिछले उदाहरण से आप देख सकते हैं, TracePoint के उचित उपयोग के साथ, हम लगभग प्रोग्राम को "हमें बता सकते हैं" कि यह क्या कर रहा है। अब, क्योंकि हमें जितनी लाइनों की आवश्यकता है, TracePoint हमारे प्रोग्राम के आकार के साथ रैखिक रूप से नहीं बढ़ता है। मैं कहूंगा कि पूरी प्रक्रिया O(log(n)) ऑपरेशन बन जाती है।
अगले कदम
इस लेख में, मैंने डिबगिंग के साथ मुख्य कठिनाई समझाई है। उम्मीद है, मैंने आपको यह भी समझाया है कि TracePoint गेम-चेंजर कैसे हो सकता है। लेकिन अगर आप अभी TracePoint आज़माते हैं, तो यह शायद आपको मदद करने से ज्यादा निराश करेगा।
TracePoint से आने वाली जानकारी की मात्रा के साथ, आप जल्द ही शोर से भर जाएंगे। नई चुनौती शोर को फ़िल्टर करना है, मूल्यवान जानकारी छोड़कर। उदाहरण के लिए, अधिकांश मामलों में, हम केवल विशिष्ट मॉडल या सर्विस ऑब्जेक्ट्स की परवाह करते हैं। इन मामलों में, हम रिसीवर की क्लास द्वारा कॉल्स को फ़िल्टर कर सकते हैं, जैसे:
TracePoint.trace(:call) do |tp| next unless tp.self.is_a?(Order) # ट्रेसिंग तर्क endध्यान रखने वाली एक और बात यह है कि TracePoint के लिए आपके द्वारा परिभाषित ब्लॉक का मूल्यांकन दसियों हज़ार बार किया जा सकता है। इस पैमाने पर, आप फ़िल्टरिंग तर्क को कैसे लागू करते हैं, इसका आपके ऐप के प्रदर्शन पर बहुत प्रभाव पड़ सकता है। उदाहरण के लिए, मैं इसकी अनुशंसा नहीं करता:
TracePoint.trace(:call) do |tp| trace = caller[0] next unless trace.match?("app") # ट्रेसिंग तर्क endइन 2 समस्याओं के लिए, मैंने एक और लेख तैयार किया है ताकि आप कुछ ट्रिक्स और गॉटचा जान सकें जो मुझे कुछ उपयोगी बॉयलरप्लेट्स के साथ मिले हैं जो सामान्य रूबी/रेल्स एप्लिकेशन के लिए हैं।
और यदि आप इस अवधारणा को दिलचस्प पाते हैं, तो मैंने tapping_device नामक एक जेम भी बनाया है जो सभी कार्यान्वयन परेशानियों को छुपाता है।
निष्कर्ष
डिबगर और ट्रेसिंग दोनों डिबगिंग के लिए बेहतरीन उपकरण हैं, और हम वर्षों से उनका उपयोग कर रहे हैं। लेकिन जैसा कि मैंने इस लेख में दिखाया है, उनका उपयोग करने के लिए डिबगिंग प्रक्रिया के दौरान कई मैन्युअल ऑपरेशंस की आवश्यकता होती है। हालांकि, TracePoint की मदद से, आप उनमें से कई को स्वचालित कर सकते हैं और इस प्रकार अपने डिबगिंग प्रदर्शन को बढ़ा सकते हैं। मुझे आशा है कि अब आप TracePoint को अपने डिबगिंग टूलबॉक्स में जोड़ सकते हैं और इसे आज़मा सकते हैं।