Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

But At What Cost?

But At What Cost?

Building a template processor for Rails.

Aaron Patterson

May 02, 2019
Tweet

More Decks by Aaron Patterson

Other Decks in Technology

Transcript

  1. "

  2. Java (1.3?) vs Ruby List list = new ArrayList(); list.add("1");

    list.add("2"); list.add("3"); Iterator iter = list.iterator(); List intList = new ArrayList(); while (iter.hasNext()) { String str = (String)iter.next(); intList.add(Integer.parseInt(str)); } list = %w{ 1 2 3 } intList = list.map { |l| l.to_i } Java Ruby
  3. Parens vs No Parens def foo1(bar, baz) end def foo2(bar,

    baz) end def foo3(bar, baz) end def foo1 bar, baz end def foo2 bar, baz end def foo3 bar, baz end Parens No Parens
  4. Parse States $ ruby --disable-gems -y with-parens.rb | wc -l

    805 $ ruby --disable-gems -y without-parens.rb | wc -l 778
  5. Allocation Count Started GET "/users/new" for ::1 at 2019-04-30 16:45:49

    -0500 Processing by UsersController#new as */* Rendering users/new.html.erb within layouts/application Rendered users/_form.html.erb (Duration: 1.4ms | Allocations: 552) Rendered users/new.html.erb within layouts/application (Duration: 1.7ms | Allocations: 640) Completed 200 OK in 6ms (Views: 5.4ms | ActiveRecord: 0.0ms | Allocations: 4140) Started GET "/users/new" for ::1 at 2019-04-30 16:47:02 -0500 Processing by UsersController#new as */* Rendering users/new.html.erb within layouts/application Rendered users/_form.html.erb (Duration: 1.4ms | Allocations: 827) Rendered users/_expensive.html.erb (Duration: 1.6ms | Allocations: 10098) Rendered users/new.html.erb within layouts/application (Duration: 4.3ms | Allocations: 11553) Completed 200 OK in 9ms (Views: 8.5ms | ActiveRecord: 0.0ms | Allocations: 15981)
  6. Allocation Count Started GET "/users/new" for ::1 at 2019-04-30 16:45:49

    -0500 Processing by UsersController#new as */* Rendering users/new.html.erb within layouts/application Rendered users/_form.html.erb (Duration: 1.4ms | Allocations: 552) Rendered users/new.html.erb within layouts/application (Duration: 1.7ms | Allocations: 640) Completed 200 OK in 6ms (Views: 5.4ms | ActiveRecord: 0.0ms | Allocations: 4140) Started GET "/users/new" for ::1 at 2019-04-30 16:47:02 -0500 Processing by UsersController#new as */* Rendering users/new.html.erb within layouts/application Rendered users/_form.html.erb (Duration: 1.4ms | Allocations: 827) Rendered users/_expensive.html.erb (Duration: 1.6ms | Allocations: 10098) Rendered users/new.html.erb within layouts/application (Duration: 4.3ms | Allocations: 11553) Completed 200 OK in 9ms (Views: 8.5ms | ActiveRecord: 0.0ms | Allocations: 15981)
  7. ERB

  8. ERB Compiler require "erb" # Compile the template template =

    ERB.new File.read "test.html.erb" # Print the template source puts template.src # Evaluate the template puts template.result(binding) compile.rb
  9. Compiled Template _erbout = +"" _erbout << "<h1>New User</h1>\n\n".freeze _erbout

    << render_form.to_s _erbout << "\n\n".freeze _erbout << link_to("Back").to_s _erbout << "\n".freeze _erbout
  10. Modified Compile Script require "erb" def render_form "<form></form>" end def

    link_to(text) "<a href=\"#\">#{text}</a>" end # Compile the template template = ERB.new File.read "test.html.erb" # Evaluate the template puts template.result(binding)
  11. ERB Translations ERB Source Translated Ruby <%= something %> _erbout

    << something.to_s <% something_else %> something_else
  12. Capturing Blocks <div> <%= capture do %> Hello! <% end

    %> </div> require "erb" def capture yield end # Compile the template template = ERB.new File.read "test.html.erb" puts template.result(binding)
  13. Compiled Source _erbout = +'' _erbout << "<div>\n ".freeze _erbout

    << ( capture do ).to_s _erbout << "\n Hello!\n ".freeze end _erbout << "\n</div>\n".freeze _erbout <div> <%= capture do %> Hello! <% end %> </div>
  14. Compiled Source _erbout = +'' _erbout << "<div>\n ".freeze _erbout

    << ( capture do ).to_s _erbout << "\n Hello!\n ".freeze end _erbout << "\n</div>\n".freeze _erbout <div> <%= capture do %> Hello! <% end %> </div>
  15. ERB Compiler BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ def add_expression(indicator, code) flush_newline_if_pending(src) if

    (indicator == "==") || @escape src << "@output_buffer.safe_expr_append=" else src << "@output_buffer.append=" end if BLOCK_EXPR.match?(code) src << " " << code else src << "(" << code << ");" end end
  16. Rendering Speed require "erb" require "benchmark/ips" class AaronView TEMPLATES =

    { } TEMPLATES["index"] = <<-eerb <html> <body> <h1>Hello <%= name %><h1> </html> eerb def name; "Aaron"; end def render(template) ERB.new(TEMPLATES[template]).result(binding) end end view = AaronView.new Benchmark.ips do |x| x.report("render") { view.render("index") } end
  17. Rendering Speed $ ruby rendering_speed.rb Warming up -------------------------------------- render 2.696k

    i/100ms Calculating ------------------------------------- render 27.117k (± 5.9%) i/s - 137.496k in 5.092633s
  18. Cache Compilation class AaronView TEMPLATES = { } TEMPLATES["index"] =

    <<-eerb <html> <body> <h1>Hello <%= name %><h1> </html> eerb def name; "Aaron"; end def render(template) ERB.new(TEMPLATES[template]).result(binding) end COMPILED_TEMPLATES = { } def render_compiled(template) erb = COMPILED_TEMPLATES[template] ||= ERB.new(TEMPLATES[template]) erb.result(binding) end end view = AaronView.new Benchmark.ips do |x| x.report("render") { view.render("index") } x.report("render compiled") { view.render_compiled("index") } x.compare! end
  19. Cache Compilation $ ruby rendering_speed.rb Warming up -------------------------------------- render 2.709k

    i/100ms render compiled 6.293k i/100ms Calculating ------------------------------------- render 27.115k (± 2.6%) i/s - 138.159k in 5.098881s render compiled 63.645k (± 3.9%) i/s - 320.943k in 5.050544s Comparison: render compiled: 63645.5 i/s render: 27114.8 i/s - 2.35x slower
  20. Define a Method class AaronView TEMPLATES = { } TEMPLATES["index"]

    = <<-eerb <html> <body> <h1>Hello <%= name %><h1> </html> eerb def name; "Aaron"; end def render(template) ERB.new(TEMPLATES[template]).result(binding) end COMPILED_TEMPLATES = { } def render_compiled(template) erb = COMPILED_TEMPLATES[template] ||= ERB.new(TEMPLATES[template]) erb.result(binding) end METHODS_DEFINED = { } def render_with_method(template) unless METHODS_DEFINED.key? template erb = ERB.new(TEMPLATES[template]) self.class.class_eval "def render_#{template}; #{erb.src}; end" METHODS_DEFINED[template] = true end send "render_#{template}" end end
  21. Define a Method $ ruby rendering_speed.rb Warming up -------------------------------------- render

    2.750k i/100ms render compiled 6.462k i/100ms render method 114.420k i/100ms Calculating ------------------------------------- render 28.389k (± 2.7%) i/s - 143.000k in 5.041158s render compiled 65.860k (± 2.4%) i/s - 329.562k in 5.006962s render method 1.477M (± 1.7%) i/s - 7.437M in 5.038304s Comparison: render method: 1476591.3 i/s render compiled: 65860.1 i/s - 22.42x slower render: 28388.6 i/s - 52.01x slower
  22. Method Generation require "erb" erb_source = <<-erb <html> <body> <h1>Hello

    <%= name %><h1> </html> erb erb = ERB.new erb_source template = "index" self.class.class_eval "def render_#{template}; #{erb.src}; end" send "render_#{template}" Template Source Compile Source Define a m ethod Render the Template
  23. Method Generation def render_index _erbout = +'' _erbout << "<html>\n

    <body>\n <h1>Hello ".freeze _erbout << name.to_s _erbout << "<h1>\n</html>\n".freeze _erbout end Compiled Source
  24. Rails Template Methods <h1> This is an inner template <%

    puts methods.grep(/_app/) %> </h1> Template in Rails
  25. Memory Size def render_index _erbout = +'' _erbout << "<html>\n

    <body>\n <h1>Hello ".freeze _erbout << name.to_s _erbout << "<h1>\n</html>\n".freeze _erbout end require "objspace" iseq = RubyVM::InstructionSequence.of( method(:render_index)) p ObjectSpace.memsize_of iseq $ ruby test.rb 1264
  26. Instance Variable Visibility <h1>This is template Foo!</h1> <%= @some_ivar %>

    <h1>This is template Bar!</h1> <%= @some_ivar %> _foo.html.erb _bar.html.erb
  27. Compiled Templates class ActionView::Base def render_foo _erbout = +'' _erbout

    << "<h1>This is template Foo!</h1>\n".freeze _erbout << @some_ivar.to_s _erbout << "\n".freeze _erbout end def render_bar _erbout = +'' _erbout << "<h1>This is template Barr!</h1>\n".freeze _erbout << @some_ivar.to_s _erbout << "\n".freeze _erbout end end _foo.html.erb _bar.html.erb Same Instance Same Instance
  28. Instance Variable Visibility <h1>This is template Foo!</h1> <%= @some_ivar %>

    <h1>This is template Bar!</h1> <%= @some_ivar %> _foo.html.erb _bar.html.erb
  29. Compiling With Locals <h1> Hello, <%= name %> </h1> def

    render_content _erbout = +'' _erbout << "<h1>\n Hello, ".freeze _erbout << name.to_s _erbout << "\n</h1>\n".freeze _erbout end content.html.erb Compiled Method Method Call
  30. Compiling With Locals <h1> Hello, <%= name %> </h1> def

    render_content(locals) # Locals preamble name = locals[:name] _erbout = +'' _erbout << "<h1>\n Hello, ".freeze _erbout << name.to_s _erbout << "\n</h1>\n".freeze _erbout end content.html.erb Compiled Method
  31. Templates Require Context <body> <%= render "content", locals: { name:

    "Gorby" } %> <%= render "content" %> </body> <h1> Hello, <%= name %> </h1> main.html.erb content.html.erb Method call or local variable? local variable method call
  32. Too Many Compilations <body> <%= render "content", locals: { name:

    "Gorby" } %> <%= render "content", locals: { name: "Gorby", friend: "Aaron" } %> </body> def render_content_1(locals) # Locals preamble name = locals[:name] _erbout = +'' _erbout << "<h1>\n Hello, ".freeze _erbout << name.to_s _erbout << "\n</h1>\n".freeze _erbout end def render_content_2(locals) # Locals preamble name = locals[:name] friend = locals[:friend] _erbout = +'' _erbout << "<h1>\n Hello, ".freeze _erbout << name.to_s _erbout << "\n</h1>\n".freeze _erbout end main.html.erb Compiled "content" Unique Preambles
  33. Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name
  34. Users Controller class UsersController < ApplicationController # GET /users #

    GET /users.json def index @users = User.all render "index" end end $ tree app/views/users app/views/users !"" index.html.erb #"" index.xml.erb Controller app/views/users/*
  35. Requests and Rendering Request Response Template Rendered curl -H 'Accept:

    application/xml' http:// localhost:3000/users <h1>XML template</h1> index.xml.erb curl http://localhost: 3000/users <h1>Users</h1> index.html.erb curl -H 'Accept: text/ html' http://localhost: 3000/users <h1>Users</h1> index.html.erb
  36. Users Controller class UsersController < ApplicationController # GET /users #

    GET /users.json def index @users = User.all render "index" end end $ tree app/views/users app/views/users !"" _my_template.png.erb !"" _my_template.xml.erb #"" index.html.erb Controller app/views/users/*
  37. Template Contents <h1>Users</h1> <%= render "my_template" %> I guess this

    is a png? index.html.erb _my_template.png.erb <cool> XML is cool! </cool> _my_template.xml.erb
  38. Requests and Rendering Request Response Template Rendered curl -H 'Accept:

    application/xml' http://localhost:3000/users Missing XML template Error None curl -H 'Accept: text/html' http://localhost:3000/users Missing HTML partial Error None Browser Missing HTML partial Error None curl http://localhost:3000/users <h1>Users</h1> I guess this is a png? index.html.erb _my_template.png.erb
  39. Requests and Rendering Request Response Template Rendered curl -H 'Accept:

    application/xml' http://localhost:3000/users Missing XML template Error None curl -H 'Accept: text/html' http://localhost:3000/users Missing HTML partial Error None Browser Missing HTML partial Error None curl http://localhost: 3000/users <h1>Users</h1> I guess this is a png? index.html.erb _my_template.png.erb
  40. respond_to Controller class UsersController < ApplicationController # GET /users #

    GET /users.json def index @users = User.all render "index" end end class UsersController < ApplicationController # GET /users # GET /users.json def index @users = User.all respond_to do |format| format.html do render "index" end end end end "Bare" render respond_to render
  41. Requests and Rendering Request Response Template Rendered curl -H 'Accept:

    application/ xml' http://localhost:3000/ users Missing XML template Error None curl -H 'Accept: text/html' http://localhost:3000/users Missing HTML partial Error None Browser Missing HTML partial Error None curl http://localhost:3000/ users Missing HTML partial Error None
  42. Context Dependent <h1>Users</h1> <%= render "my_template" %> class UsersController <

    ApplicationController # GET /users # GET /users.json def index @users = User.all render "index" end end class UsersController < ApplicationController # GET /users # GET /users.json def index @users = User.all respond_to do |format| format.html do render "index" end end end end index.html.erb
  43. Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name Cheap! Cheap! Expensive!
  44. What Method Will This Call? <h1>Users</h1> <%= render "my_template" %>

    <h1>Users</h1> <%= render_my_template_00011123abf %> index.html.erb translated template Calls a unique method. But what method?
  45. Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name Cheap! Cheap! Expensive!
  46. Three Templates <ul> <%= render partial: "customer", collection: customers %>

    </ul> <ul> <%= render partial: "customer", collection: customers, cached: true %> </ul> _col.html.erb _cached_col.html.erb <li> Hello: <%= customer.name %> </li> _customer.html.erb
  47. Results $ be ruby render_benchmark.rb -- create_table(:customers, {:force=>true}) -> 0.0108s

    <#ActiveSupport::Cache::MemoryStore entries=1000, size=333893, options={}> Warming up -------------------------------------- collection render: no cache 6.000 i/100ms collection render: with cache 1.000 i/100ms Calculating ------------------------------------- collection render: no cache 65.319 (± 7.7%) i/s - 330.000 in 5.080651s collection render: with cache 11.504 (± 8.7%) i/s - 58.000 in 5.071030s Comparison: collection render: no cache: 65.3 i/s collection render: with cache: 11.5 i/s - 5.68x slower
  48. Profiled The Cache ================================== Mode: wall(1000) Samples: 4690 (0.00% miss

    rate) GC: 207 (4.41%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 594 (12.7%) 594 (12.7%) ActiveModel::LazyAttributeHash#[] 387 (8.3%) 387 (8.3%) ActiveRecord::Transactions#update_attributes_from_transaction_state 1199 (25.6%) 246 (5.2%) ActiveSupport::Cache::Store#expanded_key 301 (6.4%) 218 (4.6%) AbstractController::Caching::Fragments#combined_fragment_cache_key 207 (4.4%) 207 (4.4%) (garbage collection) 932 (19.9%) 196 (4.2%) ActionView::CollectionCaching#collection_by_cache_keys 450 (9.6%) 181 (3.9%) ActiveSupport::Cache::Store#expanded_version
  49. Cache Payoff <li> Hello: <%= customer.name %> </li> <li>Hello: <%=

    customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> <li>Hello: <%= customer.name %></li> Old Template New Template
  50. Benchmark Results $ be ruby render_benchmark.rb -- create_table(:customers, {:force=>true}) ->

    0.0075s <#ActiveSupport::Cache::MemoryStore entries=1000, size=900893, options={}> Warming up -------------------------------------- collection render: no cache 4.000 i/100ms collection render: with cache 4.000 i/100ms Calculating ------------------------------------- collection render: no cache 41.066 (± 2.4%) i/s - 208.000 in 5.070051s collection render: with cache 43.192 (± 4.6%) i/s - 216.000 in 5.012803s Comparison: collection render: with cache: 43.2 i/s collection render: no cache: 41.1 i/s - same-ish: difference falls within error
  51. Static Analysis <%= render "no_locals" %> <%= render "static_locals", locals:

    { foo: bar } %> <%= render "dynamic_locals", locals: { foo: bar }.merge(baz) %> Can Precompile! Can Precompile! Nope!
  52. "Same Format Assumption" <h1>Users</h1> <%= render "my_template" %> <%= render

    "my_template", format: :png %> $ tree app/views/users app/views/users !"" _my_template.html.erb !"" _my_template.png.erb !"" _my_template.xml.erb #"" index.html.erb index.html.erb templates
  53. Ambiguous Render Problem <h1>Users</h1> <%= render "my_template" %> <%= render

    "my_template", format: :html %> $ tree app/views/users app/views/users !"" _my_template.png.erb !"" _my_template.xml.erb #"" index.html.erb index.html.erb templates Sometimes an exception Sometimes a PNG Always an exception
  54. "Always Optimize" <h1>Users</h1> <%= render "my_template" %> <%= render "my_template",

    format: :html %> <h1>Users</h1> <%= render "my_template", format: :png %> <%= render "my_template", format: :html %> index.html.erb index.html.erb
  55. "Same Format Assumption" <h1>Users</h1> <%= render "my_template" %> <%= render

    "my_template", format: :png %> $ tree app/views/users app/views/users !"" _my_template.html.erb !"" _my_template.png.erb !"" _my_template.xml.erb #"" index.html.erb index.html.erb templates
  56. "Same Format Assumption" <h1>Users</h1> <%= render "my_template" %> <%= render

    "my_template", format: :png %> <h1>Users</h1> <%= render_my_template_html_00123abc %> <%= render_my_template_png_00123abc %> index.html.erb Optimized Template
  57. Ambiguous Render Problem <h1>Users</h1> <%= render "my_template" %> <%= render

    "my_template", format: :html %> $ tree app/views/users app/views/users !"" _my_template.png.erb !"" _my_template.xml.erb #"" index.html.erb index.html.erb templates
  58. Ambiguous Render Problem <h1>Users</h1> <%= render "my_template" %> <%= render

    "my_template", format: :html %> <h1>Users</h1> <%= render "my_template" %> <%= raise MissingTemplateError %> index.html.erb Optimized Template