Details
-
Type:
Bug
-
Status:
Resolved
-
Priority:
Blocker
-
Resolution: Fixed
-
Affects Version/s: JRuby 1.6RC1, JRuby 1.6RC2
-
Fix Version/s: JRuby 1.6RC3
-
Component/s: None
-
Labels:None
-
Environment:Mac OS X 10.6
-
Number of attachments :
Description
The rendering of rails views is consistently 4x slower in JRuby 1.6 vs JRuby 1.5.6
JRuby 1.5.6:
Rendered config/systems/Financial/views/cases/_address_fields.html.haml (116.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (250.0ms) Rendered config/systems/Financial/views/cases/_address_fields.html.haml (77.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (75.0ms) Rendered config/systems/Financial/views/cases/_applicants_infos.html.haml (510.0ms)
Jruby 1.6.0RC:
Rendered config/systems/Financial/views/cases/_address_fields.html.haml (467.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (1045.0ms) Rendered config/systems/Financial/views/cases/_address_fields.html.haml (439.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (506.0ms) Rendered config/systems/Financial/views/cases/_applicants_infos.html.haml (2054.0ms)
Ok, so I figured out where the problem lies. It's not in view rendering...it's in the i18n gem.
In JRuby 1.6, we moved to using JVM-level facilities for generating exception backtraces. These facilities are considerably more expensive than the artificial backtraces we had been maintaining before...not incredibly expensive, but not free or as cheap as they used to be by any means. If applications and libraries use exceptions for exceptional error cases, it's not a big deal.
I ran a profile of your app and saw that it was spending as much as 40% of its time generating backtraces. That's a tremendous amount; even a 1000-call-deep trace (which is several thousand Java frames) takes only a couple ms to generate.
~/projects/jruby/tmp/case_center ➔ jruby -rbenchmark -e "def foo(a); if a > 1000; 1000.times { begin; raise; rescue Exception; end }; else; foo(a + 1); end; end; 10.times { puts Benchmark.measure { foo(1) } }" 1.721000 0.000000 1.721000 ( 1.602000) 1.586000 0.000000 1.586000 ( 1.586000) 1.562000 0.000000 1.562000 ( 1.562000) 1.565000 0.000000 1.565000 ( 1.565000) 1.564000 0.000000 1.564000 ( 1.564000) 1.577000 0.000000 1.577000 ( 1.577000) 1.563000 0.000000 1.563000 ( 1.563000) 1.555000 0.000000 1.555000 ( 1.555000) 1.552000 0.000000 1.552000 ( 1.552000) 1.560000 0.000000 1.560000 ( 1.560000)In order for exceptions to be impacting performance, there would have to be hundreds of them generated and thrown away. So I added logging to exception backtrace generation, to see where they were coming from. It turns out that the vast majority were from the following code in i18n:
def translate(locale, key, options = {}) raise InvalidLocale.new(locale) unless locale entry = key && lookup(locale, key, options[:scope], options) if options.empty? entry = resolve(locale, key, entry, options) else count, default = options.values_at(:count, :default) values = options.except(*RESERVED_KEYS) entry = entry.nil? && default ? default(locale, key, default, options) : resolve(locale, key, entry, options) end raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil? entry = entry.dup if entry.is_a?(String) entry = pluralize(locale, entry, count) if count entry = interpolate(locale, entry, values) if values entry end ... def resolve(locale, object, subject, options = {}) return subject if options[:resolve] == false case subject when Symbol return I18n.translate(subject, options.merge(:locale => locale, :raise => true)) when Proc date_or_time = options.delete(:object) || object return resolve(locale, object, subject.call(date_or_time, options)) else return subject end rescue MissingTranslationData nil endEssentially, it's using exceptions as flow control here; the exception itself is never used, and the resulting overhead from all those backtraces gives us the performance you (and I) see. To make matters worse, it seems to be raising this exception repeatedly rather than caching some result somewhere. Even if someone were to claim JRuby's backtrace generation is too slow, this is still a serious code antipattern.
With those exceptions being raised, we end up with the following performance similar to yours:
Started GET "/cases/new?system=financial&type=consumer" for 0:0:0:0:0:0:0:1%0 at Wed Feb 23 21:50:08 -0600 2011 Processing by CasesController#new as */* Parameters: {"system"=>"financial", "type"=>"consumer"} Rendered config/systems/Financial/views/cases/_address_fields.html.haml (189.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (435.0ms) Rendered config/systems/Financial/views/cases/_address_fields.html.haml (185.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (225.0ms) Rendered config/systems/Financial/views/cases/_applicants_infos.html.haml (876.0ms) Rendered cases/new.html.haml within layouts/application (887.0ms) Completed 200 OK in 1071ms (Views: 922.0ms | ActiveRecord: 0.0ms)I modified i18n to not raise exceptions but instead use catch/throw to unroll the stack, and the performance difference is pretty shocking:
Started GET "/cases/new?system=financial&type=consumer" for 0:0:0:0:0:0:0:1%0 at Wed Feb 23 21:51:20 -0600 2011 Processing by CasesController#new as */* Parameters: {"system"=>"financial", "type"=>"consumer"} Rendered config/systems/Financial/views/cases/_address_fields.html.haml (21.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (68.0ms) Rendered config/systems/Financial/views/cases/_address_fields.html.haml (22.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (27.0ms) Rendered config/systems/Financial/views/cases/_applicants_infos.html.haml (167.0ms) Rendered cases/new.html.haml within layouts/application (177.0ms) Completed 200 OK in 364ms (Views: 213.0ms | ActiveRecord: 0.0ms)This is not quite back to 1.5.6 performance, but it's close. 1.6 with the fixed i18n seems to average from 290ms to 400ms after a few requests, where 1.5.6 averages 200ms to 350ms. And 1.5.6 without the fixed backtraces There are additional exceptions being raised and swallowed by Rails that I'll also look into.
~/projects/jruby/tmp/case_center ➔ jruby -rbenchmark -e "def foo(a); if a > 1000; 1000.times { begin; raise; rescue Exception; end }; else; foo(a + 1); end; end; 10.times { puts Benchmark.measure { foo(1) } }" 1.721000 0.000000 1.721000 ( 1.602000) 1.586000 0.000000 1.586000 ( 1.586000) 1.562000 0.000000 1.562000 ( 1.562000) 1.565000 0.000000 1.565000 ( 1.565000) 1.564000 0.000000 1.564000 ( 1.564000) 1.577000 0.000000 1.577000 ( 1.577000) 1.563000 0.000000 1.563000 ( 1.563000) 1.555000 0.000000 1.555000 ( 1.555000) 1.552000 0.000000 1.552000 ( 1.552000) 1.560000 0.000000 1.560000 ( 1.560000)def translate(locale, key, options = {}) raise InvalidLocale.new(locale) unless locale entry = key && lookup(locale, key, options[:scope], options) if options.empty? entry = resolve(locale, key, entry, options) else count, default = options.values_at(:count, :default) values = options.except(*RESERVED_KEYS) entry = entry.nil? && default ? default(locale, key, default, options) : resolve(locale, key, entry, options) end raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil? entry = entry.dup if entry.is_a?(String) entry = pluralize(locale, entry, count) if count entry = interpolate(locale, entry, values) if values entry end ... def resolve(locale, object, subject, options = {}) return subject if options[:resolve] == false case subject when Symbol return I18n.translate(subject, options.merge(:locale => locale, :raise => true)) when Proc date_or_time = options.delete(:object) || object return resolve(locale, object, subject.call(date_or_time, options)) else return subject end rescue MissingTranslationData nil endStarted GET "/cases/new?system=financial&type=consumer" for 0:0:0:0:0:0:0:1%0 at Wed Feb 23 21:50:08 -0600 2011 Processing by CasesController#new as */* Parameters: {"system"=>"financial", "type"=>"consumer"} Rendered config/systems/Financial/views/cases/_address_fields.html.haml (189.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (435.0ms) Rendered config/systems/Financial/views/cases/_address_fields.html.haml (185.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (225.0ms) Rendered config/systems/Financial/views/cases/_applicants_infos.html.haml (876.0ms) Rendered cases/new.html.haml within layouts/application (887.0ms) Completed 200 OK in 1071ms (Views: 922.0ms | ActiveRecord: 0.0ms)Started GET "/cases/new?system=financial&type=consumer" for 0:0:0:0:0:0:0:1%0 at Wed Feb 23 21:51:20 -0600 2011 Processing by CasesController#new as */* Parameters: {"system"=>"financial", "type"=>"consumer"} Rendered config/systems/Financial/views/cases/_address_fields.html.haml (21.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (68.0ms) Rendered config/systems/Financial/views/cases/_address_fields.html.haml (22.0ms) Rendered config/systems/Financial/views/cases/_consumer_fields.html.haml (27.0ms) Rendered config/systems/Financial/views/cases/_applicants_infos.html.haml (167.0ms) Rendered cases/new.html.haml within layouts/application (177.0ms) Completed 200 OK in 364ms (Views: 213.0ms | ActiveRecord: 0.0ms)