But At What Cost?

But At What Cost?

Building a template processor for Rails.

F29327647a9cff5c69618bae420792ea?s=128

Aaron Patterson

May 02, 2019
Tweet

Transcript

  1. 1.
  2. 6.
  3. 8.
  4. 12.
  5. 13.
  6. 14.
  7. 15.
  8. 16.
  9. 17.
  10. 18.
  11. 21.
  12. 22.
  13. 25.

    "

  14. 26.
  15. 27.
  16. 28.
  17. 29.
  18. 30.
  19. 33.
  20. 34.
  21. 41.
  22. 43.
  23. 45.
  24. 46.
  25. 51.
  26. 53.
  27. 56.

    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
  28. 60.
  29. 70.
  30. 71.
  31. 73.
  32. 76.
  33. 79.

    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
  34. 81.
  35. 82.

    Parse States $ ruby --disable-gems -y with-parens.rb | wc -l

    805 $ ruby --disable-gems -y without-parens.rb | wc -l 778
  36. 86.

    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)
  37. 87.

    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)
  38. 100.

    ERB

  39. 103.

    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
  40. 104.

    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
  41. 105.

    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)
  42. 107.

    ERB Translations ERB Source Translated Ruby <%= something %> _erbout

    << something.to_s <% something_else %> something_else
  43. 108.

    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)
  44. 110.

    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>
  45. 111.

    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>
  46. 113.

    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
  47. 117.

    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
  48. 118.

    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
  49. 119.

    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
  50. 120.

    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
  51. 121.

    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
  52. 122.

    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
  53. 123.

    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
  54. 124.

    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
  55. 126.

    Rails Template Methods <h1> This is an inner template <%

    puts methods.grep(/_app/) %> </h1> Template in Rails
  56. 128.

    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
  57. 130.

    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
  58. 131.

    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
  59. 134.

    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
  60. 137.

    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
  61. 139.

    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
  62. 140.

    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
  63. 143.

    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
  64. 148.

    Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name
  65. 151.

    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/*
  66. 153.

    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
  67. 156.

    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/*
  68. 157.

    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
  69. 158.

    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
  70. 159.
  71. 160.

    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
  72. 162.

    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
  73. 163.

    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
  74. 164.

    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
  75. 167.

    Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name Cheap! Cheap! Expensive!
  76. 168.

    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?
  77. 169.

    Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name Cheap! Cheap! Expensive!
  78. 170.
  79. 174.
  80. 175.

    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
  81. 177.

    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
  82. 184.

    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
  83. 186.

    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
  84. 187.

    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
  85. 193.

    Static Analysis <%= render "no_locals" %> <%= render "static_locals", locals:

    { foo: bar } %> <%= render "dynamic_locals", locals: { foo: bar }.merge(baz) %> Can Precompile! Can Precompile! Nope!
  86. 195.

    "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
  87. 196.

    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
  88. 197.

    "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
  89. 198.

    "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
  90. 199.

    "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
  91. 200.

    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
  92. 201.

    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
  93. 202.