কম্পিউটার টিউটোরিয়াল

রুবি এফিশিয়েন্সি নিশ্চিত করুন: অবজেক্ট এলোকেশনের জন্য RSpec টেস্ট

আজকাল সবাই রুবি পারফরম্যান্স নিয়ে কথা বলছে, এবং কারণও আছে। কোডে ছোট ছোট টুইক দিয়ে ৯৯.৯% পর্যন্ত পারফরম্যান্স বাড়ানো সম্ভব।

কোড কীভাবে অপ্টিমাইজ করতে হয় সে নিয়ে অনেক আর্টিকেল আছে, কিন্তু আপনি কীভাবে নিশ্চিত করবেন আপনার কোড অপ্টিমাইজড থাকছে?

আপনি সবসময় চিন্তা করেন না যখন একটি রেগুলারলি কল্ড মেথডে ফ্রোজেন কনস্ট্যান্টের বদলে স্ট্রিং লিটারেল এম্বেড করেন—ভবিষ্যতে কোড মেইনটেইন করার সময় আপনার অপ্টিমাইজেশনের বাঁচানো সুবিধা হারানো খুব সহজ।

এই চিন্তাগুলো مؤخرে আমার মনে এসেছিল হানিব্যাজারের রুবি জেমে কিছু কোড অপ্টিমাইজ করার دومোরা (বা তিমোরা) সময়: "কি ভালো হত যদি এই অপ্টিমাইজেশনগুলো রিগ্রেস না করে তা নিশ্চিত করার উপায় থাকত!"

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

খুশির খবর, আমাদের কাছে একটি সিক্রেট হাতিয়ার আছে: টেস্ট। আপনি ডগম্যাটিক TDD অনুশীলন করুন বা না করুন, টেস্ট অসম্ভাব্য বাগ ফিক্স করার জন্য কারণ সেটা ইস্যু এবং সমাধান প্রোগ্রামেটিক্যালি প্রদর্শন করে। টেস্ট আমাদের আত্মবিশ্বাস দেয় যে পরিবর্তন হলে রিগ্রেশন হবena।

পরিচিত লাগছে? আমারও একই লাগেছিল, তাই আমি চিন্তা করলাম, "যদি পারফরম্যান্স অপ্টিমাইজেশন রিগ্রেস করতে পারে, তবে কেন আমি সেগুলো টেস্ট দিয়ে ক্যাচ করব না?"

রুবির বিভিন্ন পারফরম্যান্স দিক প্রোফাইল করার জন্য অনেক ভালো টুল আছে যার মধ্যে অবজেক্ট এলোকেশন, মেমরি, সিপিইউ, গারবেজ কালেকশন ইত্যাদি। এর মধ্যে কিছু হলো ruby-prof, stackprof এবং allocation_tracer।

আমি সম্প্রতি allocation_stats ব্যবহার করছি অবজেক্ট এলোকেশন প্রোফাইল করার জন্য। এলোকেশন কমাতে একটি সহজ কাজ, যা মেমরি খরচ এবং গতির জন্য অনেক লো-হ্যাঙ্গিং ফ্রুট দেয়।

উদাহরণস্বরূপ, এখানে একটি বেসিক রুবি ক্লাস যা ৫টি স্ট্রিং এর একটি অ্যারে সংরক্ষণ করে যেগুলো ডিফল্টভাবে 'foo':

class MyClass
  def initialize
    @values = Array.new(5)
    5.times { @values << 'foo' }
  end
end

AllocationStats API সহজ। এটাকে প্রোফাইল করার জন্য একটি ব্লক দিন, এবং এটি প্রিন্ট করবে সবচেয়ে বেশি অবজেক্ট কোথায় এলোকেট হয়েছে।

$ ruby -r allocation_stats -r ./lib/my_class
stats = AllocationStats.trace { MyClass.new }
puts stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text
^D
sourcefile sourceline class count
-------------------- -------- ----- -----
/lib/my_class.rb 4 String 5
/lib/my_class.rb 3 Array 1
- 1 MyClass 1

