Kevin Scaldeferri
2009-May-08 03:48 UTC
Why does ActionController::Response write line-by-line to the network?
I''ve been doing some benchmarking of our application recently, and comparing implementing components in Metal vs. traditional controller actions. One thing I discovered was that there was a substantial gap between the time reported within Rails to serve pages, versus what apachebench measured. Also, this gap was much larger for the traditional serving compared to Metal. In one case, ab reported 65ms more than what was seen inside Rails. The same page served via Metal had only a 3.5ms gap between the two measurements. I was pretty puzzled until I finally discovered that the controller returned an ActionController::Response to Rack, whereas the metal component was returning an [String]. Going to the source I found this def each(&callback) if @body.respond_to?(:call) @writer = lambda { |x| callback.call(x) } @body.call(self, self) elsif @body.is_a?(String) @body.each_line(&callback) <======== look here else @body.each(&callback) end @writer = callback @block.call(self) if @block end In the case where the body of the response is a String (which always seems to be the case in our application; I''m not sure how you get the other cases), each() yields each line of the response individually. As a result, every line in the output leads to a socket write and a system call. For the particular page I was looking at, admittedly rather substantial, this was 1300 calls. This is, to put it mildly, bad for performance. I changed that line to "[@body].each(&callback)", and *poof* the page got 65ms faster. This particular line was added in http://github.com/rails/rails/commit/5eb893f72d189a7ad82c9a6f45873e2317f202d5 , which doesn''t seem to give any particular justification for iterating by line, rather than just returning the whole string. Is there any argument for why it should be this way? It seems to me like it can only be a pessimization. -kevin
Joshua Peek
2009-May-08 04:24 UTC
Re: Why does ActionController::Response write line-by-line to the network?
That commit was probably made for 1.9 compatibility. Since then there were some changes to the actual rack SPEC that the body must respond to each and can no longer be a string. @body.each_line(&callback) should probably just be changed to yield @body Can you test that and see if it fixes your performance problems. On May 7, 10:48 pm, Kevin Scaldeferri <kevin+goo...@scaldeferri.com> wrote:> I''ve been doing some benchmarking of our application recently, and > comparing implementing components in Metal vs. traditional controller > actions. One thing I discovered was that there was a substantial gap > between the time reported within Rails to serve pages, versus what > apachebench measured. Also, this gap was much larger for the > traditional serving compared to Metal. In one case, ab reported 65ms > more than what was seen inside Rails. The same page served via Metal > had only a 3.5ms gap between the two measurements. I was pretty > puzzled until I finally discovered that the controller returned an > ActionController::Response to Rack, whereas the metal component was > returning an [String]. Going to the source I found this > > def each(&callback) > if @body.respond_to?(:call) > @writer = lambda { |x| callback.call(x) } > @body.call(self, self) > elsif @body.is_a?(String) > @body.each_line(&callback) <======== look here > else > @body.each(&callback) > end > > @writer = callback > @block.call(self) if @block > end > > In the case where the body of the response is a String (which always > seems to be the case in our application; I''m not sure how you get the > other cases), each() yields each line of the response individually. > As a result, every line in the output leads to a socket write and a > system call. For the particular page I was looking at, admittedly > rather substantial, this was 1300 calls. This is, to put it mildly, > bad for performance. I changed that line to > "[@body].each(&callback)", and *poof* the page got 65ms faster. > > This particular line was added inhttp://github.com/rails/rails/commit/5eb893f72d189a7ad82c9a6f45873e23... > , which doesn''t seem to give any particular justification for > iterating by line, rather than just returning the whole string. Is > there any argument for why it should be this way? It seems to me like > it can only be a pessimization. > > -kevin
Kevin Scaldeferri
2009-May-08 14:18 UTC
Re: Why does ActionController::Response write line-by-line to the network?
Yes, that change fixes the performance issue. -kevin On May 7, 2009, at 9:24 PM, Joshua Peek wrote:> > That commit was probably made for 1.9 compatibility. Since then there > were some changes to the actual rack SPEC that the body must respond > to each and can no longer be a string. > > @body.each_line(&callback) > > should probably just be changed to > > yield @body > > Can you test that and see if it fixes your performance problems. > > On May 7, 10:48 pm, Kevin Scaldeferri <kevin+goo...@scaldeferri.com> > wrote: >> I''ve been doing some benchmarking of our application recently, and >> comparing implementing components in Metal vs. traditional controller >> actions. One thing I discovered was that there was a substantial gap >> between the time reported within Rails to serve pages, versus what >> apachebench measured. Also, this gap was much larger for the >> traditional serving compared to Metal. In one case, ab reported 65ms >> more than what was seen inside Rails. The same page served via Metal >> had only a 3.5ms gap between the two measurements. I was pretty >> puzzled until I finally discovered that the controller returned an >> ActionController::Response to Rack, whereas the metal component was >> returning an [String]. Going to the source I found this >> >> def each(&callback) >> if @body.respond_to?(:call) >> @writer = lambda { |x| callback.call(x) } >> @body.call(self, self) >> elsif @body.is_a?(String) >> @body.each_line(&callback) <======== look here >> else >> @body.each(&callback) >> end >> >> @writer = callback >> @block.call(self) if @block >> end >> >> In the case where the body of the response is a String (which always >> seems to be the case in our application; I''m not sure how you get the >> other cases), each() yields each line of the response individually. >> As a result, every line in the output leads to a socket write and a >> system call. For the particular page I was looking at, admittedly >> rather substantial, this was 1300 calls. This is, to put it mildly, >> bad for performance. I changed that line to >> "[@body].each(&callback)", and *poof* the page got 65ms faster. >> >> This particular line was added inhttp://github.com/rails/rails/commit/5eb893f72d189a7ad82c9a6f45873e23 >> ... >> , which doesn''t seem to give any particular justification for >> iterating by line, rather than just returning the whole string. Is >> there any argument for why it should be this way? It seems to me >> like >> it can only be a pessimization. >> >> -kevin > >