Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The View is Clear From Here

The View is Clear From Here

About view compilation in Rails

Aaron Patterson

April 06, 2019
Tweet

More Decks by Aaron Patterson

Other Decks in Technology

Transcript

  1. ERB

  2. 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
  3. 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
  4. 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)
  5. ERB Translations ERB Source Translated Ruby <%= something %> _erbout

    << something.to_s <% something_else %> something_else
  6. 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)
  7. 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>
  8. 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
  9. 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 $ ruby rendering_speed.rb Warming up -------------------------------------- render 2.696k i/100ms Calculating ------------------------------------- render 27.117k (± 5.9%) i/s - 137.496k in 5.092633s
  10. 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 $ 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
  11. 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 $ 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
  12. 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}" def render_index _erbout = +'' _erbout << "<html>\n <body>\n <h1>Hello ".freeze _erbout << name.to_s _erbout << "<h1>\n</html>\n".freeze _erbout end Template Source Compile Source Define a m ethod Render the Tem plate Compiled Source
  13. Rails Template Methods <h1> This is an inner template <%

    puts methods.grep(/_app/) %> </h1> Template in Rails Console Output _app_views_users_index_html_erb___4485674087862414886_70282093274980 _app_views_users__first_ja_html_erb__657974686689484881_70282053324000 _app_views_users__second_html_erb___3508242123539422948_70282053523460
  14. 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 Measuring Memory Console Output
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name
  23. 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/*
  24. 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
  25. 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/*
  26. 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
  27. Requests and Rendering Request Response Template Rendered curl -H 'Accept:

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

    application/xml' http://localhost:3000/users Missing XML template Error None curl http://localhost:3000/users Missing HTML partial Error None curl -H 'Accept: text/html' http:// localhost:3000/users Missing HTML partial Error None Browser Missing HTML partial Error None
  30. 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
  31. Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name Cheap! Cheap! Expensive!
  32. 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?
  33. Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name Cheap! Cheap! Expensive!
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. "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
  40. 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
  41. "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
  42. "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
  43. "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
  44. 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
  45. 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