#to_text মেথড (এলোকেশন গ্রুপে কল করা) সিম্পলি একটি সুন্দর মানব-পাঠযোগ্য টেবিল প্রিন্ট করে যা আপনি যে ক্রাইটেরিয়া দিয়ে গ্রুপ করেছেন।

এই আউটপুট ম্যানুয়ালি প্রোফাইল করার জন্য চমৎকার, কিন্তু আমার লক্ষ্য ছিল একটি টেস্ট তৈরি করা যা আমার নরমাল ইউনিট টেস্ট সুইটের পাশাপাশি চলবে (যেটা RSpec-এ লেখা)। আমরা দেখতে পাচ্ছি my_class.rb-র লাইন ৪-এ ৫টি স্ট্রিং এলোকেট হচ্ছে, যা অনাবশ্যক যেহেতু আমি জানি সেগুলোর মান একই। আমি আমার সিনেরিওটা এইভাবে লিখতে চেয়েছিলাম: "MyClass ইনিশিয়ালাইজ করার সময় এটি ৬টির নিচে অবজেক্ট এলোকেট করে"। RSpec-এ এটা দেখাবে এমন:

describe MyClass do
  context "when initializing" do
    specify { expect { MyClass.new }.to allocate_under(6).objects }
  end
end

এই সিনট্যাক্স ব্যবহার করে আমার কাছে সব কিছু আছে যাতে টেস্ট করতে পারি অবজেক্ট এলোকেশন একটি নির্দিষ্ট সংখ্যা থেকে কম কিনা বর্ণিত কোড ব্লকের জন্য ( expect ব্লকের ভিতরে) একটি কাস্টোম RSpec ম্যাচার ব্যবহার করে।

ট্রেস রেজাল্ট প্রিন্ট করার পাশাপাশি, AllocationStats কিছু মেথড প্রদান করে এলোকেশন রুবি মাধ্যমে অ্যাক্সেস করার জন্য, যার মধ্যে #allocations এবং #new_allocations। এগুলোই আমি আমার ম্যাচার বানানোর জন্য ব্যবহার করেছি:

begin
  require 'allocation_stats'
rescue LoadError
  puts 'Skipping AllocationStats.'
end

RSpec::Matchers.define :allocate_under do |expected|
  match do |actual|
    return skip('AllocationStats is not available: skipping.') unless defined?(AllocationStats)
    @trace = actual.is_a?(Proc) ? AllocationStats.trace(&actual) : actual
    @trace.new_allocations.size < expected
  end

  def objects
    self
  end

  def supports_block_expectations?
    true
  end

  def output_trace_info(trace)
    trace.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text
  end

  failure_message do |actual|
    "expected under #{ expected } objects to be allocated; got #{ @trace.new_allocations.size }:\n\n" \
    << output_trace_info(@trace)
  end

  description do
    "allocates under #{ expected } objects"
  end
end

আমি LoadError রেসকিউ করেছি ইনিশিয়াল রিকোয়ার স্টেটমেন্টে কারণ আমি AllocationStats প্রতিটি টেস্ট রানে ইনক্লুড করতে চাই না (এটা টেস্ট স্লো করে)। তারপর আমি :allocate_under ম্যাচার ডিফাইন করেছি যা match ব্লকের ভিতরে ট্রেস পারফর্ম করে। failure_message ব্লকটিও গুরুত্বপূর্ণ কারণ এতে AllocationStats ট্রেসের to_text আউটপুট রাইট ইনসাইড মায় ফেইলার মেসেজ থাকে! ম্যাচারের বাকি অংশ অধিকাংশ স্ট্যান্ডার্ড RSpec কনফিগারেশন।

আমার ম্যাচার লোড হলে, আমি এখন আমার সিনেরিও রান করতে পারি, এবং এটা ফেইল होते দেখতে পারি:

$ rspec spec/my_class_spec.rb
MyClass
  when initializing
    should allocates under 6 objects (FAILED - 1)

