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. The View Is Clear from Here

    View Slide

  2. Aaron Patterson

    View Slide

  3. @tenderlove

    View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. I have stickers of my cat!
    (please say "hello")

    View Slide

  8. View Slide

  9. View Slide

  10. Ruby Core Team

    View Slide

  11. Rails Core Team

    View Slide

  12. G GitHub

    View Slide

  13. Le Git

    View Slide

  14. git push -f

    View Slide

  15. git checkout -b

    View Slide

  16. git cherry-pick

    View Slide

  17. View Slide

  18. Happy GPS Rollover Day!
    April 6, 23:59:42 UTC
    I guess this is actually tomorrow……

    View Slide

  19. GPS Uses 10 bits!

    View Slide

  20. Single Quotes Are 2x Faster
    Than Double Quotes

    View Slide

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

    View Slide

  22. Logic
    AND OR
    NOT
    A

    View Slide

  23. Гематоген

    View Slide

  24. "атоген"

    View Slide

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

    View Slide

  26. The View Is Clear from Here

    View Slide

  27. WARNING!@
    There Will Be Code

    View Slide

  28. There Will Be Ruby

    View Slide

  29. There Will Be Rails

    View Slide

  30. Software Engineering Practices

    View Slide

  31. Rails is MVC

    View Slide

  32. MVC
    Model
    View
    Controller

    View Slide

  33. MVC
    Model View
    Controller
    Request

    View Slide

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

    View Slide

  35. Action View

    View Slide

  36. Action View
    Finds Templates
    Compiles Templates
    Executes Views

    View Slide

  37. Finding Templates

    View Slide

  38. View Templates

    View Slide

  39. ERB

    View Slide

  40. Developed by @m_seki

    View Slide

  41. ERB Template
    New User
    <%= render_form %>
    <%= link_to 'Back' %>
    new.html.erb

    View Slide

  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

    View Slide

  43. Compiled Template
    _erbout = +""
    _erbout << "New User\n\n".freeze
    _erbout << render_form.to_s
    _erbout << "\n\n".freeze
    _erbout << link_to("Back").to_s
    _erbout << "\n".freeze
    _erbout

    View Slide

  44. Modified Compile Script
    require "erb"
    def render_form
    ""
    end
    def link_to(text)
    "#{text}"
    end
    # Compile the template
    template = ERB.new File.read "test.html.erb"
    # Evaluate the template
    puts template.result(binding)

    View Slide

  45. Compiler Output
    $ ruby compiler.rb
    New User

    Back

    View Slide

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

    View Slide

  47. Capturing Blocks

    <%= capture do %>
    Hello!
    <% end %>

    require "erb"
    def capture
    yield
    end
    # Compile the template
    template = ERB.new File.read "test.html.erb"
    puts template.result(binding)

    View Slide

  48. What is the output?
    Hello!



    Hello!

    ERROR!

    View Slide

  49. Compiled Source
    _erbout = +''
    _erbout << "\n ".freeze
    _erbout << ( capture do ).to_s
    _erbout << "\n Hello!\n ".freeze
    end
    _erbout << "\n\n".freeze
    _erbout

    <%= capture do %>
    Hello!
    <% end %>

    View Slide

  50. How does this work in Rails?

    View Slide

  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

    View Slide

  52. ERB in Rails

    View Slide

  53. Different ERB Flavors
    Original ERB
    ERubis
    ERubi

    View Slide

  54. ERB Performance

    View Slide

  55. Rendering Speed
    require "erb"
    require "benchmark/ips"
    class AaronView
    TEMPLATES = { }
    TEMPLATES["index"] = <<-eerb


    Hello <%= name %>

    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

    View Slide

  56. Cache Compilation
    class AaronView
    TEMPLATES = { }
    TEMPLATES["index"] = <<-eerb


    Hello <%= name %>

    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

    View Slide

  57. Define a Method
    class AaronView
    TEMPLATES = { }
    TEMPLATES["index"] = <<-eerb


    Hello <%= name %>

    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

    View Slide

  58. Method Generation
    require "erb"
    erb_source = <<-erb


    Hello <%= name %>

    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 << "\n \n Hello ".freeze
    _erbout << name.to_s
    _erbout << "\n\n".freeze
    _erbout
    end
    Template
    Source
    Compile
    Source
    Define
    a
    m
    ethod
    Render the
    Tem
    plate
    Compiled Source

    View Slide

  59. Templates Are Translated to
    Methods

    View Slide

  60. Rails Template Methods

    This is an inner template
    <% puts methods.grep(/_app/) %>

    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

    View Slide

  61. Memory Size
    def render_index
    _erbout = +''
    _erbout << "\n \n Hello ".freeze
    _erbout << name.to_s
    _erbout << "\n\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

    View Slide

  62. Template Handling
    In Rails

    View Slide

  63. Instance Variable Visibility
    This is template Foo!
    <%= @some_ivar %>
    This is template Bar!
    <%= @some_ivar %>
    _foo.html.erb _bar.html.erb

    View Slide

  64. Compiled Templates
    class ActionView::Base
    def render_foo
    _erbout = +''
    _erbout << "This is template Foo!\n".freeze
    _erbout << @some_ivar.to_s
    _erbout << "\n".freeze
    _erbout
    end
    def render_bar
    _erbout = +''
    _erbout << "This is template Barr!\n".freeze
    _erbout << @some_ivar.to_s
    _erbout << "\n".freeze
    _erbout
    end
    end
    _foo.html.erb
    _bar.html.erb
    Same Instance
    Same Instance

    View Slide

  65. Don’t use instance variables in
    your templates

    View Slide

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

    View Slide

  67. Instance Variable Visibility
    This is template Foo!
    <%= @some_ivar %>
    This is template Bar!
    <%= @some_ivar %>
    _foo.html.erb _bar.html.erb

    View Slide

  68. Local Variables

    View Slide

  69. Rendering With Locals

    <%= render "content", locals: { name: "Gorby" } %>

    View Slide

  70. Compiling With Locals

    Hello, <%= name %>

    def render_content
    _erbout = +''
    _erbout << "\n Hello, ".freeze
    _erbout << name.to_s
    _erbout << "\n\n".freeze
    _erbout
    end
    content.html.erb Compiled Method
    Method Call

    View Slide

  71. Preamble With Locals

    View Slide

  72. Compiling With Locals

    Hello, <%= name %>

    def render_content(locals)
    # Locals preamble
    name = locals[:name]
    _erbout = +''
    _erbout << "\n Hello, ".freeze
    _erbout << name.to_s
    _erbout << "\n\n".freeze
    _erbout
    end
    content.html.erb Compiled Method

    View Slide

  73. Templates Require Context

    <%= render "content",
    locals: { name: "Gorby" } %>
    <%= render "content" %>


    Hello, <%= name %>

    main.html.erb content.html.erb
    Method call or
    local variable?
    local variable
    method call

    View Slide

  74. Templates Can’t Be Compiled
    In Advance

    View Slide

  75. Compiled Too Many Times

    View Slide

  76. Too Many Compilations

    <%= render "content",
    locals: { name: "Gorby" } %>
    <%= render "content",
    locals: { name: "Gorby",
    friend: "Aaron" } %>

    def render_content_1(locals)
    # Locals preamble
    name = locals[:name]
    _erbout = +''
    _erbout << "\n Hello, ".freeze
    _erbout << name.to_s
    _erbout << "\n\n".freeze
    _erbout
    end
    def render_content_2(locals)
    # Locals preamble
    name = locals[:name]
    friend = locals[:friend]
    _erbout = +''
    _erbout << "\n Hello, ".freeze
    _erbout << name.to_s
    _erbout << "\n\n".freeze
    _erbout
    end
    main.html.erb Compiled "content"
    Unique Preambles

    View Slide

  77. "This is nice, but
    what should I do?"

    View Slide

  78. Always pass the same locals to
    individual templates.

    View Slide

  79. The render function

    View Slide

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

    View Slide

  81. Find a Template
    (from the cache)
    Compiled?
    Compile the Template
    Call the method
    Calculate a Method
    Name

    View Slide

  82. Finding a Template

    View Slide

  83. Template Rendering Depends
    on "Requested Format"

    View Slide

  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/*

    View Slide

  85. Template Source
    XML template Users
    index.xml.erb index.html.erb

    View Slide

  86. Requests and Rendering
    Request Response Template Rendered
    curl -H 'Accept: application/xml'
    http://localhost:3000/users
    XML template index.xml.erb
    curl http://localhost:3000/users Users index.html.erb
    curl -H 'Accept: text/html' http://
    localhost:3000/users
    Users index.html.erb

    View Slide

  87. Template Cache Keys
    • Local variables
    • Format
    • Locale
    • Variant (iPhone, etc)

    View Slide

  88. Strange Render Behavior

    View Slide

  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/*

    View Slide

  90. Template Contents
    Users
    <%= render "my_template" %>
    I guess this is a png?
    index.html.erb _my_template.png.erb

    XML is cool!

    _my_template.xml.erb

    View Slide

  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
    Users

    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

    View Slide

  92. Error!!

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  96. Context Dependent
    Users
    <%= 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

    View Slide

  97. Templates are not
    "Context Free"

    View Slide

  98. Prediction and Consistency

    View Slide

  99. Find a Template
    (from the cache)
    Compiled?
    Compile the Template
    Call the method
    Calculate a Method
    Name
    Cheap!
    Cheap!
    Expensive!

    View Slide

  100. What Method Will This Call?
    Users
    <%= render "my_template" %>
    Users
    <%= render_my_template_00011123abf %>
    index.html.erb translated template
    Calls a unique method.
    But what method?

    View Slide

  101. Find a Template
    (from the cache)
    Compiled?
    Compile the Template
    Call the method
    Calculate a Method
    Name
    Cheap!
    Cheap!
    Expensive!

    View Slide

  102. Slower Production Boot
    (but could be eliminated by BootSnap)

    View Slide

  103. Lower Memory

    View Slide

  104. Faster Runtime
    No More "Cache Check" Penalty

    View Slide

  105. DETOUR!

    View Slide

  106. Three Templates

    <%= render partial: "customer",
    collection: customers %>


    <%= render partial: "customer",
    collection: customers,
    cached: true %>

    _col.html.erb _cached_col.html.erb

    Hello: <%= customer.name %>

    _customer.html.erb

    View Slide

  107. Benchmark
    https://github.com/rails/rails/issues/35257

    View Slide

  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

    View Slide

  109. I was convinced
    the cache didn’t work

    View Slide

  110. I was convinced
    the cache didn’t work

    View Slide

  111. I was convinced
    the cache didn’t work

    View Slide

  112. I was convinced
    the cache didn’t work

    View Slide

  113. But, there were entries
    in the cache

    View Slide

  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

    View Slide

  115. Cache Key Calculation
    Was More Expensive
    Than Template Execution

    View Slide

  116. Cache Payoff

    Hello: <%= customer.name %>

    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Hello: <%= customer.name %>
    Old Template New Template

    View Slide

  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

    View Slide

  118. "Cache" doesn’t mean "fast"

    View Slide

  119. Making Render Fast

    View Slide

  120. Make Render Usually Fast

    View Slide

  121. "Same Format Assumption"
    Users
    <%= 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

    View Slide

  122. Ambiguous Render Problem
    Users
    <%= 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

    View Slide

  123. "Always Optimize"
    Users
    <%= render "my_template" %>
    <%= render "my_template", format: :html %>
    Users
    <%= render "my_template", format: :png %>
    <%= render "my_template", format: :html %>
    index.html.erb index.html.erb

    View Slide

  124. "Same Format Assumption"
    Users
    <%= 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

    View Slide

  125. "Same Format Assumption"
    Users
    <%= render "my_template" %>
    <%= render "my_template", format: :png %>
    Users
    <%= render_my_template_html_00123abc %>
    <%= render_my_template_png_00123abc %>
    index.html.erb Optimized Template

    View Slide

  126. Ambiguous Render Problem
    Users
    <%= 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

    View Slide

  127. Ambiguous Render Problem
    Users
    <%= render "my_template" %>
    <%= render "my_template", format: :html %>
    Users
    <%= render "my_template" %>
    <%= raise MissingTemplateError %>
    index.html.erb Optimized Template

    View Slide

  128. Wrap It Up

    View Slide

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

    View Slide

  130. Be Consistent

    View Slide

  131. Cache != Fast

    View Slide

  132. Benchmark First!

    View Slide

  133. Make Your Locals Match

    View Slide

  134. Thank You!!!

    View Slide