কম্পিউটার

ট্রেসপয়েন্ট দিয়ে রুবিতে ডিবাগ করার পদ্ধতি পরিবর্তন করা হচ্ছে

রুবি সবসময় তার ডেভেলপারদের কাছে নিয়ে আসা উৎপাদনশীলতার জন্য পরিচিত। মার্জিত সিনট্যাক্স, সমৃদ্ধ মেটা-প্রোগ্রামিং সমর্থন ইত্যাদি বৈশিষ্ট্যগুলির পাশাপাশি যা আপনাকে কোড লেখার সময় উত্পাদনশীল করে তোলে, এটিতে TracePoint নামে আরেকটি গোপন অস্ত্র রয়েছে যা আপনাকে দ্রুত "ডিবাগ" করতে সাহায্য করতে পারে৷

এই পোস্টে, আমি ডিবাগিং সম্পর্কে জানতে পেরেছি এমন 2টি আকর্ষণীয় তথ্য দেখাতে আমি একটি সাধারণ উদাহরণ ব্যবহার করব:

  1. অধিকাংশ সময়, বাগ নিজেই খুঁজে পাওয়া কঠিন নয়, তবে আপনার প্রোগ্রামটি কীভাবে কাজ করে তা বিস্তারিতভাবে বোঝা। একবার আপনি এটি গভীরভাবে বুঝতে পেরেছেন, আপনি সাধারণত বাগটি এখনই খুঁজে পেতে পারেন৷
  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টি ধাপে বাগ মোকাবেলা করতে সক্ষম হওয়া উচিত:

  1. ডিজাইন থেকে প্রত্যাশা জানুন
  2. বর্তমান বাস্তবায়ন বুঝুন
  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
  1. plus_1 নামে একটি পদ্ধতি সংজ্ঞায়িত করে
  2. ইনপুট পুনরুদ্ধার করে ("1" ) ARGV থেকে
  3. কল করে to_i "1"-এ , যা 1 প্রদান করে
  4. 1 বরাদ্দ করে স্থানীয় পরিবর্তনশীল input-এ
  5. কল plus_1 input সহ পদ্ধতি (1 ) এর যুক্তি হিসাবে। প্যারামিটার n এখন 1 এর একটি মান বহন করে
  6. কল + 1 এ পদ্ধতি একটি যুক্তি 2 সহ , এবং ফলাফল 3 প্রদান করে
  7. 3 ফেরত দেয় ধাপ 5
  8. এর জন্য
  9. কল puts
  10. কল to_s 3-এ , যা "3" প্রদান করে
  11. "3" পাস করে puts-এ ধাপ 8 থেকে কল করুন, যা একটি পার্শ্ব প্রতিক্রিয়া ট্রিগার করে যা স্ট্রিংটিকে Stdout এ প্রিন্ট করে। তারপর এটি nil ফেরত দেয় .

বর্ণনাটি 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

আমাদের কোড এখন অনেক বেশি পঠনযোগ্য। এটা আশ্চর্যজনক না? এটি বিশদ বিবরণের সাথে বেশিরভাগ প্রোগ্রাম এক্সিকিউশন প্রিন্ট করে! এমনকি আমরা এটিকে আমার আগের এক্সিকিউশন ব্রেকডাউন দিয়ে ম্যাপ করতে পারি:

  1. plus_1 নামে একটি পদ্ধতি সংজ্ঞায়িত করে
  2. ইনপুট পুনরুদ্ধার করে ("1" ) ARGV থেকে
  3. কল করে to_i "1"-এ , যা 1 প্রদান করে
  4. 1 বরাদ্দ করে স্থানীয় পরিবর্তনশীল input-এ
  5. কল plus_1 input সহ পদ্ধতি (1 ) এর যুক্তি হিসাবে। প্যারামিটার n এখন একটি মান 1 বহন করে
  6. কল + 1 এ পদ্ধতি একটি যুক্তি 2 সহ , এবং ফলাফল 3 প্রদান করে
  7. 3 ফেরত দেয় ধাপ 5
  8. এর জন্য
  9. কল puts
  10. কল to_s 3-এ , যা "3" প্রদান করে
  11. "3" পাস করে puts-এ ধাপ 8 থেকে কল করুন, যা একটি পার্শ্ব প্রতিক্রিয়া ট্রিগার করে যা স্ট্রিংটিকে Stdout এ প্রিন্ট করে। এবং তারপর এটি nil ফেরত দেয় .
# 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. ইনপুট পুনরুদ্ধার করে ("1" ) ARGV থেকে
    • ARGV এবং নিম্নলিখিত [] এই মুহূর্তে কল/c_call হিসাবে বিবেচিত হয় না
    1. 1 বরাদ্দ করে স্থানীয় পরিবর্তনশীল input-এ
    • বর্তমানে, পরিবর্তনশীল অ্যাসাইনমেন্টের জন্য কোন ইভেন্ট নেই। আমরা এটিকে line দিয়ে ট্র্যাক করতে পারি ঘটনা + regex, কিন্তু এটি সঠিক হবে না
    1. কল + 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টি সমস্যার জন্য, আমি সাধারণ রুবি/রেল অ্যাপ্লিকেশনের জন্য কিছু দরকারী বয়লারপ্লেটের সাথে পাওয়া কিছু কৌশল এবং গোটা সম্পর্কে আপনাকে জানানোর জন্য আরেকটি নিবন্ধ তৈরি করেছি।

এবং যদি আপনি এই ধারণাটিকে আকর্ষণীয় মনে করেন, আমি ট্যাপিং_ডিভাইস নামে একটি রত্নও তৈরি করেছি যা বাস্তবায়নের সমস্ত ঝামেলা লুকিয়ে রাখে৷

উপসংহার

ডিবাগার এবং ট্রেসিং উভয়ই ডিবাগিংয়ের জন্য দুর্দান্ত সরঞ্জাম এবং আমরা বহু বছর ধরে সেগুলি ব্যবহার করে আসছি। কিন্তু আমি এই নিবন্ধে প্রদর্শন করেছি, ডিবাগিং প্রক্রিয়া চলাকালীন তাদের ব্যবহার করার জন্য অনেক ম্যানুয়াল অপারেশন প্রয়োজন। যাইহোক, TracePoint এর সাহায্যে , আপনি তাদের অনেকগুলিকে স্বয়ংক্রিয় করতে পারেন এবং এইভাবে আপনার ডিবাগিং কর্মক্ষমতা বাড়াতে পারেন৷ আমি আশা করি আপনি এখন TracePoint যোগ করতে পারেন আপনার ডিবাগিং টুলবক্সে এবং একবার চেষ্টা করে দেখুন।


  1. রুবি গ্রেপ পদ্ধতি কীভাবে ব্যবহার করবেন (উদাহরণ সহ)

  2. রুবি মানচিত্র পদ্ধতি কীভাবে ব্যবহার করবেন (উদাহরণ সহ)

  3. রুবির সাথে এন-কুইন্স সমস্যা সমাধান করা

  4. রুবি ট্রান্সপোজ পদ্ধতিতে সারিগুলিকে কলামে পরিণত করুন