Failures:
  1) MyClass when initializing should allocates under 6 objects
  Failure/Error: expect { MyClass.new }.to allocate_under(6).objects
  expected under 6 objects to be allocated; got 7:
  sourcefile sourceline class count
  --------------------------- ---------- ------- -----
  /spec/my_class_spec.rb 6 MyClass 1
  /lib/my_class.rb 3 Array 1
  /lib/my_class.rb 4 String 5
  # ./spec/my_class_spec.rb:6:in `block (3 levels) in '
Finished in 0.15352 seconds (files took 0.22293 seconds to load)
1 example, 1 failure

Failed examples:
rspec ./spec/my_class_spec.rb:5 # MyClass when initializing should allocates under 6 objects

ঠিক আছে, আমি প্রোগ্রামেটিক্যালি পারফরম্যান্স সমস্যাটি প্রদর্শন করলাম, যেটা হলো MyClass একই মান্বালা অতিরিক্ত স্ট্রিং অবজেক্ট এলোকেট করছে। এটা ফিক্স করি সেই মানগুলো একটি ফ্রোজেন কনস্ট্যান্টে রাখে:

class MyClass
  DEFAULT = 'foo'.freeze
  def initialize
    @values = Array.new(5)
    5.times { @values << DEFAULT }
  end
end

এখন আমি সমস্যাটি ফিক্স করলাম, আমি আবার আমার টেস্ট রান করব এবং পাস होते দেখব:

$ rspec spec/my_class_spec.rb
MyClass
  when initializing
    should allocates under 6 objects
Finished in 0.14952 seconds (files took 0.22056 seconds to load)
1 example, 0 failures

পরবর্তী বার আমি MyClass#initialize মেথড পরিবর্তন করলে, আমি নিশ্চিত থাকব যে আমি অনেক অবজেক্ট এলোকেট করছি না।

যেহেতু এলোকেশন প্রোফাইল করা রিলেটিভলি স্লো হতে পারে, আদর্শ হবে এইগুলো অন-ডিমান্ড রান করা সবসময় না। যেহেতু আমি ইতিমধ্যে allocation_stats মিসিং হ্যান্ডেল করছি গ্রেসফুলি, আমি বান্ডলার ব্যবহার করে মাল্টিপল জেমফাইল তৈরি করতে পারি এবং তারপর BUNDLE_GEMFILE এনভায়রনমেন্ট ভেরিয়েবল দিয়ে নির্দিষ্ট করতে পারি কোন জেমফাইল ব্যবহার করতে চাই:

$ BUNDLE_GEMFILE=with_performance.gemfile bundle exec rspec spec/
$ BUNDLE_GEMFILE=without_performance.gemfile bundle exec rspec spec/

এনঅথার অপশন হলো appraisal জেমের মতো লাইব্রেরি ব্যবহার করা, যা একই পদ্ধতি নেয় এবং বান্ডলার গটচা সমাধান করে। জেসন ক্লার্ক রুবি অন এলসে মার্চ ২০১৫-এ একটি চমৎকার প্রেজেন্টেশন দিয়েছিলেন এই বিষয়ে; তার স্লাইডস দেখে আরও শিখুন।

আমি ও মনে করি এই টাইপের টেস্টগুলো নরমাল ইউনিট টেস্ট থেকে আলাদা রাখা ভালো আইডিয়া, তাই আমি একটি নতুন "performance" ডিরেক্টরি তৈরি করব যাতে আমার ইউনিট টেস্ট সুইট থাকে spec/unit/-এ এবং পারফরম্যান্স সুইট থাকে spec/performance/-এ:

spec/
|-- spec_helper.rb
|-- unit/
|-- features/
|-- performance/

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

  1. HTML DOM Code অবজেক্ট

  2. C# কোডের জন্য ইউনিট টেস্টিং

  3. QRGen দিয়ে ম্যালিসিয়াস QR কোড তৈরির প্রক্রিয়া

  4. RuboCop দিয়ে রুবি কোড কোয়ালিটি উন্নত: লিন্টিং ও অটো-ফরম্যাটিং