The View is Clear From Here

The View is Clear From Here

About view compilation in Rails

F29327647a9cff5c69618bae420792ea?s=128

Aaron Patterson

April 06, 2019
Tweet

Transcript

  1. The View Is Clear from Here

  2. Aaron Patterson

  3. @tenderlove

  4. None
  5. None
  6. None
  7. I have stickers of my cat! (please say "hello")

  8. None
  9. None
  10. Ruby Core Team

  11. Rails Core Team

  12. G GitHub

  13. Le Git

  14. git push -f

  15. git checkout -b

  16. git cherry-pick

  17. None
  18. Happy GPS Rollover Day! April 6, 23:59:42 UTC I guess

    this is actually tomorrow……
  19. GPS Uses 10 bits!

  20. Single Quotes Are 2x Faster Than Double Quotes

  21. You Have To Hit The Shift Key To Type "

  22. Logic AND OR NOT A

  23. Гематоген

  24. "атоген"

  25. gem 'атоген' Gemfile

  26. The View Is Clear from Here

  27. WARNING!@ There Will Be Code

  28. There Will Be Ruby

  29. There Will Be Rails

  30. Software Engineering Practices

  31. Rails is MVC

  32. MVC Model View Controller

  33. MVC Model View Controller Request

  34. MVC Model View Controller Request Action Controller Action View Active

    Record
  35. Action View

  36. Action View Finds Templates Compiles Templates Executes Views

  37. Finding Templates

  38. View Templates

  39. ERB

  40. Developed by @m_seki

  41. ERB Template <h1>New User</h1> <%= render_form %> <%= link_to 'Back'

    %> new.html.erb
  42. 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
  43. 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
  44. 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)
  45. Compiler Output $ ruby compiler.rb <h1>New User</h1> <form></form> <a href="#">Back</a>

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

    << something.to_s <% something_else %> something_else
  47. 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)
  48. What is the output? Hello! <div> </div> <div> Hello! </div>

    ERROR!
  49. 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>
  50. How does this work in Rails?

  51. 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
  52. ERB in Rails

  53. Different ERB Flavors Original ERB ERubis ERubi

  54. ERB Performance

  55. 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
  56. 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
  57. 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
  58. 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
  59. Templates Are Translated to Methods

  60. 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
  61. 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
  62. Template Handling In Rails

  63. 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
  64. 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
  65. Don’t use instance variables in your templates

  66. "Don’t use instance variables in your templates" - Aaron

  67. 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
  68. Local Variables

  69. Rendering With Locals <body> <%= render "content", locals: { name:

    "Gorby" } %> </body>
  70. 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
  71. Preamble With Locals

  72. 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
  73. 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
  74. Templates Can’t Be Compiled In Advance

  75. Compiled Too Many Times

  76. 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
  77. "This is nice, but what should I do?"

  78. Always pass the same locals to individual templates.

  79. The render function

  80. Finds a Template Compiles the Template Calculates a Method Name

    Calls the Method
  81. Find a Template (from the cache) Compiled? Compile the Template

    Call the method Calculate a Method Name
  82. Finding a Template

  83. Template Rendering Depends on "Requested Format"

  84. 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/*
  85. Template Source <h1>XML template</h1> <h1>Users</h1> index.xml.erb index.html.erb

  86. 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
  87. Template Cache Keys • Local variables • Format • Locale

    • Variant (iPhone, etc)
  88. Strange Render Behavior

  89. 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/*
  90. 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
  91. 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
  92. Error!!

  93. We Can’t Predict <h1>Users</h1> <%= render "my_template" %>

  94. 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
  95. 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
  96. 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
  97. Templates are not "Context Free"

  98. Prediction and Consistency

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

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

    Call the method Calculate a Method Name Cheap! Cheap! Expensive!
  102. Slower Production Boot (but could be eliminated by BootSnap)

  103. Lower Memory

  104. Faster Runtime No More "Cache Check" Penalty

  105. DETOUR!

  106. 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
  107. Benchmark https://github.com/rails/rails/issues/35257

  108. 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
  109. I was convinced the cache didn’t work

  110. I was convinced the cache didn’t work

  111. I was convinced the cache didn’t work

  112. I was convinced the cache didn’t work

  113. But, there were entries in the cache

  114. 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
  115. Cache Key Calculation Was More Expensive Than Template Execution

  116. 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
  117. 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
  118. "Cache" doesn’t mean "fast"

  119. Making Render Fast

  120. Make Render Usually Fast

  121. "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
  122. 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
  123. "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
  124. "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
  125. "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
  126. 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
  127. 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
  128. Wrap It Up

  129. Be "Context Free" Don’t depend on side effects

  130. Be Consistent

  131. Cache != Fast

  132. Benchmark First!

  133. Make Your Locals Match

  134. Thank You!!!