Computer >> Máy Tính >  >> Lập trình >> Ruby

Kiểm tra phân bổ đối tượng với RSpec

Mọi người đang nói về hiệu suất của Ruby gần đây, và có lý do chính đáng. Hóa ra là với một số chỉnh sửa nhỏ đối với mã của bạn, bạn có thể tăng hiệu suất lên đến 99,9%.

Có rất nhiều bài báo trên mạng về cách thức để tối ưu hóa mã của bạn, nhưng làm cách nào bạn có thể đảm bảo mã của mình vẫn còn tối ưu hóa?

Không phải lúc nào bạn cũng có thể cân nhắc hậu quả khi nhúng một chuỗi theo nghĩa đen chứ không phải là một hằng số cố định trong một phương thức được gọi thường xuyên - quá dễ dàng để mất khoản tiết kiệm tối ưu hóa khi duy trì mã của bạn trong tương lai.

Đây là những suy nghĩ của tôi gần đây khi tôi tối ưu hóa một số mã lần thứ hai (hoặc thứ ba) trong Ruby gem của chúng tôi tại Honeybadger:"Sẽ không tuyệt nếu có một cách để đảm bảo rằng những tối ưu hóa này không thoái lui ? "

Hồi quy là thứ mà hầu hết chúng ta đều quen thuộc trong phát triển phần mềm, ngay cả khi không phải tên. Hồi quy xảy ra khi một lỗi hoặc một vấn đề đã được giải quyết trong quá khứ tái diễn do thay đổi trong tương lai đối với cùng một mã. Không ai thích làm cùng một công việc nhiều hơn một lần; hồi quy giống như theo dõi bụi bẩn trên sàn nhà ngay sau khi nó được quét.

May mắn thay, chúng ta có một vũ khí bí mật:các bài kiểm tra. Cho dù bạn có thực hành TDD giáo điều hay không, các bài kiểm tra đều tuyệt vời để sửa lỗi vì chúng chứng minh sự cố và giải pháp theo chương trình. Các bài kiểm tra cho chúng tôi sự tin tưởng rằng việc hồi quy sẽ không xảy ra khi có các thay đổi.

Nghe có vẻ quen? Tôi cũng nghĩ vậy, điều này khiến tôi tự hỏi, "nếu tối ưu hóa hiệu suất có thể thoái lui, tại sao tôi cũng không thể bắt được những hồi quy đó bằng các bài kiểm tra?"

Có rất nhiều công cụ tuyệt vời để lập hồ sơ các khía cạnh hiệu suất khác nhau của Ruby bao gồm cấp phát đối tượng, bộ nhớ, CPU, thu gom rác, v.v. Một số công cụ này bao gồm ruby-prof, stackprof và cert_tracer.

Gần đây, tôi đã sử dụng phân bổ_tình trạng để phân bổ đối tượng cấu hình. Giảm phân bổ là một nhiệm vụ khá dễ thực hiện, mang lại nhiều kết quả thấp để điều chỉnh tốc độ và mức tiêu thụ bộ nhớ.

Ví dụ:đây là một lớp Ruby cơ bản lưu trữ một Mảng gồm 5 chuỗi được mặc định là 'foo':

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

API AllocationStats rất đơn giản. Cung cấp cho nó một khối vào hồ sơ và nó sẽ in ra nơi phân bổ hầu hết các đối tượng.

$ 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 (được gọi trên một nhóm phân bổ) chỉ cần in ra một bảng đẹp mà con người có thể đọc được được nhóm theo bất kỳ tiêu chí nào bạn yêu cầu.

Đầu ra này rất tuyệt khi lập hồ sơ theo cách thủ công, nhưng mục tiêu của tôi là tạo một thử nghiệm có thể chạy cùng với bộ thử nghiệm đơn vị bình thường của tôi (được viết bằng RSpec). Chúng ta có thể thấy rằng trên dòng 4 của my_class.rb, 5 chuỗi đang được cấp phát , điều này có vẻ không cần thiết vì tôi biết tất cả chúng đều chứa cùng một giá trị. Tôi muốn kịch bản của mình đọc một cái gì đó như:"khi khởi tạo MyClass, nó phân bổ dưới 6 đối tượng". Trong RSpec, nó trông giống như sau:

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

