Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ทดสอบการจัดสรรวัตถุด้วย RSpec

ช่วงนี้ใครๆ ก็พูดถึงผลงานของ Ruby ด้วยเหตุผลที่ดี ปรากฎว่าด้วยการปรับแต่งเล็กน้อยในโค้ดของคุณ คุณสามารถเพิ่มประสิทธิภาพได้ถึง 99.9%

มีบทความมากมายเกี่ยวกับ วิธีการ เพื่อเพิ่มประสิทธิภาพโค้ดของคุณ แต่คุณจะแน่ใจได้อย่างไรว่าโค้ดของคุณยังคงอยู่ ปรับให้เหมาะสมหรือไม่

คุณอาจไม่ได้พิจารณาถึงผลที่ตามมาเสมอเมื่อฝังสตริงตามตัวอักษร แทนที่จะเป็นค่าคงที่ที่ตรึงไว้ในวิธีที่เรียกเป็นประจำ ซึ่งง่ายเกินไปที่จะสูญเสียการประหยัดของการเพิ่มประสิทธิภาพเมื่อรักษาโค้ดของคุณในอนาคต

นี่เป็นความคิดของฉันเมื่อเร็วๆ นี้ขณะที่ฉันปรับโค้ดบางส่วนเป็นครั้งที่สอง (หรือสาม) ใน Ruby gem ของเราที่ Honeybadger:"จะดีไหมถ้ามีวิธีที่จะทำให้มั่นใจว่าการเพิ่มประสิทธิภาพเหล่านี้จะไม่ถดถอย ?"

การถดถอยเป็นสิ่งที่พวกเราส่วนใหญ่คุ้นเคยในการพัฒนาซอฟต์แวร์ แม้ว่าจะไม่ใช่โดยใช้ชื่อก็ตาม การถดถอยเกิดขึ้นเมื่อจุดบกพร่องหรือปัญหาที่ได้รับการแก้ไขในอดีตเกิดขึ้นอีกเนื่องจากการเปลี่ยนแปลงรหัสเดิมในอนาคต ไม่มีใครชอบทำงานแบบเดียวกันมากกว่าหนึ่งครั้ง การถดถอยก็เหมือนการติดตามสิ่งสกปรกบนพื้นทันทีหลังจากที่กวาดไป

โชคดีที่เรามีอาวุธลับ:การทดสอบ ไม่ว่าคุณจะฝึก TDD แบบดันทุรังหรือไม่ก็ตาม การทดสอบนั้นยอดเยี่ยม สำหรับการแก้ไขจุดบกพร่องเพราะแสดงให้เห็นปัญหาและวิธีแก้ปัญหาโดยทางโปรแกรม การทดสอบทำให้เรามั่นใจว่าการถดถอยจะไม่เกิดขึ้นเมื่อมีการเปลี่ยนแปลง

เสียงคุ้นเคย? ฉันก็คิดอย่างนั้นเหมือนกัน ซึ่งทำให้ฉันสงสัยว่า "หากการเพิ่มประสิทธิภาพประสิทธิภาพสามารถถดถอยได้ ทำไมฉันจึงตรวจดูการถดถอยเหล่านั้นด้วยการทดสอบไม่ได้"

มีเครื่องมือที่ยอดเยี่ยมมากมายสำหรับการทำโปรไฟล์ด้านประสิทธิภาพต่างๆ ของ Ruby รวมถึงการจัดสรรอ็อบเจ็กต์ หน่วยความจำ CPU การรวบรวมขยะ ฯลฯ สิ่งเหล่านี้รวมถึง ruby-prof, stackprof และ allocation_tracer

ฉันเพิ่งใช้ allocation_stats กับโปรไฟล์การจัดสรรอ็อบเจ็กต์ การลดการจัดสรรเป็นงานที่ค่อนข้างง่ายในการทำให้สำเร็จ โดยได้ผลลัพธ์จำนวนมากสำหรับการปรับการใช้หน่วยความจำและความเร็ว

ตัวอย่างเช่น นี่คือคลาส Ruby พื้นฐานที่เก็บ Array 5 สตริงซึ่งมีค่าเริ่มต้นเป็น '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) เราจะเห็นว่าในบรรทัดที่ 4 ของ my_class.rb มีการจัดสรร 5 สตริง ซึ่งดูเหมือนไม่จำเป็นเพราะฉันรู้ว่าทั้งหมดมีค่าเท่ากัน ฉันต้องการให้สถานการณ์ของฉันอ่านบางอย่างเช่น:"เมื่อเริ่มต้น MyClass จะจัดสรรภายใต้ 6 อ็อบเจ็กต์" ใน RSpec จะมีลักษณะดังนี้:

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

การใช้ไวยากรณ์นี้ ฉันมีทุกสิ่งที่ฉันต้องการเพื่อทดสอบว่าการจัดสรรวัตถุน้อยกว่าจำนวนที่กำหนดสำหรับบล็อกโค้ดที่อธิบายไว้ (ภายใน expect บล็อก) โดยใช้ตัวจับคู่ RSpec ที่กำหนดเอง

นอกจากการพิมพ์ผลลัพธ์การติดตามแล้ว AllocationStats ยังมีวิธีการสองสามวิธีในการเข้าถึงการจัดสรรผ่าน Ruby รวมถึง #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 บล็อกก็มีความสำคัญเช่นกันเพราะมี to_text ผลลัพธ์จากการติดตาม AllocationStats ภายในข้อความแสดงความล้มเหลวของฉัน ! ส่วนที่เหลือของตัวจับคู่ส่วนใหญ่เป็นการกำหนดค่า 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
       ---------------------------  ----------  -------  -----
       <PWD>/spec/my_class_spec.rb           6  MyClass      1
       <PWD>/lib/my_class.rb                 3  Array        1
       <PWD>/lib/my_class.rb                 4  String       5
     # ./spec/my_class_spec.rb:6:in `block (3 levels) in <top (required)>'

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 ที่หายไปได้อย่างสวยงาม ฉันจึงสามารถใช้ Bundler เพื่อสร้าง gemfile หลายไฟล์ จากนั้นระบุ gemfile ที่ฉันต้องการใช้กับตัวแปรสภาพแวดล้อม BUNDLE_GEMFILE:

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

อีกทางเลือกหนึ่งคือการใช้ไลบรารีเช่น Appraisal Gem ซึ่งใช้แนวทางเดียวกันนี้และแก้ปัญหา Bundler gotchas บางส่วน Jason Clark นำเสนอวิธีการทำสิ่งนี้ที่ Ruby on Ales อย่างดีเยี่ยมในเดือนมีนาคม 2015; ดูสไลด์ของเขาเพื่อเรียนรู้เพิ่มเติม

ฉันยังคิดว่าการรักษาประเภทการทดสอบเหล่านี้แยกจากการทดสอบหน่วยปกติของฉันเป็นความคิดที่ดี ดังนั้นฉันจะสร้างไดเร็กทอรี "ประสิทธิภาพ" ใหม่เพื่อให้ชุดทดสอบหน่วยของฉันอยู่ใน spec/unit/ และชุดประสิทธิภาพของฉันอยู่ใน spec /ประสิทธิภาพ/:

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

ฉันยังคงปรับแต่งวิธีการสร้างโปรไฟล์รหัส Ruby เพื่อประสิทธิภาพ ฉันหวังว่าการรักษาชุดทดสอบประสิทธิภาพจะช่วยให้ฉันปรับปรุงความเร็วของโค้ดได้ในตอนนี้ ใช้งานได้เร็วในอนาคต และสร้างเอกสารสำหรับตัวเองและผู้อื่น