RSpecを使用したオブジェクト割り当てのテスト
最近、誰もがRubyのパフォーマンスについて話し合っていますが、それには十分な理由があります。コードを少し調整するだけで、パフォーマンスを最大99.9%向上させることができます。
方法に関する記事はたくさんあります コードを最適化するためですが、コードが残っていることをどのように確認できますか 最適化されていますか?
定期的に呼び出されるメソッドにフリーズ定数ではなく文字列リテラルを埋め込む場合、必ずしも結果を考慮するとは限りません。将来コードを保守するときに、最適化の節約を失うのは簡単です。
HoneybadgerのRubygemで2回目(または3回目)にコードを最適化したときの最近の考えは次のとおりです。「これらの最適化が回帰しないようにする方法があれば素晴らしいと思いません / em> ?"
回帰は、名前ではなくても、ソフトウェア開発で私たちのほとんどが精通しているものです。同じコードへの将来の変更により、過去に解決されたバグまたは問題が再発した場合、リグレッションが発生します。同じ仕事を2回以上行うのが好きな人は誰もいません。退行は、床が掃除された直後に床の汚れを追跡するようなものです。
幸いなことに、私たちは秘密兵器を持っています:テスト。独断的なTDDを実践するかどうかにかかわらず、テストは素晴らしい プログラムで問題と解決策を示しているため、バグを修正してください。テストにより、変更が行われたときに回帰が発生しないという確信が得られます。
おなじみですか?私もそう思ったので、「パフォーマンスの最適化が低下する可能性があるのに、なぜそれらの低下をテストでキャッチできないのか」と疑問に思いました。
オブジェクトの割り当て、メモリ、CPU、ガベージコレクションなど、Rubyのさまざまなパフォーマンスの側面をプロファイリングするための優れたツールがたくさんあります。これらには、ruby-prof、stackprof、allocation_tracerなどがあります。
最近、allocation_statsを使用してオブジェクトの割り当てをプロファイリングしています。割り当てを減らすことは、達成するのがかなり簡単な作業であり、メモリ消費と速度を調整するための多くの手間のかからない成果をもたらします。
たとえば、デフォルトで「foo」になっている5つの文字列の配列を格納する基本的なRubyクラスは次のとおりです。
class MyClass
def initialize
@values = Array.new(5)
5.times { @values << 'foo' }
end
end
AllocationStatsAPIはシンプルです。プロファイルにブロックを付けると、ほとんどのオブジェクトが割り当てられている場所が印刷されます。
$ 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の4行目に、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は、#allocations
など、Rubyを介して割り当てにアクセスするためのいくつかのメソッドを提供します。 および#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を含めたくない場合があるため、最初のrequireステートメントに(テストの速度が低下する傾向があります)。次に、: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
OK、それで私はプログラムでパフォーマンスの問題を示しました。それは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を作成し、BUNDLE_GEMFILE環境変数で使用するgemfileを指定できます。
$ BUNDLE_GEMFILE=with_performance.gemfile bundle exec rspec spec/
$ BUNDLE_GEMFILE=without_performance.gemfile bundle exec rspec spec/
もう1つのオプションは、評価宝石のようなライブラリを使用することです。これは、これと同じアプローチを採用し、いくつかのBundlerの落とし穴を解決します。 Jason Clarkは、2015年3月にRubyonAlesでこれを行う方法について優れたプレゼンテーションを行いました。詳細については、彼のスライドをご覧ください。
また、これらのタイプのテストを通常の単体テストとは別に維持することも良い考えだと思うので、新しい「パフォーマンス」ディレクトリを作成して、ユニットテストスイートがspec / unit /にあり、パフォーマンススイートがspecにあるようにします。 / performance /:
spec/
|-- spec_helper.rb
|-- unit/
|-- features/
|-- performance/
パフォーマンスのためにRubyコードをプロファイリングするためのアプローチをまだ改良しています。パフォーマンステストスイートを維持することで、現在のコードの速度を改善し、将来的に高速に保ち、自分自身や他の人のためにドキュメントを作成するのに役立つことを願っています。
-
QRGenを使用した悪意のあるQRコード
QRコードは、自動的にスキャンする必要があるものすべてに使用される機械可読データ形式です。製品のパッケージングから航空会社の搭乗券など、あらゆる場所でカスタムQRコードにパックされたエクスプロイトを使用して、一般的な脆弱性を悪用することができます。ハッカーは、脆弱なデバイスを標的とする悪意のあるQRコードを作成するツールQRGenを使用しました。人間はQRコードに含まれる情報をスキャンせずに読み取ったり理解したりすることができないため、QRコード攻撃は強力であり、コードの解読を試みるために使用されるデバイスが、QRコードに含まれるエクスプロイトにさらされる可能性があります。人間は実際にスキャン
-
RuboCopを使用したRubyコードのリンティングと自動フォーマット
リンティングは、プログラムおよびスタイルのエラーについてソースコードを自動チェックすることです。このチェックは、リンターと呼ばれる静的コード分析ツールによって実行されます。ただし、コードフォーマッタは、事前に構成された一連のルールに厳密に準拠するようにソースコードをフォーマットするためのツールです。リンターは通常違反を報告しますが、問題を修正するのは通常プログラマー次第ですが、コードフォーマッターはそのルールをソースコードに直接適用する傾向があるため、フォーマットの間違いを自動的に修正します。 プロジェクトでより一貫性のあるコードスタイルを作成するタスクでは、通常、個別のリンティングツールと