Sử dụng cú pháp này, tôi có mọi thứ tôi cần để kiểm tra rằng phân bổ đối tượng nhỏ hơn một số nhất định cho khối mã được mô tả (bên trong expect chặn) bằng cách sử dụng trình đối sánh RSpec tùy chỉnh.

Ngoài việc in kết quả theo dõi, AllocationStats cung cấp một số phương pháp để truy cập các phân bổ thông qua Ruby, bao gồm #allocations#new_allocations . Đây là những gì tôi đã sử dụng để xây dựng trình kết hợp của mình:

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

Tôi đang giải cứu LoadError trong câu lệnh request ban đầu vì tôi có thể không muốn bao gồm AllocationStats trong mọi lần chạy thử nghiệm (nó có xu hướng làm chậm các thử nghiệm). Sau đó, tôi xác định :allocate_under trình so khớp thực hiện theo dõi bên trong match khối. failure_message khối cũng quan trọng vì nó bao gồm to_text đầu ra từ dấu vết AllocationStats ngay bên trong thông báo lỗi của tôi ! Phần còn lại của trình so khớp chủ yếu là cấu hình RSpec tiêu chuẩn.

Với trình kết hợp của tôi đã được tải, bây giờ tôi có thể chạy kịch bản của mình từ trước đó và xem nó không thành công:

$ 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

OK, vậy là tôi đã trình bày vấn đề hiệu suất theo chương trình, đó là MyClass phân bổ các đối tượng chuỗi bổ sung có cùng giá trị. Hãy khắc phục sự cố đó bằng cách ném các giá trị đó vào một hằng số cố định:

class MyClass
  DEFAULT = 'foo'.freeze

  def initialize
    @values = Array.new(5)
    5.times { @values << DEFAULT }
  end
end

Bây giờ tôi đã khắc phục được sự cố, tôi sẽ chạy lại bài kiểm tra của mình và xem nó vượt qua:

$ 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

Lần tới, tôi thay đổi MyClass#initialize , tôi có thể tự tin rằng mình không phân bổ quá nhiều đối tượng.

Bởi vì phân bổ hồ sơ có thể tương đối chậm, sẽ là lý tưởng nhất để chạy những thứ này theo yêu cầu hơn là mọi lúc. Bởi vì tôi đã xử lý dễ dàng các số liệu phân bổ bị thiếu, tôi có thể sử dụng Bundler để tạo nhiều tệp tin đá quý và sau đó chỉ định tệp đá quý nào tôi muốn sử dụng với biến môi trường BUNDLE_GEMFILE:

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

Một lựa chọn khác là sử dụng một thư viện như đá quý thẩm định, sử dụng cùng một cách tiếp cận này và giải quyết một số lỗi của Bundler. Jason Clark đã có một bài thuyết trình xuất sắc về cách thực hiện điều này tại Ruby on Ales vào tháng 3 năm 2015; xem các trang trình bày của anh ấy để tìm hiểu thêm.

Tôi cũng nghĩ rằng việc duy trì các loại thử nghiệm này riêng biệt với các thử nghiệm đơn vị thông thường của tôi là một ý tưởng hay, vì vậy tôi sẽ tạo một thư mục "hiệu suất" mới để bộ thử nghiệm đơn vị của tôi nằm trong spec / unit / và bộ hiệu suất của tôi nằm trong spec / performance /:

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

Tôi vẫn đang tinh chỉnh cách tiếp cận của mình để lập hồ sơ mã Ruby cho hiệu suất; hy vọng của tôi là việc duy trì một bộ kiểm tra hiệu suất sẽ giúp tôi cải thiện tốc độ mã của mình hiện tại, giữ tốc độ nhanh trong tương lai và tạo tài liệu cho chính tôi và những người khác.