रूबी हमेशा अपने डेवलपर्स के लिए उत्पादकता के लिए जानी जाती है। सुरुचिपूर्ण सिंटैक्स, समृद्ध मेटा-प्रोग्रामिंग समर्थन, आदि जैसी सुविधाओं के साथ, जो आपको कोड लिखते समय उत्पादक बनाती हैं, इसमें 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
3
- एक विधि को परिभाषित करता है जिसे
plus_1
. कहा जाता है - इनपुट पुनर्प्राप्त करता है (
"1"
)ARGV
. से - कॉल करता है
to_i
"1"
. पर , जो1
. लौटाता है - असाइन करता है
1
स्थानीय चर के लिएinput
- कॉल करता है
plus_1
input
के साथ विधि (1
) इसके तर्क के रूप में। पैरामीटरn
अब1
. का मान है - कॉल करता है
+
1
. पर विधि तर्क के साथ2
, और परिणाम देता है3
- रिटर्न
3
चरण 5 के लिए - कॉल करता है
puts
- कॉल
to_s
पर3
, जो"3"
. लौटाता है - पास करता है
"3"
करने के लिएputs
चरण 8 से कॉल करें, जो एक साइड इफेक्ट को ट्रिगर करता है जो स्ट्रिंग को Stdout पर प्रिंट करता है। फिर यहnil
returns लौटाता है ।
विवरण 100% सटीक नहीं है, लेकिन यह एक सरल व्याख्या के लिए पर्याप्त है।
बग को संबोधित करना
अब जब हमने जान लिया है कि हमारे प्रोग्राम को कैसे काम करना चाहिए और यह वास्तव में कैसे काम करता है, तो हम बग की तलाश शुरू कर सकते हैं। हमारे पास मौजूद जानकारी के साथ, हम ऊपर की ओर (चरण 10 से शुरू) या नीचे की ओर (चरण 1 से शुरू) विधि कॉल का पालन करके बग की खोज कर सकते हैं। इस मामले में, हम इसे उस विधि पर वापस ट्रेस करके कर सकते हैं जो पहले स्थान पर 3 लौटाती है—जो कि 1 + 2
है step 6
. में ।
यह वास्तविकता से बहुत दूर है!
बेशक, हम सभी जानते हैं कि वास्तविक डिबगिंग उतना सरल नहीं है जितना कि उदाहरण से पता चलता है। वास्तविक जीवन के कार्यक्रमों और हमारे उदाहरण के बीच महत्वपूर्ण अंतर आकार है। 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
जोड़ने की जरूरत है कोड में, लेकिन, मूल्यों को अलग से प्रिंट करने के लिए, हमें कुछ मूल्यों के मध्यवर्ती राज्यों तक पहुंचने के लिए अपने तर्क को विभाजित करने की भी आवश्यकता है। इस मामले में, हमें आंतरिक राज्यों के लिए 8 परिवर्तनों के साथ 4 अतिरिक्त आउटपुट मिले। औसतन, आउटपुट की 1 पंक्ति के लिए परिवर्तनों की यह 2 पंक्तियाँ हैं! और चूंकि परिवर्तनों की संख्या प्रोग्राम के आकार के साथ रैखिक रूप से बढ़ती है, हम इसकी तुलना O(n)
से कर सकते हैं ऑपरेशन।
डिबगिंग महंगा क्यों है?
हमारे कार्यक्रमों को कई लक्ष्यों को ध्यान में रखते हुए लिखा जा सकता है:रखरखाव, प्रदर्शन, सरलता, आदि लेकिन आमतौर पर "ट्रेसेबिलिटी" के लिए नहीं, जिसका अर्थ है, निरीक्षण के लिए मूल्य प्राप्त करना, जिसके लिए आमतौर पर कोड के संशोधन की आवश्यकता होती है, उदा। विभाजित जंजीर विधि कॉल।
- जितनी अधिक जानकारी आपको मिलेगी, आपको कोड में उतने ही अधिक परिवर्धन/परिवर्तन करने होंगे।
हालाँकि, एक बार आपको प्राप्त होने वाली जानकारी की मात्रा एक निश्चित बिंदु तक पहुँच जाती है, तो आप इसे कुशलता से संसाधित नहीं कर पाएंगे। इसलिए हमें या तो जानकारी को फ़िल्टर करने की आवश्यकता है या इसे समझने में हमारी सहायता करने के लिए इसे लेबल करना होगा।
- जानकारी जितनी सटीक होगी, आपको कोड में उतने ही अधिक परिवर्धन/परिवर्तन करने होंगे।
अंत में, क्योंकि काम में कोडबेस को छूना शामिल है—जो बग के बीच बहुत भिन्न हो सकता है (जैसे नियंत्रक बनाम मॉडल तर्क)—इसे स्वचालित करना कठिन है। भले ही आपका कोडबेस ट्रेसिंग-फ्रेंडली हो (जैसे कि यह "लॉ ऑफ डेमेटर" का सख्ती से पालन करता है), ज्यादातर समय, आपको अलग-अलग वेरिएबल/मेथड नाम मैन्युअल रूप से टाइप करने होंगे।
(वास्तव में, रूबी में, इससे बचने के लिए कुछ तरकीबें हैं- जैसे __method__
. लेकिन आइए यहां चीजों को जटिल न करें।)
ट्रेसपॉइंट:उद्धारकर्ता
हालांकि, रूबी हमें एक असाधारण टूल प्रदान करती है जो काफी हद तक लागत को कम कर सकती है: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}"
# if you call `return` on any non-return events, it'll raise error
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
. कहा जाता है - इनपुट पुनर्प्राप्त करता है (
"1"
)ARGV
. से - कॉल करता है
to_i
"1"
. पर , जो1
. लौटाता है - असाइन करता है
1
स्थानीय चर के लिएinput
- कॉल करता है
plus_1
input
के साथ विधि (1
) इसके तर्क के रूप में। पैरामीटरn
अब एक मान है1
- कॉल करता है
+
1
. पर विधि तर्क के साथ2
, और परिणाम देता है3
- रिटर्न
3
चरण 5 के लिए - कॉल करता है
puts
- कॉल
to_s
पर3
, जो"3"
. लौटाता है - पास करता है
"3"
करने के लिएputs
चरण 8 से कॉल करें, जो एक साइड इफेक्ट को ट्रिगर करता है जो स्ट्रिंग को Stdout पर प्रिंट करता है। और फिर यहnil
returns लौटाता है ।
# ignore this, it's TracePoint tracing itself ;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. Defines a method called `plus_1`.
return of Module#method_added on Object => nil
call of String#to_i on "1" # 3-1. Calls `to_i` on `"1"`
return of String#to_i on "1" => 1 # 3-2. which returns `1`
call of Object#plus_1 on main # 5. Calls `plus_1` method with `input`(`1`) as its argument.
return of Object#plus_1 on main => 3 # 7. Returns `3` for step 5
call of Kernel#puts on main # 8. Calls `puts`
call of IO#puts on #<IO:<STDOUT>>
call of Integer#to_s on 3 # 9. Calls `to_s` on `3`, which returns `"3"`
return of Integer#to_s on 3 => "3"
call of IO#write on #<IO:<STDOUT>> # 10-1. Passes `"3"` to the `puts` call from step 8
# 10-2. which triggers a side effect that prints the string to Stdout
3 # original output
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. And then it returns `nil`.
हम यह भी कह सकते हैं कि यह मेरे द्वारा पहले कही गई बातों से अधिक विस्तृत है! हालाँकि, आप देख सकते हैं कि चरण 2, 4 और 6 आउटपुट से गायब हैं। दुर्भाग्य से, वे TracePoint
. द्वारा ट्रैक करने योग्य नहीं हैं निम्नलिखित कारणों से:
-
- इनपुट पुनर्प्राप्त करता है (
"1"
)ARGV
. से
ARGV
और निम्नलिखित[]
इस समय कॉल/c_call के रूप में नहीं माना जाता है
- इनपुट पुनर्प्राप्त करता है (
-
- असाइन करता है
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)
# tracing logic
end
ध्यान रखने वाली एक और बात यह है कि जिस ब्लॉक को आप TracePoint
. के लिए परिभाषित करते हैं हजारों बार मूल्यांकन किया जा सकता है। इस पैमाने पर, आप फ़िल्टरिंग तर्क को कैसे लागू करते हैं, इसका आपके ऐप के प्रदर्शन पर बहुत प्रभाव पड़ सकता है। उदाहरण के लिए, मैं इसकी अनुशंसा नहीं करता:
TracePoint.trace(:call) do |tp|
trace = caller[0]
next unless trace.match?("app")
# tracing logic
end
इन 2 समस्याओं के लिए, मैंने आपको कुछ तरकीबों और गठजोड़ के बारे में बताने के लिए एक और लेख तैयार किया है, जो मुझे विशिष्ट रूबी/रेल अनुप्रयोगों के लिए कुछ उपयोगी बॉयलरप्लेट के साथ मिला है।
और अगर आपको यह अवधारणा दिलचस्प लगती है, तो मैंने भी एक रत्न बनाया है जिसे Taping_device कहा जाता है जो कार्यान्वयन की सभी बाधाओं को छुपाता है।
निष्कर्ष
डीबगर और ट्रेसिंग डिबगिंग के लिए दोनों महान उपकरण हैं, और हम कई वर्षों से उनका उपयोग कर रहे हैं। लेकिन जैसा कि मैंने इस लेख में प्रदर्शित किया है, उनका उपयोग करने के लिए डिबगिंग प्रक्रिया के दौरान कई मैन्युअल संचालन की आवश्यकता होती है। हालांकि, TracePoint
. की मदद से , आप उनमें से कई को स्वचालित कर सकते हैं और इस प्रकार अपने डिबगिंग प्रदर्शन को बढ़ा सकते हैं। मुझे आशा है कि अब आप TracePoint
add जोड़ सकते हैं अपने डिबगिंग टूलबॉक्स में और इसे आज़माएं।