RailsとMerbのマージ:パフォーマンス(パート2/6)

RailsとMerbの合併に関する6つの連続した記事が、2009年12月から2010年4月にwww.engineyard.comで公開されました これは2番目の記事です。 最初のものはここです。



MerbのRailsに実装したいと考えていた次の大きな改善は、パフォーマンスの向上でした。 MerbはRailsの後に来たので、Merb開発者はRailsのどの部分がより頻繁に使用されているかを見つけ、パフォーマンスを最適化する機会がありました。



Merbからパフォーマンスの改善を行い、それらをRails 3に転送したかったのです。この投稿では、Rails 3に追加されたいくつかの最適化ポイントについて説明します。





始めるために、Railsの特定の、しかし広く使用されているいくつかの部分のパフォーマンスに注目しました。



これは明らかに大まかな見積もりですが、パフォーマンスが大幅に向上する可能性があるほとんどのケースをカバーしましたが、Rails開発者は自分でそれを改善することはできませんでした。



コントローラーのオーバーヘッド



最初のステップは、Railsコントローラーのオーバーヘッドを改善することでした。 Rails 2.3でそれらをテストする方法はありません。これは、 render :string



を使用して、応答としてテキストをクライアントに送信render :string



必要があるためです。 それでも、これを可能な限り減らすことを望んでいました。



これを行う際に、CallStackPrinterにCallStackPrinter



ruby-profのStefan Kaesのフォークを使用しCallStackPrinter



(これまで見たすべてのRubyアプリケーションからプロファイルデータを視覚化する最良の方法)。 また、特定のサイトに焦点を当て、より正確なデータを取得するために、作業の過程でプロファイラーを複製できるベンチマークをいくつか作成しました。



コントローラーのプロセスを見ると、答えの作成が支配的な部分を占めていることがわかりました。 さらに掘り下げた後、ActionControllerがヘッダーを直接設定し、さらに情報を取得するために応答を返す前にヘッダーを再度解析することがわかりました。 この現象の良い例は、2つのコンポーネント(コンテンツタイプ自体とオプションの文字セット)を持つContent-Type



ヘッダーです。 両方のコンポーネントは、ゲッターとセッターを介してResponseオブジェクトで使用できました。



Copy Source | Copy HTML<br/> def content_type =(mime_type)<br/> self .headers[ "Content-Type" ] =<br/> if mime_type =~ / charset / || (c = charset ). nil ?<br/> mime_type.to_s<br/> else <br/> "#{mime_type}; charset=#{c}" <br/> end <br/> end <br/> <br/> # Returns the response's content MIME type, or nil if content type has been set. <br/> def content_type <br/> content_type = String (headers[ "Content-Type" ] || headers[ "type" ]). split ( ";" )[ 0 ]<br/> content_type .blank? ? nil : content_type <br/> end <br/> <br/> # Set the charset of the Content-Type header. Set to nil to remove it. <br/> # If no content type is set, it defaults to HTML. <br/> def charset =( charset )<br/> headers[ "Content-Type" ] =<br/> if charset <br/> "#{content_type || Mime::HTML}; charset=#{charset}" <br/> else <br/> content_type || Mime ::HTML.to_s<br/> end <br/> end <br/> <br/> def charset <br/> charset = String (headers[ "Content-Type" ] || headers[ "type" ]). split ( ";" )[ 1 ]<br/> charset .blank? ? nil : charset .strip. split ( "=" )[ 1 ]<br/> end <br/>





ご覧のとおり、ResponseオブジェクトはContent-Typeヘッダーと直接連携し、必要に応じてヘッダーの一部を処理します。 応答は、応答をクライアントに送信する前の準備中にヘッダーで追加の作業を行ったため、特に問題がありました。



Copy Source | Copy HTML<br/> def assign_default_content_type_and_charset !<br/> self .content_type ||= Mime ::HTML<br/> self .charset ||= default_charset unless sending_file?<br/> end <br/>





つまり、応答を送信する前に、Railsは再びContent-Type



ヘッダーをセミコロンでContent-Type



し、文字列をさらに処理してそれらを再び接続しました。 そしてもちろん、テンプレートのタイプまたはrespond_to



ブロック内に応じて、それを正しく設定するために、 Response#content_type=



Railsの他の部分で使用されました。



これは要求時に数百ミリ秒かかりませんでしたが、キャッシュの多いアプリケーションでは、キャッシュからオブジェクトを取得してクライアントに返すよりもコストが高くなる可能性がありました。



この場合の解決策は、応答オブジェクトのフィールドにコンテンツタイプと文字セットを保存し、応答を準備するときにこれらを1つの簡単なクイック操作と組み合わせることです。



Copy Source | Copy HTML<br/>attr_accessor :charset, :content_type<br/> <br/> def assign_default_content_type_and_charset !<br/> return if headers[CONTENT_TYPE].present?<br/> <br/> @content_type ||= Mime ::HTML<br/> @charset ||= self . class .default_charset<br/> <br/> type = @content_type. to_s .dup<br/> type < < "; charset=#{@charset}" unless @sending_file<br/> <br/> headers[CONTENT_TYPE] = type<br/> end <br/>





インスタンス変数を見つけて、1つの文字列を作成します。 これらのコード行に対する複数の変更により、噴火の時間が約400マイクロ秒から100マイクロ秒に短縮されました。 もちろん、それほど時間はかかりませんが、パフォーマンスに敏感なアプリケーションを本当に弱める可能性があります。



部分コレクションレンダー



部分コレクションのレンダリングは、最適化のもう1つの良い機会でした。 そしてこの場合、改善はマイクロ秒ではなくミリ秒でした!



開始するには、Rails 2.3の実装:



Copy Source | Copy HTML<br/> def render_partial_collection (options = {}) #:nodoc: <br/> return nil if options[:collection].blank?<br/> <br/> partial = options[:partial]<br/> spacer = options[:spacer_template] ? render (:partial => options[:spacer_template]) : '' <br/> local_assigns = options[:locals] ? options[:locals].clone : {}<br/> as = options[:as]<br/> <br/> index = 0 <br/> options[:collection].map do |object|<br/> _partial_path ||= partial ||<br/> ActionController::RecordIdentifier.partial_path(object, controller. class .controller_path)<br/> template = _pick_partial_template(_partial_path)<br/> local_assigns[template.counter_name] = index<br/> result = template.render_partial( self , object, local_assigns.dup, as)<br/> index += 1 <br/> result<br/> end .join(spacer).html_safe!<br/> end <br/>





重要な部分は、サイクル内で発生したことです。これは、パーシャルの大規模なコレクションで何百回も発生する可能性があります。 この場合、Merbの実装のパフォーマンスが向上し、Railsに移植できました。 これがMerbの実装です。



Copy Source | Copy HTML<br/>with = [opts. delete (:with)].flatten<br/>as = (opts. delete (:as) || template.match(%r[(?:.*/)?_([^\./]*)])[ 1 ]).to_sym<br/> <br/> # Ensure that as is in the locals hash even if it isn't passed in here <br/> # so that it's included in the preamble. <br/>locals = opts.merge(:collection_index => - 1 , :collection_size => with.size, as => opts[as])<br/>template_method, template_location = _template_for(<br/> template,<br/> opts. delete (: format ) || content_type,<br/> kontroller,<br/> template_path,<br/> locals.keys)<br/> <br/> # this handles an edge-case where the name of the partial is _foo.* and your opts <br/> # have :foo as a key. <br/>named_local = opts.key?(as)<br/> <br/>sent_template = with.map do |temp|<br/> locals[as] = temp unless named_local<br/> <br/> if template_method && self . respond_to ?(template_method)<br/> locals[:collection_index] += 1 <br/> send(template_method, locals)<br/> else <br/> raise TemplateNotFound, "Could not find template at #{template_location}.*" <br/> end <br/> end .join<br/> <br/>sent_template <br/>





現在、これは理想からほど遠いことを理解しています。 ここでは多くのことが行われています(そして、私は個人的にこのメソッドをリファクタリングすることを望んでいます)。 しかし、興味深い部分はループ内で何が起こるかです( sent_template = with.map



)。 テンプレート名を検索し、テンプレートオブジェクト、カウンタ名などを取得したActionViewとは異なり、Merbはいくつかのハッシュ値を設定してメソッドを呼び出すことにより、ループ内のアクティビティを制限しました。



100個のパーシャルのコレクションの場合、コストの差は約10ミリ秒と3ミリ秒になります。 小さなパーシャルのコレクションでは、これが顕著でした(そして、そもそもパーシャルに対応するインラインパーシャルの理由)。



Rails 3では、ループ内で発生することを減らすことでパフォーマンスを改善しました。 残念ながら、Railsの1つの機能により、最適化が少し難しくなりました。 特に、異種コレクション(たとえば、Post、Article、Pageオブジェクトを含むコレクション)を使用してパーシャルをレンダリングでき、Railsは各オブジェクトの正しいテンプレートをレンダリングします(Articleオブジェクトは_article.html.erb



などでレンダリングされます) 。 これは、レンダリングする必要があるテンプレートを特定することが常に可能であるとは限らないことを意味します。



この問題に直面して、異機種混在の場合を完全に最適化することはできませんでしたが、 render :partial => "name", :collection => @array



高速化しました。 これを実現するために、ロジックを2つの方法に分割します。テンプレートを知っている場合は高速で、オブジェクトに応じて定義する必要がある場合は低速です。



テンプレートがわかっている場合、コレクションのレンダリングは次のようになります。



Copy Source | Copy HTML<br/> def collection_with_template (template = @template)<br/> segments, locals, as = [], @locals, @options[:as] || template.variable_name<br/> <br/> counter_name = template.counter_name<br/> locals[counter_name] = - 1 <br/> <br/> @collection.each do |object|<br/> locals[counter_name] += 1 <br/> locals[as] = object<br/> <br/> segments < < template. render (@view, locals)<br/> end <br/> <br/> @template = template<br/> segments<br/> end <br/>





重要なのは、サイクル自体が小さくなったことです(Merbのサイクル内で発生したものよりもさらに単純です)。 注目に値するのは、コードのパフォーマンスを改善する過程で、状態を追跡するためにPartialRendererオブジェクトを作成したことです。 新しいオブジェクトの作成には費用がかかると思うかもしれませんが、Rubyでのオブジェクトの作成は比較的安価であり、オブジェクトは手続き型コードでははるかに複雑なキャッシング機能を提供できることがわかります。



写真の改善を見たい人のために、いくつかのことがあります。まず、Ruby 1.9でのRails 2.3とRails 3の改善(バーが小さいほど速度が速くなります)。

画像



しかし、より高価な操作の場合:

画像



最後に、4つのプラットフォーム(Ruby 1.8、Ruby 1.9、Rubinius、およびJRuby)でのRails 3の比較:

画像



ご覧のとおり、Rails 3はRails 2.3よりも著しく高速であり、Rubyiusを含むすべてのプラットフォームがRuby 1.8よりも著しく改善されています。 全体として、Rubyにとって素晴らしい年です!



次回の投稿では、プラグイン作成者向けのRails 3 APIの改善についてお話します-メッセージに注目し、いつものようにコメントを残してください!



All Articles